From 8e16bde49162ab18f6045934fe0d21e08ec032fb Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Tue, 15 Sep 2026 11:49:38 -0700 Subject: [PATCH] test: fold the schema-retry context test into the output-schema module Reuse tests/tools/test_delegate_output_schema.py's _StubChild instead of a new one-test file with its own double; the invariant (the retry turn sees is_delegated_child_context() True and the flag is restored afterwards) is unchanged. Trim the source comment to the WHY. --- tests/tools/test_delegate_output_schema.py | 24 +++++++++ .../test_delegate_schema_retry_context.py | 53 ------------------- tools/delegate_tool_child_run.py | 6 +-- 3 files changed, 26 insertions(+), 57 deletions(-) delete mode 100644 tests/tools/test_delegate_schema_retry_context.py diff --git a/tests/tools/test_delegate_output_schema.py b/tests/tools/test_delegate_output_schema.py index 0d9d3d9d0e..5e4ff7b9ed 100644 --- a/tests/tools/test_delegate_output_schema.py +++ b/tests/tools/test_delegate_output_schema.py @@ -297,6 +297,30 @@ class TestRunSingleChildSchemaValidation: assert entry["status"] == "completed" assert "error" not in entry + def test_retry_turn_runs_in_delegated_child_context(self, monkeypatch): + """The retry is a second run_conversation on the child, issued from the parent's + thread where HERMES_KANBAN_TASK is set. Unwrapped it carries the worker's identity, + so the kanban stop guard nudges the child toward a board tool it does not have + (#109735 / #87671).""" + from agent.delegation_context import is_delegated_child_context + from tools.delegate_tool_child_run import _validate_child_output_schema + + monkeypatch.setenv("HERMES_KANBAN_TASK", "t_parent") + seen: list = [] + + class _Child(_StubChild): + session_id = "sess-child" + + def run_conversation(self, user_message, task_id=None, **_kwargs): + seen.append(is_delegated_child_context()) + return super().run_conversation(user_message, task_id, **_kwargs) + + child = _Child(['{"city": "Berlin"}']) + child._delegate_output_schema = ADDRESS_SCHEMA + _validate_child_output_schema(child, {"final_response": "not json", "api_calls": 1}, 0, "child-0", None) + assert seen == [True] + assert is_delegated_child_context() is False + # --------------------------------------------------------------------------- # delegate_task dispatch-time schema handling diff --git a/tests/tools/test_delegate_schema_retry_context.py b/tests/tools/test_delegate_schema_retry_context.py deleted file mode 100644 index b3b040599a..0000000000 --- a/tests/tools/test_delegate_schema_retry_context.py +++ /dev/null @@ -1,53 +0,0 @@ -"""The schema-retry turn must run inside the delegated-child context. - -The main child turn is wrapped in ``delegated_child_context``; the bounded retry in -``_validate_child_output_schema`` is a second ``run_conversation`` on the same child. -Unwrapped, it runs with the parent worker's identity: ``HERMES_KANBAN_TASK`` is set and -nothing marks it as a child, so the kanban stop guard nudges the child to call -``kanban_complete``. A child owns no board task and cannot, so the nudge text displaces -the answer the retry exists to produce and the retry fails the same schema again. -""" - -from __future__ import annotations - -import pytest - -from agent.delegation_context import is_delegated_child_context -from tools.delegate_tool_child_run import _validate_child_output_schema - - -class _Child: - """Minimal stand-in recording the context state of each run_conversation turn.""" - - def __init__(self): - self._delegate_output_schema = { - "type": "object", - "properties": {"ok": {"type": "boolean"}}, - "required": ["ok"], - } - self.session_id = "sess-child" - self.seen_in_child_context = [] - - def run_conversation(self, user_message=None, task_id=None, stream_callback=None): - self.seen_in_child_context.append(is_delegated_child_context()) - return {"final_response": '{"ok": true}', "api_calls": 1} - - -@pytest.fixture -def kanban_env(monkeypatch): - monkeypatch.setenv("HERMES_KANBAN_TASK", "t_parent") - monkeypatch.delenv("HERMES_KANBAN_STOP_NUDGE", raising=False) - return monkeypatch - - -def test_schema_retry_runs_in_child_context(kanban_env): - child = _Child() - result = {"final_response": "not json at all", "api_calls": 1} - - assert is_delegated_child_context() is False - _validate_child_output_schema(child, result, 0, "child-task-0", None) - - assert child.seen_in_child_context == [True], ( - "the schema-retry turn ran outside delegated_child_context" - ) - assert is_delegated_child_context() is False diff --git a/tools/delegate_tool_child_run.py b/tools/delegate_tool_child_run.py index 6766dfed21..037cffee57 100644 --- a/tools/delegate_tool_child_run.py +++ b/tools/delegate_tool_child_run.py @@ -424,10 +424,8 @@ def _validate_child_output_schema( # schema re-paste — the child already holds the contract in its context). _retry_result = None try: - # Same identity as the main child turn (delegate_tool_child_run's worker): this runs - # on the parent's thread, where HERMES_KANBAN_TASK is set, so without the marker the - # kanban stop guard nudges the child to call kanban_complete. It owns no board task - # and cannot, and the nudge displaces the answer this retry exists to produce. + # Same identity as the main child turn: this runs on the parent worker's thread, and an + # unmarked turn is misread as the dispatcher-owned worker by every HERMES_KANBAN_* gate. from agent.delegation_context import delegated_child_context with delegated_child_context(str(getattr(child, "session_id", "") or "")): _retry_result = child.run_conversation(