From c7d8acb6a5a4cb137032c91d716b892c4bc79816 Mon Sep 17 00:00:00 2001 From: Patrick Buckley Date: Fri, 26 Jun 2026 08:24:04 -0700 Subject: [PATCH] fix(effect-status): harden effect_status decode + fix tests for typed synth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Turn.effect_status also catches TypeError: a corrupt non-string meta value (e.g. a dict that survived into the column) would otherwise crash a consumer on access, since EffectStatus(non-str) raises TypeError, not ValueError. Degrade to None, mirroring the meta decoders (Copilot review). - test_lowering: the wire-repair synth now carries the _effect_status side channel (stripped before the provider wire) — assert it. - test_session_mcp_dispatch_error: the _capture stub swallows the new status kwarg via **_ so it stays signature-compatible with _report_tool_result. --- tests/test_lowering.py | 3 +++ tests/test_session_mcp_dispatch_error.py | 6 +++++- turnstone/core/trajectory.py | 5 ++++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/test_lowering.py b/tests/test_lowering.py index d9224a23..e59f5338 100644 --- a/tests/test_lowering.py +++ b/tests/test_lowering.py @@ -126,6 +126,9 @@ def test_repair_synthesizes_trailing_orphan() -> None: "tool_call_id": "c1", "content": CANCELLED_TOOL_RESULT, "is_error": True, + # The unobserved synth carries the typed disposition (wire-invisible + # side channel, stripped by the translator before the provider wire). + "_effect_status": "unknown", } diff --git a/tests/test_session_mcp_dispatch_error.py b/tests/test_session_mcp_dispatch_error.py index 16fb729d..c1826053 100644 --- a/tests/test_session_mcp_dispatch_error.py +++ b/tests/test_session_mcp_dispatch_error.py @@ -105,7 +105,11 @@ def _record_outputs(session) -> list[tuple[str, str, str, bool]]: """Patch ``_report_tool_result`` to capture (call_id, name, output, is_error).""" captures: list[tuple[str, str, str, bool]] = [] - def _capture(call_id: str, name: str, output: str, *, is_error: bool = False) -> None: + def _capture( + call_id: str, name: str, output: str, *, is_error: bool = False, **_: object + ) -> None: + # ``**_`` swallows the typed ``status`` kwarg (and any future ones) so + # the stub stays signature-compatible with ``_report_tool_result``. captures.append((call_id, name, output, is_error)) session._report_tool_result = _capture # type: ignore[method-assign] diff --git a/turnstone/core/trajectory.py b/turnstone/core/trajectory.py index 93b324c6..d7c2b3c0 100644 --- a/turnstone/core/trajectory.py +++ b/turnstone/core/trajectory.py @@ -161,7 +161,10 @@ class Turn: return None try: return EffectStatus(raw) - except ValueError: + except (ValueError, TypeError): + # ValueError: not a known status string. TypeError: a corrupt + # non-string value (e.g. a dict survived into the meta). Mirror the + # meta decoders and degrade to None rather than crash a consumer. return None # -- construction helpers (blunt the wrapping cost of uniform block content) --