You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airflow.apache.org by vi...@apache.org on 2026/07/20 20:51:07 UTC

(airflow) branch main updated: Fix dag.test() dropping defer-time kwargs when trigger yields an event (#69942)

This is an automated email from the ASF dual-hosted git repository.

vincbeck pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 4be96fbd96a Fix dag.test() dropping defer-time kwargs when trigger yields an event (#69942)
4be96fbd96a is described below

commit 4be96fbd96a0b1d5717b705bb970c7fc67f2b0bd
Author: Sean Ghaeli <58...@users.noreply.github.com>
AuthorDate: Mon Jul 20 13:51:00 2026 -0700

    Fix dag.test() dropping defer-time kwargs when trigger yields an event (#69942)
    
    When DAG.test() runs a deferred task's trigger inline and the trigger
    yields an event, the kwargs passed at defer() time were discarded and
    replaced with only {"event": ...}. The resume method then ran with its
    default argument values.
    
    This broke example_neptune_analytics on the deferrable path:
    NeptuneCreatePrivateGraphEndpointOperator defers with
    kwargs={"vpc_id": self.vpc_id} and resumes into
    execute_complete(..., vpc_id=""). The lost vpc_id fell back to "" and
    was pushed to XCom; the downstream delete_endpoint task built a malformed
    request URI (/graphs/<id>/endpoints/) and the service rejected it with
    AccessDeniedException: Unable to determine service/operation name to be
    authorized.
    
    Merge the event into the deferral's next_kwargs instead of replacing
    them, mirroring the production resume path in airflow.models.trigger.
    Add a regression test.
---
 airflow-core/tests/unit/models/test_dag.py  | 27 +++++++++++++++++++++++++++
 task-sdk/src/airflow/sdk/definitions/dag.py | 11 ++++++++++-
 2 files changed, 37 insertions(+), 1 deletion(-)

diff --git a/airflow-core/tests/unit/models/test_dag.py b/airflow-core/tests/unit/models/test_dag.py
index 25d02d86686..ead765b3171 100644
--- a/airflow-core/tests/unit/models/test_dag.py
+++ b/airflow-core/tests/unit/models/test_dag.py
@@ -1958,6 +1958,33 @@ class TestDag:
         assert parked_states_seen == [TaskInstanceState.AWAITING_INPUT]
         assert resume_calls == [(["Approve"], {})]
 
+    @pytest.mark.execution_timeout(60)
+    def test_dag_test_preserves_defer_kwargs_through_inline_trigger(self, testing_dag_bundle):
+        """dag.test() must keep defer()-time kwargs when the inline trigger yields an event."""
+        from airflow.providers.standard.triggers.temporal import TimeDeltaTrigger
+
+        seen_kwargs: list = []
+
+        class DeferKwargOperator(BaseOperator):
+            def execute(self, context):
+                self.defer(
+                    trigger=TimeDeltaTrigger(datetime.timedelta(seconds=0)),
+                    method_name="resume",
+                    kwargs={"vpc_id": "vpc-abc123"},
+                )
+
+            def resume(self, context, event=None, vpc_id=""):
+                seen_kwargs.append(vpc_id)
+
+        dag = DAG(dag_id="test_dag_test_defer_kwargs", schedule=None, start_date=DEFAULT_DATE)
+        with dag:
+            DeferKwargOperator(task_id="defer_task")
+        sync_dag_to_db(dag)
+
+        dag.test()
+
+        assert seen_kwargs == ["vpc-abc123"]  # was [""] before the fix
+
     def test_dag_connection_file(self, tmp_path, testing_dag_bundle):
         test_connections_string = """
 ---
diff --git a/task-sdk/src/airflow/sdk/definitions/dag.py b/task-sdk/src/airflow/sdk/definitions/dag.py
index 5ba0254b208..cadfe697830 100644
--- a/task-sdk/src/airflow/sdk/definitions/dag.py
+++ b/task-sdk/src/airflow/sdk/definitions/dag.py
@@ -1544,7 +1544,16 @@ def _run_task(
                 trigger = import_string(msg.classpath)(**kwargs)
                 event = _run_inline_trigger(trigger, task_sdk_ti)
                 ti.next_method = msg.next_method
-                ti.next_kwargs = {"event": serialize(event.payload)} if event else msg.next_kwargs
+                # Add the trigger event to the kwargs the task passed to defer(), instead of
+                # discarding them, so the resume method still receives those kwargs.
+                if event:
+                    next_kwargs = deserialize(msg.next_kwargs) if msg.next_kwargs else {}
+                    if TYPE_CHECKING:
+                        assert isinstance(next_kwargs, dict)
+                    next_kwargs["event"] = event.payload
+                    ti.next_kwargs = serialize(next_kwargs)
+                else:
+                    ti.next_kwargs = msg.next_kwargs
                 log.info("[DAG TEST] Trigger completed")
 
                 # Set the state to SCHEDULED so that the task can be resumed.