Skip to content

test: regression tests for #5282 (after_run_callback on BaseNode roots)#5301

Draft
surfai wants to merge 1 commit intogoogle:mainfrom
surfai:test/5282-after-run-callback-regression
Draft

test: regression tests for #5282 (after_run_callback on BaseNode roots)#5301
surfai wants to merge 1 commit intogoogle:mainfrom
surfai:test/5282-after-run-callback-regression

Conversation

@surfai
Copy link
Copy Markdown

@surfai surfai commented Apr 12, 2026

Summary

Regression test for #5282: Runner._run_node_async does not dispatch plugin_manager.run_after_run_callback on the BaseNode path (the runners.py:427 TODO).

  • Baseline test — confirms on_user_message_callback, before_run_callback, and on_event_callback all fire on a Workflow root (they do, via _consume_event_queue at runners.py:619).
  • Regression anchor — strict xfail asserting after_run_callback == 1. Passes as xfail on stock 2.0.0a3. When the TODO is wired, delete the @xfail decorator and it flips green.
  • Workaround proofWorkaroundRunner subclass wrapping run_async to dispatch run_after_run_callback post-drain. Confirms the fix shape is a single await ic.plugin_manager.run_after_run_callback(invocation_context=ic) after the event queue drains.

The fix itself (one dispatch call after the _consume_event_queue loop in _run_node_async) is left to the maintainer — this PR provides the test harness and regression guard.

ContextVar note for WorkaroundRunner adopters

The WorkaroundRunner in the test uses self._last_ic to stash the InvocationContext. This is safe for single-invocation tests but races under concurrent run_async calls on a shared Runner instance — the second call overwrites _last_ic before the first call's post-drain dispatch. Production use of this pattern should store ic on a contextvars.ContextVar scoped to the async task instead of self.

adk api_server injection gap

The WorkaroundRunner subclass is only viable for teams instantiating Runner directly. adk api_server hardcodes Runner(...) at adk_web_server.py:737 with no injection point for a custom subclass. Teams using adk api_server (or adk web) have no workaround short of monkey-patching or the in-graph terminal-FunctionNode pattern described in #5282. Worth considering when the runners.py:427 fix lands — ensuring _run_node_async dispatches natively covers both paths.

Test plan

  • test_workflow_root_dispatches_pre_run_and_event_hooks — PASSED on 2.0.0a3
  • test_workflow_root_after_run_callback_not_dispatched — XFAIL (strict) on 2.0.0a3
  • test_workaround_runner_dispatches_after_run_callback — PASSED on 2.0.0a3

When runners.py:427 is resolved:

  • Remove @pytest.mark.xfail from test_workflow_root_after_run_callback_not_dispatched — it should PASS

Fixes #5282

…e roots)

Three tests for Runner._run_node_async plugin lifecycle on Workflow roots:

- Baseline: pre-run + event hooks fire (PASSED on 2.0.0a3)
- Regression anchor: after_run_callback strict xfail (remove when
  runners.py:427 TODO is wired — flips green as the signal)
- Workaround proof: Runner subclass wrapping run_async dispatches
  after_run_callback post-drain (PASSED on 2.0.0a3)

Closes google#5282
@adk-bot adk-bot added the tools [Component] This issue is related to tools label Apr 12, 2026
surfai added a commit to surfai/adk-python that referenced this pull request Apr 14, 2026
…gle#5282)

Runner._run_node_async had a TODO at runners.py:427 for node runtime
plugin lifecycle, and never dispatched plugin_manager.run_after_run_callback.
Because both Workflow(BaseNode) and (post a3 refactor) LlmAgent roots funnel
through _run_node_async, any BasePlugin subclass that overrides
after_run_callback silently no-op'd — breaking the canonical pattern for
memory persistence, metrics emission, and post-run audit hooks.

This wires one dispatch call after _consume_event_queue drains, mirroring
the legacy path in _exec_with_plugin at runners.py:1230 exactly: outside
the finally block, so semantics match legacy (fires on natural drain only,
skipped on error in the loop, skipped on caller-break via GeneratorExit).

Also:
- Narrows the TODO at runners.py:427 from "tracing and plugin lifecycle"
  to "tracing" since plugin lifecycle is now wired.
- Incidental pyink reformat at runners.py:1451: pre-existing 82-char line
  on v2 HEAD, unrelated to google#5282 but required by the pyink CI gate.

Companion to test-only PR google#5301 (which established the regression anchor).
This PR ships the fix plus flips the anchor by replacing the WorkaroundRunner
scaffolding with a direct positive assertion.

Alternatives considered and rejected:
- Wrap in finally to fire on error/break — changes legacy contract.
- Lift dispatch to run_async call sites — requires plumbing ic out of
  _run_node_async, bigger blast radius.
- Extract a shared _with_plugin_lifecycle context manager — right long-term
  shape, but refactors the legacy path too and expands scope.

Fixes google#5282

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

tools [Component] This issue is related to tools

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Custom memory persistence silently no-ops on Workflow (BaseNode) roots in 2.x — no working post-run hook

2 participants