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