Files
turnstone/tests/_parity_832.py
T
Patrick Buckley e103af94e7 test(832): port the seam-coupled suites to the folded architecture
Seventeen files, ~1,300 tests, re-pointed or redesigned per the triage
ledger's recipes: wholesale turn-scripting moves to ModelTurnResult
fakes; streaming-behavior suites drive the REAL wrapper+consumer+drain
path through armed provider fakes (tests/_parity_832.arm_session — the
eager cancel_ref append every real adapter performs, exception elements
for creation-phase failures, sequential per-turn scripts, and the title
lane quieted: a provider-level fake otherwise loses its one-shot script
to best-effort title generation, which is why the old tests patched at
the session level); kwarg-capture suites assert through model_turn's
create_streaming call with system-prepend-aware index math; delegate
wrappers retired by the fold re-aim at their model_turn module twins.
Old-architecture pins are replaced by their new-world equivalents rather
than deleted: no shared cancel ref exists (pinned), the handle slot and
per-attempt refs carry the cancel surface, the retry gate reads the
serving lane's provider, and a superseded generation's death exits send
silently as cancelled — a named delta: no arbitrary exception class
escapes an orphaned thread anymore.

Full suite: 10651 passed, 10 skipped. The wire-payload goldens pass
untouched — the fold's lowering composition is byte-equivalent on every
provider's request path, as designed.
2026-08-06 01:04:32 -07:00

319 lines
12 KiB
Python

"""#832 replay-parity harness: scenario table + runner.
The fold's acceptance is a controller-determinism audit: with the plant's
chunk sequence held fixed, the streaming phase must produce an identical
UI event sequence and an identical committed message — modulo the deltas
the design's D12 table rules (docs/design/832-main-loop-model-turn.md,
local). This module is the shared half: the scenario scripts (drawn from
the dataflow map's V11 chunk-field→UI grid) and the runner that drives one
scenario through the session's streaming seam, recording everything the
turn observably produced.
Baselines are captured from the PRE-FOLD path (``UPDATE_832_PARITY=1``,
run at a tree where ``session.py`` is byte-identical to main — the fixture
commit's history proves it) into ``tests/data/parity_832/``. The assert
mode replays the same scripts through the current tree and compares
against the baseline, applying the D12 transforms where the design ruled
a behavior change. A mismatch outside a ruled transform is a fold
regression.
The provider fake arms ``cancel_ref`` EAGERLY (a closeable sentinel
appended inside ``create_streaming``, before the iterator is returned),
mirroring every real adapter — the post-fold wrapper classifies
creation-vs-midstream failures by that arming, so a fake that skipped it
would exercise only the creation arm (design gap-check G8).
"""
from __future__ import annotations
import json
import os
import re
from pathlib import Path
from typing import Any
from unittest.mock import MagicMock
from tests._session_helpers import RecordingUI, make_session
from turnstone.core.model_turn import ModelTurnResult
from turnstone.core.providers._protocol import (
ModelCapabilities,
StreamChunk,
ToolCallDelta,
UsageInfo,
)
from turnstone.core.trajectory import ProviderNative, ToolCall, Turn
def make_result(
content: str = "",
*,
tool_calls: list[dict[str, Any]] | None = None,
finish_reason: str = "stop",
usage: UsageInfo | None = None,
native_blocks: list[dict[str, Any]] | None = None,
producer: str = "openai-compatible",
wire_msgs: list[dict[str, Any]] | None = None,
) -> ModelTurnResult:
"""A ``ModelTurnResult`` shaped like the streaming wrapper's return —
triage recipe R1's patched-result form, for tests that only need "a
turn happened" and patch ``_stream_response`` wholesale. The Turn and
the ``tool_calls`` mirror are built from the same dicts, preserving
the #825 pairing invariant fakes must not break."""
calls = list(tool_calls or [])
tc_tuple = tuple(
ToolCall(
id=tc.get("id", ""),
name=tc.get("function", {}).get("name", ""),
arguments=tc.get("function", {}).get("arguments", ""),
)
for tc in calls
)
native = (
ProviderNative(producer=producer, blocks=tuple(native_blocks)) if native_blocks else None
)
return ModelTurnResult(
turn=Turn.assistant(content, tool_calls=tc_tuple, native=native),
finish_reason=finish_reason,
usage=usage,
tool_calls=calls,
wire_msgs=wire_msgs,
producer=producer,
)
FIXTURE_DIR = Path(__file__).parent / "data" / "parity_832"
UPDATE = os.environ.get("UPDATE_832_PARITY") == "1"
class ArmedHandle:
"""Closeable sentinel standing in for the SDK stream handle."""
def __init__(self) -> None:
self.closed = False
def close(self) -> None:
self.closed = True
def arm_session(
session: Any,
*streams: Any,
retryable: frozenset[str] = frozenset({"IncompleteStreamError"}),
name: str = "openai-compatible",
) -> MagicMock:
"""Install a sequential multi-turn armed provider fake on *session*.
Each ``create_streaming`` call serves the next element of *streams*:
an iterable/generator is armed (a closeable sentinel appended to
``cancel_ref`` — the eager append every real adapter performs, which
the fold's creation-vs-midstream classifier keys on) and returned to
be consumed once; an EXCEPTION instance is raised at create time
WITHOUT arming — a creation-phase failure the per-lane ladder owns.
Calls beyond the script fail loudly (the pre-fold lax consumer used
to absorb an exhausted iterator as a silent empty turn; the strict
finish gate rejects that now, so an under-scripted test must say so).
Title generation is latched off — with a provider-LEVEL fake the
best-effort title lane would otherwise consume the first script
before the main loop ran.
"""
session._title_generated = True
provider = MagicMock()
provider.provider_name = name
provider.get_capabilities.return_value = ModelCapabilities()
provider.retryable_error_names = retryable
provider._armed_handle = MagicMock()
remaining = list(streams)
def _create(**kwargs: Any):
assert remaining, "arm_session: script exhausted — send looped for more turns than scripted"
nxt = remaining.pop(0)
if isinstance(nxt, BaseException):
raise nxt
ref = kwargs.get("cancel_ref")
if ref is not None:
ref.append(provider._armed_handle)
return iter(nxt) if not hasattr(nxt, "__next__") else nxt
provider.create_streaming = MagicMock(side_effect=_create)
session._provider = provider
return provider
def scripted_provider(chunks: list[StreamChunk]) -> MagicMock:
"""Provider fake replaying *chunks*, arming ``cancel_ref`` eagerly.
Assign to ``session._provider`` (never mutate a resolved provider —
the create_provider singleton rule in ``_session_helpers``). Each
call returns a FRESH iterator over the same script so ladder tests
re-drive it; the armed handle is appended per call, matching the
one-handle-per-create behavior of every real adapter.
"""
provider = MagicMock()
provider.provider_name = "openai-compatible"
provider.get_capabilities.return_value = ModelCapabilities()
provider.retryable_error_names = frozenset({"IncompleteStreamError"})
def _create(**kwargs: Any):
ref = kwargs.get("cancel_ref")
if ref is not None:
ref.append(ArmedHandle())
return iter(chunks)
provider.create_streaming = MagicMock(side_effect=_create)
return provider
def _tc(index: int, call_id: str, name: str = "", args: str = "") -> ToolCallDelta:
return ToolCallDelta(index=index, id=call_id, name=name, arguments_delta=args)
_USAGE_A = UsageInfo(prompt_tokens=11, completion_tokens=0, total_tokens=11)
_USAGE_B = UsageInfo(prompt_tokens=11, completion_tokens=7, total_tokens=18)
# Scenario table — the V11 grid, one script per row. Scripts are chunk
# LISTS; the runner re-iterates a fresh iterator per attempt.
SCENARIOS: dict[str, list[StreamChunk]] = {
"content_only": [
StreamChunk(content_delta="Hello "),
StreamChunk(content_delta="world."),
StreamChunk(finish_reason="stop", usage=_USAGE_B),
],
"reasoning_then_content": [
StreamChunk(reasoning_delta="think a", usage=_USAGE_A),
StreamChunk(reasoning_delta=" think b"),
StreamChunk(content_delta="Answer."),
StreamChunk(finish_reason="stop", usage=_USAGE_B),
],
"tools_simple": [
StreamChunk(content_delta="Calling."),
StreamChunk(tool_call_deltas=[_tc(0, "call_1", "get_weather", '{"city": ')]),
StreamChunk(tool_call_deltas=[_tc(0, "", "", '"Paris"}')]),
StreamChunk(finish_reason="tool_calls", usage=_USAGE_B),
],
"combined_content_tools_finish": [
StreamChunk(content_delta="Before "),
StreamChunk(
content_delta="tools",
tool_call_deltas=[_tc(0, "call_1", "get_weather", '{"city": "Nice"}')],
finish_reason="tool_calls",
),
StreamChunk(usage=_USAGE_B),
],
"info_prefinish": [
StreamChunk(info_delta="[Searching: pinniped taxonomy]"),
StreamChunk(content_delta="Seals are pinnipeds."),
StreamChunk(finish_reason="stop", usage=_USAGE_B),
],
"info_postfinish_footer": [
StreamChunk(content_delta="Answer with sources."),
StreamChunk(finish_reason="stop", usage=_USAGE_B),
StreamChunk(info_delta="Sources:\n- example.com/page"),
],
"think_tags_split_across_chunks": [
StreamChunk(content_delta="<thi"),
StreamChunk(content_delta="nk>plan</think>\n\nAnswer"),
StreamChunk(finish_reason="stop", usage=_USAGE_B),
],
"blank_id_tools": [
StreamChunk(tool_call_deltas=[_tc(0, "", "get_weather", '{"city": "Oslo"}')]),
StreamChunk(finish_reason="tool_calls", usage=_USAGE_B),
],
"length_with_tools": [
StreamChunk(content_delta="Partial answer"),
StreamChunk(tool_call_deltas=[_tc(0, "call_1", "get_weather", '{"city": "Par')]),
StreamChunk(finish_reason="length", usage=_USAGE_B),
],
"content_filter": [
StreamChunk(content_delta="Redac"),
StreamChunk(finish_reason="content_filter", usage=_USAGE_B),
],
"no_finish_clean_exhaust": [
StreamChunk(content_delta="Half an ans"),
StreamChunk(usage=_USAGE_A),
],
"finish_only_no_content": [
StreamChunk(finish_reason="stop", usage=_USAGE_B),
],
"provider_blocks_on_terminal": [
StreamChunk(content_delta="Blocked."),
StreamChunk(
finish_reason="stop",
usage=_USAGE_B,
provider_blocks=[{"type": "reasoning_text", "text": "captured"}],
),
],
}
_SYNTH_ID = re.compile(r"^call_[0-9a-f]{32}$")
def _mask_synth_ids(record: dict[str, Any]) -> dict[str, Any]:
"""Replace uuid-backfilled tool-call ids with stable placeholders.
The blank-id repair mints ``call_<uuid4hex>`` per run — real
nondeterminism inside the seam, but not behavior: mask ONLY that exact
shape (never a scripted provider id) with an index-stable token so
captures compare across runs. Applied to the committed projection;
UI events never carry call ids in this harness.
"""
result = record.get("result")
if not result:
return record
for i, tc in enumerate(result.get("tool_calls") or []):
if _SYNTH_ID.match(tc.get("id", "")):
tc["id"] = f"synth-id-{i}"
for i, block in enumerate(result.get("provider_content") or []):
if isinstance(block, dict) and _SYNTH_ID.match(str(block.get("id", ""))):
block["id"] = f"synth-id-{i}"
return record
def run_scenario(name: str) -> dict[str, Any]:
"""Drive one scenario through the streaming seam; return the record.
The record is everything the streaming phase observably produced:
the ordered UI events, the committed-message projection, the
mid-stream usage slot, and the exception class if the seam raised.
Deliberately seam-level (the ``_stream_response`` boundary pre-fold,
its wrapper successor post-fold) — full ``send()`` scenarios ride the
ported ladder suites instead.
"""
ui = RecordingUI()
session = make_session(ui=ui)
session._provider = scripted_provider(SCENARIOS[name])
session.messages.append(Turn.user("hi"))
record: dict[str, Any] = {"scenario": name}
try:
result = session._stream_response(0)
record["result"] = {
"content": result.content,
"tool_calls": result.tool_calls or None,
"provider_content": (
[dict(b) for b in result.turn.native.blocks] if result.turn.native else None
),
}
record["raised"] = None
except BaseException as exc: # noqa: BLE001 — the record IS the observation
record["result"] = None
record["raised"] = type(exc).__name__
record["ui_events"] = [[k, d] for k, d in ui.events]
record["last_usage"] = session._last_usage
record["cancelled_partial"] = session._cancelled_partial_msg
return _mask_synth_ids(record)
def fixture_path(name: str) -> Path:
return FIXTURE_DIR / f"{name}.json"
def load_fixture(name: str) -> dict[str, Any]:
return json.loads(fixture_path(name).read_text())
def write_fixture(name: str, record: dict[str, Any]) -> None:
FIXTURE_DIR.mkdir(parents=True, exist_ok=True)
fixture_path(name).write_text(json.dumps(record, indent=2, sort_keys=True) + "\n")