@@ -3102,6 +3102,95 @@ def g(v):
31023102 assert result == expected
31033103
31043104
3105+ def test_downstream_placeholder_handles_upstream_post_expansion (dag_maker , session ):
3106+ """
3107+ Test dynamic task mapping behavior when an upstream placeholder task
3108+ (map_index = -1) has been replaced by the first expanded task
3109+ (map_index = 0).
3110+
3111+ This verifies that trigger rule evaluation correctly resolves relevant
3112+ upstream map indexes both when referencing the original placeholder
3113+ and when referencing the first expanded task instance.
3114+ """
3115+
3116+ with dag_maker (session = session ) as dag :
3117+
3118+ @task
3119+ def get_mapping_source ():
3120+ return ["one" , "two" , "three" ]
3121+
3122+ @task
3123+ def mapped_task (x ):
3124+ output = f"{ x } "
3125+ return output
3126+
3127+ @task_group (prefix_group_id = False )
3128+ def the_task_group (x ):
3129+ start = MockOperator (task_id = "start" )
3130+ upstream = mapped_task (x )
3131+
3132+ # Plain downstream inside task group (no mapping source).
3133+ downstream = MockOperator (task_id = "downstream" )
3134+
3135+ start >> upstream >> downstream
3136+
3137+ mapping_source = get_mapping_source ()
3138+ mapped_tg = the_task_group .expand (x = mapping_source )
3139+
3140+ mapping_source >> mapped_tg
3141+
3142+ # Create DAG run and execute prerequisites.
3143+ dr = dag_maker .create_dagrun ()
3144+
3145+ dag_maker .run_ti ("get_mapping_source" , map_index = - 1 , dag_run = dr , session = session )
3146+
3147+ # Force expansion of the upstream mapped task.
3148+ upstream_task = dag .get_task ("mapped_task" )
3149+ _ , max_index = TaskMap .expand_mapped_task (
3150+ upstream_task ,
3151+ dr .run_id ,
3152+ session = session ,
3153+ )
3154+ expanded_ti_count = max_index + 1
3155+
3156+ downstream_task = dag .get_task ("downstream" )
3157+
3158+ # Grab the downstream placeholder TI.
3159+ downstream_ti = dr .get_task_instance (task_id = "downstream" , map_index = - 1 , session = session )
3160+ downstream_ti .refresh_from_task (downstream_task )
3161+
3162+ result = downstream_ti .get_relevant_upstream_map_indexes (
3163+ upstream = upstream_task ,
3164+ ti_count = expanded_ti_count ,
3165+ session = session ,
3166+ )
3167+
3168+ assert result == 0
3169+
3170+ # Now do the same for downstream expanded (map_index = 0) to ensure existing behavior is not broken.
3171+ # Force expansion of the downstream mapped task.
3172+ _ , max_index = TaskMap .expand_mapped_task (
3173+ downstream_task ,
3174+ dr .run_id ,
3175+ session = session ,
3176+ )
3177+ expanded_ti_count = max_index + 1
3178+
3179+ # Grab the first expanded downstream task. Behavior is the same for all cases where map_index >= 0.
3180+ downstream_ti = dr .get_task_instance (task_id = "downstream" , map_index = 0 , session = session )
3181+ downstream_ti .refresh_from_task (downstream_task )
3182+
3183+ result = downstream_ti .get_relevant_upstream_map_indexes (
3184+ upstream = upstream_task ,
3185+ ti_count = expanded_ti_count ,
3186+ session = session ,
3187+ )
3188+
3189+ # Verify behavior remains unchanged once the downstream task itself
3190+ # has expanded (map_index >= 0).
3191+ assert result == 0
3192+
3193+
31053194def test_find_relevant_relatives_with_non_mapped_task_as_tuple (dag_maker , session ):
31063195 """Test that specifying a non-mapped task as a tuple doesn't raise NotMapped exception."""
31073196 # t1 -> t2 (non-mapped) -> t3
0 commit comments