core/tests/test_workbench_replay.py
Shay 80e02ce7de feat(workbench): sealed single-turn replay backend — GET /replay/{turn_id} (Wave R3)
Replaces the W-026 501 stub. Re-executes a journaled prompt in a sealed
fresh runtime (ChatRuntime(no_load_state=True): no checkpoint load, no
checkpoint write, no proposal lineage) and compares the envelope
leaf-by-leaf against the recorded TurnJournalEntry.

Design correction vs the scoping doc (amended in-doc): journaled turns
each ran in a fresh ChatRuntime(), never one continuous session, so
genesis-PREFIX replay would manufacture spurious divergence; shipped
basis is sealed_fresh_runtime_single_turn (O(1)). origin_state:
"unrecorded" — the journal does not record whether the original turn's
runtime loaded a checkpoint, so divergence is reported as nondeterminism
OR origin-state influence, never disambiguated.

- workbench/replay.py: pure comparison + injected executor; every
  TurnJournalEntry field classified critical/informational exactly once
  (exhaustiveness enforced by test)
- api.py: route wiring under _CHAT_TURN_LOCK; runtime failure -> 500
  runtime_unavailable, no comparison may be fabricated
- schemas: additive TurnReplayComparison/TurnReplayDivergence (W-026
  artifact-keyed pair retires with the frontend Replay Moment PR)
- tests: 10 obligations incl. tamper-prompt, tamper-leaf precision,
  no-execution-no-comparison, no-trace (journal + engine_state bytes),
  wall-clock tolerance, sealed-construction proof
- snapshot regenerated; NOT_YET_MIRRORED debt entries for the two new
  classes (mirrors land with the frontend PR)
- api-contract-v1.md § Replay rewritten for the turn-keyed shape
2026-06-12 17:15:39 -07:00

192 lines
7.1 KiB
Python

"""Sealed single-turn replay — GET /replay/{turn_id} (Wave R3).
Proof obligations from docs/analysis/replay-moment-backend-scoping-2026-06-12.md:
each test here must MEANINGFULLY FAIL under the violation it is written to
catch (CLAUDE.md schema-defined proof obligations rule).
"""
from __future__ import annotations
from dataclasses import fields, replace
from pathlib import Path
import pytest
from chat.runtime import ChatRuntime
from workbench import api as workbench_api
from workbench.api import WorkbenchApi, _run_sealed_chat_turn, _with_turn_cost_and_id
from workbench.journal import TurnJournal, TurnJournalEntry
from workbench.replay import CRITICAL_FIELDS, INFORMATIONAL_FIELDS, replay_turn
_ENGINE_STATE_DIR = Path("engine_state")
def _snapshot(root: Path) -> dict[str, bytes]:
snap: dict[str, bytes] = {}
if not root.exists():
return snap
for path in sorted(root.rglob("*")):
if path.is_file() and "__pycache__" not in path.relative_to(root).parts:
snap[path.relative_to(root).as_posix()] = path.read_bytes()
return snap
@pytest.fixture(scope="module")
def recorded() -> tuple[TurnJournalEntry, object]:
"""One sealed-origin turn, journaled — the genesis-true positive control.
Module-scoped: the sealed runtime turn is the expensive part; every test
derives tampered copies from this single recorded entry.
"""
result = _run_sealed_chat_turn("What is truth?")
result = _with_turn_cost_and_id(result, 7, 1)
entry = TurnJournalEntry.from_chat_turn(result, turn_id=1)
return entry, result
def _api_with_entry(tmp_path: Path, entry: TurnJournalEntry) -> WorkbenchApi:
journal = TurnJournal(journal_dir=tmp_path / "workbench_data")
journal.append(entry)
return WorkbenchApi(journal=journal)
def test_field_classification_is_exhaustive() -> None:
"""A future journal field must force an explicit severity decision."""
names = {spec.name for spec in fields(TurnJournalEntry)}
assert CRITICAL_FIELDS | INFORMATIONAL_FIELDS == names
assert not CRITICAL_FIELDS & INFORMATIONAL_FIELDS
def test_sealed_origin_turn_replays_equivalent(tmp_path, recorded) -> None:
"""Positive control: genesis-origin entry replays bit-equivalent."""
entry, _ = recorded
api = _api_with_entry(tmp_path, entry)
response = api.handle("GET", "/replay/1", b"")
assert response.status == 200
data = response.payload["data"]
assert data["equivalent"] is True
assert data["comparison_basis"] == "sealed_fresh_runtime_single_turn"
assert data["origin_state"] == "unrecorded"
assert data["original_trace_hash"] == entry.trace_hash
assert data["replay_trace_hash"] == entry.trace_hash
assert all(d["severity"] == "informational" for d in data["divergences"])
def test_tampered_prompt_breaks_equivalence(tmp_path, recorded) -> None:
"""Obligation 1: mutating the recorded prompt flips equivalent to false."""
entry, _ = recorded
tampered = replace(entry, prompt="What is beauty?")
api = _api_with_entry(tmp_path, tampered)
response = api.handle("GET", "/replay/1", b"")
assert response.status == 200
data = response.payload["data"]
assert data["equivalent"] is False
critical_paths = {d["path"] for d in data["divergences"] if d["severity"] == "critical"}
assert critical_paths # the different prompt produced a different envelope
def test_tampered_surface_diverges_at_exactly_that_leaf(tmp_path, recorded) -> None:
"""Obligation 2: comparison reads the RECORDED entry, leaf-precise."""
entry, _ = recorded
tampered = replace(entry, surface=entry.surface + " [tampered]")
api = _api_with_entry(tmp_path, tampered)
response = api.handle("GET", "/replay/1", b"")
data = response.payload["data"]
assert data["equivalent"] is False
critical = [d for d in data["divergences"] if d["severity"] == "critical"]
assert [d["path"] for d in critical] == ["surface"]
assert critical[0]["original"] == entry.surface + " [tampered]"
assert critical[0]["replay"] == entry.surface
def test_no_comparison_without_real_execution(tmp_path, recorded, monkeypatch) -> None:
"""Obligation 3: a digest-to-itself shortcut cannot pass.
If the runtime cannot execute, the endpoint must surface a typed error
and never a fabricated comparison — equivalent: true is unreachable
without a real re-execution.
"""
entry, _ = recorded
api = _api_with_entry(tmp_path, entry)
def broken(prompt: str):
raise RuntimeError("runtime unavailable in test")
monkeypatch.setattr(workbench_api, "_run_sealed_chat_turn", broken)
response = api.handle("GET", "/replay/1", b"")
assert response.status == 500
assert response.payload["error"]["code"] == "runtime_unavailable"
assert "data" not in response.payload or not response.payload.get("data")
def test_replay_leaves_no_trace(tmp_path, recorded) -> None:
"""Obligation 4: GET /replay writes nothing — journal and engine state."""
entry, _ = recorded
journal = TurnJournal(journal_dir=tmp_path / "workbench_data")
journal.append(entry)
api = WorkbenchApi(journal=journal)
journal_before = _snapshot(tmp_path / "workbench_data")
engine_state_before = _snapshot(_ENGINE_STATE_DIR)
response = api.handle("GET", "/replay/1", b"")
assert response.status == 200
assert _snapshot(tmp_path / "workbench_data") == journal_before
assert _snapshot(_ENGINE_STATE_DIR) == engine_state_before
def test_wall_clock_divergence_does_not_break_equivalence(tmp_path, recorded) -> None:
"""Obligation 5: timestamp/cost/digest differ freely; equivalence holds."""
entry, _ = recorded
aged = replace(
entry,
timestamp="1970-01-01T00:00:00+00:00",
turn_cost_ms=999_999,
journal_digest="sha256:not-a-real-digest",
)
api = _api_with_entry(tmp_path, aged)
response = api.handle("GET", "/replay/1", b"")
data = response.payload["data"]
assert data["equivalent"] is True
informational_paths = {d["path"] for d in data["divergences"]}
assert "timestamp" in informational_paths
assert "turn_cost_ms" in informational_paths
def test_unknown_and_malformed_turn_ids_refuse_404(tmp_path, recorded) -> None:
entry, _ = recorded
api = _api_with_entry(tmp_path, entry)
for raw in ("999", "not-a-number", ""):
response = api.handle("GET", f"/replay/{raw}", b"")
assert response.status == 404
assert response.payload["error"]["code"] == "not_found"
def test_sealed_runtime_construction_is_sealed() -> None:
"""The replay executor's runtime has no engine-state store at all."""
runtime = ChatRuntime(no_load_state=True)
assert runtime._engine_state_store is None
# checkpoint_engine_state must be a no-op, not an error.
runtime.checkpoint_engine_state()
def test_replay_turn_propagates_executor_failure(recorded) -> None:
"""replay_turn itself never swallows execution failure into a result."""
entry, _ = recorded
def broken(prompt: str):
raise RuntimeError("boom")
with pytest.raises(RuntimeError, match="boom"):
replay_turn(entry, execute=broken)