Files
turnstone/tests/_parity_832.py
T
Patrick Buckley 7a06f5e8bc refactor(session): make ModelLane the provider boundary (#979) (#989)
* refactor(session): make ModelLane the provider boundary (#979)

## Summary

This closes the model-lane ownership gap left by #832: `ChatSession` no longer stores raw provider/client handles. `ResolvedModelBinding` now carries the provider, client, model, capabilities, registry generation, and backend-auth configuration as one coherent snapshot.

- Atomically rebind existing sessions after model-registry changes while pinning each in-flight send, fallback, judge, output guard, task agent, title, compaction, perception, and voice operation to its initiating principal and binding.
- Fence UI publication, canonical trajectory folds, durable writes, streams, retries, child scopes, and judge work by generation. Stop can hand off to a successor without accepting late state; cancelled tools retain typed effect receipts, and concurrent approval batches resolve by exact cycle or call.
- Make create, fork, open, close, and delete race-safe with hidden `creating` reservations, incarnation-aware state tails, and an ACL-rechecked transaction that clones checkpoint-bounded history, configuration, project/persona state, and attachment references.
- Extend REST/OpenAPI and Python/TypeScript SDK contracts for create/fork inputs, routed-create metadata, live-workstream probes, targeted approvals, and structured cancellation results.
- Update architecture, storage, authentication, judge, channel, console, API, and SDK documentation, including regenerated architecture diagrams and OpenAPI artifacts.

## Validation

- SQLite suite: 11,188 passed, 9 skipped, 10 deselected
- PostgreSQL suite: 11,195 passed, 2 skipped, 10 deselected
- Live backend: 3 passed
- SSE recovery: 6 passed; browser recovery harness passed all scenarios
- Ruff: clean; 595 files correctly formatted
- mypy: 243 source files clean
- TypeScript: typecheck/build and 35 tests passed
- OpenAPI artifacts fresh; all 14 changed diagrams reproduce byte-for-byte
- `git diff --check` and Git LFS integrity clean

Closes #979.

* fix(deps): update nanoid for GHSA-2v37-7h3g-55p8

Refresh the transitive lock entry admitted by PostCSS so the TypeScript security gate no longer resolves the vulnerable custom-generator implementation.

Validation:
- npm ci
- npm audit --audit-level=moderate: 0 vulnerabilities
- TypeScript typecheck and build
- TypeScript tests: 35 passed

* fix(test): assert canonical model registry URLs

Replace prefix checks with exact canonical base URL assertions so the tests do not model incomplete URL validation.

Validation: tests/test_model_registry.py (185 passed); Ruff check/format; mypy.
2026-08-08 16:13:35 -07:00

236 lines
9.7 KiB
Python

"""#832 replay-parity harness: scenario table + runner.
The audit is controller determinism: 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 RULED behavior changes
restated in full on the transforms in ``test_832_parity.py``. This
module is the shared half: the scenario scripts (one row per
chunk-field→UI translation the consumer performs) and the runner that
drives one through the 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 pre-fold main)
into ``tests/data/parity_832/``. The runner adapts to EITHER world by
signature, so a recapture at an old tree records real old-world
behavior, and capture mode refuses to write a record whose failure is
the harness's own call shape. Assert mode replays the same scripts
through the current tree and compares against the baseline, applying the
ruled transforms; a mismatch outside a ruled transform is a 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 wrapper classifies
creation-vs-midstream failures by that arming, so a fake that skipped it
would exercise only the creation arm.
"""
from __future__ import annotations
import inspect
import json
import os
import re
from pathlib import Path
from typing import Any
from tests._session_helpers import (
RecordingUI,
make_session,
replace_session_lane,
scripted_provider,
)
from turnstone.core.providers._protocol import StreamChunk, ToolCallDelta, UsageInfo
from turnstone.core.trajectory import Turn
FIXTURE_DIR = Path(__file__).parent / "data" / "parity_832"
UPDATE = os.environ.get("UPDATE_832_PARITY") == "1"
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 at ``_stream_response`` — full ``send()`` scenarios ride
the ported ladder suites instead.
Signature-adaptive so ``UPDATE_832_PARITY=1`` at a PRE-fold tree
records real old-world behavior: the pre-fold seam was
``_stream_response(msgs, my_generation) -> dict``, the post-fold one
is ``_stream_response(my_generation) -> ModelTurnResult`` (wire
prepared inside). A harness-shape failure must never be recorded as
behavior — ``write_fixture`` refuses one.
"""
ui = RecordingUI()
session = make_session(ui=ui)
# Zero the ladder backoff: a scenario that reaches the mid-stream
# re-issue ladder (no_finish_clean_exhaust) must not sleep real
# exponential delays in a unit run. The retry-notice transform in
# test_832_parity hardcodes the matching "0s" wording.
session._RETRY_BASE_DELAY = 0
replace_session_lane(session, provider=scripted_provider(SCENARIOS[name]))
pre_fold = "msgs" in inspect.signature(type(session)._stream_response).parameters
record: dict[str, Any] = {"scenario": name}
try:
if pre_fold:
# Splatted: the pre-fold seam took (msgs, my_generation), and a
# literal two-argument call reads as an arity error against the
# signature this tree actually has.
pre_fold_args: tuple[Any, ...] = ([{"role": "user", "content": "hi"}], 0)
msg = session._stream_response(*pre_fold_args)
msg.pop("_wire_msgs", None)
record["result"] = {
"content": msg.get("content", ""),
"tool_calls": msg.get("tool_calls"),
"provider_content": msg.get("_provider_content"),
}
else:
session.messages.append(Turn.user("hi"))
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:
# A TypeError before ANY UI event is the harness's own call-shape
# failure (run_scenario's signature adapter no longer matches this
# tree's seam), not old-world behavior — refuse to destroy the
# baseline with it.
if record.get("raised") == "TypeError" and not record.get("ui_events"):
raise AssertionError(
f"parity capture for {name!r} died calling the seam (TypeError before "
f"any UI event) — fix run_scenario's signature adapter; do not record"
)
FIXTURE_DIR.mkdir(parents=True, exist_ok=True)
fixture_path(name).write_text(json.dumps(record, indent=2, sort_keys=True) + "\n")