@@ -3019,6 +3019,95 @@ def g(v):
30193019 assert result == expected
30203020
30213021
3022+ def test_downstream_placeholder_handles_upstream_post_expansion (dag_maker , session ):
3023+ """
3024+ Test dynamic task mapping behavior when an upstream placeholder task
3025+ (map_index = -1) has been replaced by the first expanded task
3026+ (map_index = 0).
3027+
3028+ This verifies that trigger rule evaluation correctly resolves relevant
3029+ upstream map indexes both when referencing the original placeholder
3030+ and when referencing the first expanded task instance.
3031+ """
3032+
3033+ with dag_maker (session = session ) as dag :
3034+
3035+ @task
3036+ def get_mapping_source ():
3037+ return ["one" , "two" , "three" ]
3038+
3039+ @task
3040+ def mapped_task (x ):
3041+ output = f"{ x } "
3042+ return output
3043+
3044+ @task_group (prefix_group_id = False )
3045+ def the_task_group (x ):
3046+ start = MockOperator (task_id = "start" )
3047+ upstream = mapped_task (x )
3048+
3049+ # Plain downstream inside task group (no mapping source).
3050+ downstream = MockOperator (task_id = "downstream" )
3051+
3052+ start >> upstream >> downstream
3053+
3054+ mapping_source = get_mapping_source ()
3055+ mapped_tg = the_task_group .expand (x = mapping_source )
3056+
3057+ mapping_source >> mapped_tg
3058+
3059+ # Create DAG run and execute prerequisites.
3060+ dr = dag_maker .create_dagrun ()
3061+
3062+ dag_maker .run_ti ("get_mapping_source" , map_index = - 1 , dag_run = dr , session = session )
3063+
3064+ # Force expansion of the upstream mapped task.
3065+ upstream_task = dag .get_task ("mapped_task" )
3066+ _ , max_index = TaskMap .expand_mapped_task (
3067+ upstream_task ,
3068+ dr .run_id ,
3069+ session = session ,
3070+ )
3071+ expanded_ti_count = max_index + 1
3072+
3073+ downstream_task = dag .get_task ("downstream" )
3074+
3075+ # Grab the downstream placeholder TI.
3076+ downstream_ti = dr .get_task_instance (task_id = "downstream" , map_index = - 1 , session = session )
3077+ downstream_ti .refresh_from_task (downstream_task )
3078+
3079+ result = downstream_ti .get_relevant_upstream_map_indexes (
3080+ upstream = upstream_task ,
3081+ ti_count = expanded_ti_count ,
3082+ session = session ,
3083+ )
3084+
3085+ assert result == 0
3086+
3087+ # Now do the same for downstream expanded (map_index = 0) to ensure existing behavior is not broken.
3088+ # Force expansion of the downstream mapped task.
3089+ _ , max_index = TaskMap .expand_mapped_task (
3090+ downstream_task ,
3091+ dr .run_id ,
3092+ session = session ,
3093+ )
3094+ expanded_ti_count = max_index + 1
3095+
3096+ # Grab the first expanded downstream task. Behavior is the same for all cases where map_index >= 0.
3097+ downstream_ti = dr .get_task_instance (task_id = "downstream" , map_index = 0 , session = session )
3098+ downstream_ti .refresh_from_task (downstream_task )
3099+
3100+ result = downstream_ti .get_relevant_upstream_map_indexes (
3101+ upstream = upstream_task ,
3102+ ti_count = expanded_ti_count ,
3103+ session = session ,
3104+ )
3105+
3106+ # Verify behavior remains unchanged once the downstream task itself
3107+ # has expanded (map_index >= 0).
3108+ assert result == 0
3109+
3110+
30223111def test_find_relevant_relatives_with_non_mapped_task_as_tuple (dag_maker , session ):
30233112 """Test that specifying a non-mapped task as a tuple doesn't raise NotMapped exception."""
30243113 # t1 -> t2 (non-mapped) -> t3
0 commit comments