Skip to content

Commit 4f9125a

Browse files
committed
test: replay sanitized protocol fixtures
1 parent 3c2605e commit 4f9125a

3 files changed

Lines changed: 81 additions & 2 deletions

File tree

scripts/protocol_fixture_recorder.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,17 @@ class ProtocolFixture(BaseModel):
5151
substitutions: dict[str, str] = Field(default_factory=dict)
5252

5353

54+
class ReplayResult(BaseModel):
55+
name: str
56+
protocol_family: ProtocolFamily
57+
status_matches: bool
58+
visible_bytes_match: bool
59+
expected_status_code: int
60+
actual_status_code: int
61+
expected_visible_bytes: int
62+
actual_visible_bytes: int
63+
64+
5465
def _substitution_for(secret: str) -> str:
5566
digest = blake3.blake3(secret.encode("utf-8")).hexdigest()
5667
return f"credential:blake3:{digest}"
@@ -312,10 +323,41 @@ def record_debug_upstream(
312323
return written
313324

314325

326+
def replay_fixtures(base_url: str, fixture_paths: list[str | Path]) -> list[ReplayResult]:
327+
results: list[ReplayResult] = []
328+
for path in fixture_paths:
329+
fixture = ProtocolFixture.model_validate_json(Path(path).read_text())
330+
exchange, visible_bytes, _substitutions = _http_exchange(
331+
base_url,
332+
fixture.exchange.method,
333+
fixture.exchange.path,
334+
headers=dict(fixture.exchange.request_headers),
335+
body=fixture.exchange.request_body,
336+
)
337+
results.append(
338+
ReplayResult(
339+
name=fixture.name,
340+
protocol_family=fixture.protocol_family,
341+
status_matches=exchange.status_code == fixture.exchange.status_code,
342+
visible_bytes_match=visible_bytes == fixture.expected_visible_bytes,
343+
expected_status_code=fixture.exchange.status_code,
344+
actual_status_code=exchange.status_code,
345+
expected_visible_bytes=fixture.expected_visible_bytes,
346+
actual_visible_bytes=visible_bytes,
347+
)
348+
)
349+
return results
350+
351+
315352
def main() -> int:
316353
parser = argparse.ArgumentParser(description=__doc__)
317354
parser.add_argument("--base-url", required=True, help="capsem-debug-upstream base URL")
318355
parser.add_argument("--out-dir", required=True, type=Path, help="fixture output directory")
356+
parser.add_argument(
357+
"--replay",
358+
action="store_true",
359+
help="replay written fixtures after recording and include replay results",
360+
)
319361
parser.add_argument(
320362
"--scenario",
321363
action="append",
@@ -328,7 +370,12 @@ def main() -> int:
328370
args.out_dir,
329371
scenarios=set(args.scenarios) if args.scenarios else None,
330372
)
331-
print(json.dumps({"written": [str(path) for path in written]}, indent=2))
373+
output: dict[str, Any] = {"written": [str(path) for path in written]}
374+
if args.replay:
375+
output["replay"] = [
376+
result.model_dump() for result in replay_fixtures(args.base_url, written)
377+
]
378+
print(json.dumps(output, indent=2))
332379
return 0
333380

334381

sprints/1.3-release-correction/tracker.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,8 +240,17 @@ next one, and stage only the files for that slice.
240240
- Proof: `uv run python -m pytest tests/test_protocol_fixture_recorder.py
241241
-q` (`1 passed in 1.81s`); `uv run ruff check
242242
scripts/protocol_fixture_recorder.py tests/test_protocol_fixture_recorder.py`.
243-
- [ ] RED/GREEN: replay covers Claude/Anthropic, OpenAI/Codex-compatible,
243+
- [x] RED/GREEN: replay covers Claude/Anthropic, OpenAI/Codex-compatible,
244244
Gemini/AGY-compatible, Ollama/OpenAI-compatible, MCP, and credential flows.
245+
- 2026-06-12 progress: the recorder now exposes `replay_fixtures()`, which
246+
reissues recorded fixtures against the local lab and validates response
247+
status plus stable visible-byte counts. The test records and replays
248+
Claude/Anthropic-shaped, Codex/OpenAI-compatible, AGY/Gemini-shaped,
249+
Ollama/OpenAI-compatible, OAuth, MCP tools/list, MCP tools/call, and
250+
credential-capture fixtures without public network.
251+
- Proof: `uv run python -m pytest tests/test_protocol_fixture_recorder.py
252+
-q` (`2 passed in 0.92s`); `uv run ruff check
253+
scripts/protocol_fixture_recorder.py tests/test_protocol_fixture_recorder.py`.
245254
- [ ] RED/GREEN: live-local Ollama probe uses host `gemma4:latest` through the
246255
Capsem-routed path and records/replays the resulting native Ollama and
247256
OpenAI-compatible traffic without installing Ollama in the guest.

tests/test_protocol_fixture_recorder.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,26 @@ def test_protocol_fixture_recorder_uses_debug_upstream_and_sanitizes(tmp_path):
6565
assert fixture.auth_mode in {"none", "bearer", "api_key", "oauth_code"}
6666
assert fixture.expected_ledger_rows
6767
assert fixture.expected_visible_bytes >= 0
68+
69+
70+
def test_protocol_fixture_replay_covers_recorded_flows(tmp_path):
71+
recorder = _load_recorder()
72+
subprocess.run(
73+
["cargo", "build", "-p", "capsem-debug-upstream"],
74+
cwd=PROJECT_ROOT,
75+
check=True,
76+
)
77+
proc = None
78+
try:
79+
proc, ready = start_debug_upstream()
80+
written = recorder.record_debug_upstream(ready["base_url"], tmp_path)
81+
results = recorder.replay_fixtures(ready["base_url"], written)
82+
finally:
83+
stop_process(proc)
84+
85+
assert {result.name for result in results} == {path.stem for path in written}
86+
assert all(result.status_matches for result in results)
87+
assert all(result.visible_bytes_match for result in results)
88+
assert {
89+
result.protocol_family for result in results
90+
} == {"model", "oauth", "mcp", "credential"}

0 commit comments

Comments
 (0)