scripts/test_review_protocol.py
scripts/test_review_protocol.pyBrowse 10 files
16,973 tokens
82,646 bytes
Token encoding: o200k_base
Snapshot 506f736
← Back to SKILL.md
1#!/usr/bin/env python32 3from __future__ import annotations4 5import copy6import hashlib7import json8import os9import subprocess10import sys11import tempfile12import unittest13from pathlib import Path14from typing import Any15from unittest import mock16 17sys.path.insert(0, str(Path(__file__).parent))18 19from review_protocol import (20 ProtocolError,21 _inventory_digest,22 _read_bytes,23 _validate_credited_receipt,24 _workspace_entries,25 validate_packet,26 validate_receipt_data,27 validate_reviewer_output,28)29from review_state import _content_fingerprint, _repository_fingerprint30 31 32class ReviewProtocolTest(unittest.TestCase):33 def setUp(self) -> None:34 self.temporary_directory = tempfile.TemporaryDirectory()35 self.root = Path(self.temporary_directory.name)36 self.evidence = self.root / "diff.patch"37 self.evidence.write_text("diff evidence\n")38 self.root_evidence = self.root / "root-evidence.txt"39 self.root_evidence.write_text("root evidence\n")40 self.new_evidence = self.root / "new-evidence.txt"41 self.new_evidence.write_text("new evidence\n")42 self.task_manifest = self.root / "task.paths"43 self.task_manifest.write_text("src/example.py\n")44 self.component_manifest = self.root / "api-contract.paths"45 self.component_manifest.write_text("src/example.py\n")46 self.status_path = self.root / "repository-status.bin"47 self.status_path.write_bytes(b" M src/example.py\0")48 self.ledger_path = self.root / "ledger.json"49 self.packet_path = self.root / "packet.json"50 self.receipt_path = self.root / "receipt.json"51 self.output_path = self.root / "reviewer-output.json"52 base = "1" * 4053 head = "2" * 4054 workspace = [55 {56 "path": "src/example.py",57 "kind": "file",58 "executable": False,59 "sha256": "d" * 64,60 }61 ]62 self.combined = _content_fingerprint(base, workspace)63 self.component = _content_fingerprint(base, workspace)64 tracked_diff_sha256 = hashlib.sha256(self.evidence.read_bytes()).hexdigest()65 status_sha256 = hashlib.sha256(self.status_path.read_bytes()).hexdigest()66 self.repository = _repository_fingerprint(67 content_fingerprint=self.combined,68 head=head,69 status_sha256=status_sha256,70 tracked_diff_sha256=tracked_diff_sha256,71 complete_diff_sha256=tracked_diff_sha256,72 unfiltered_status_sha256=status_sha256,73 unfiltered_content_fingerprint=_content_fingerprint(base, workspace),74 )75 self.review_state_path = self.root / "review-state.json"76 self.review_state = {77 "fingerprint": self.combined,78 "base": base,79 "head": head,80 "content_fingerprint": self.combined,81 "repository_fingerprint": self.repository,82 "status_sha256": status_sha256,83 "tracked_diff_sha256": tracked_diff_sha256,84 "complete_diff_sha256": tracked_diff_sha256,85 "complete_diff_paths": ["src/example.py"],86 "pathspecs": ["src/example.py"],87 "components": {88 "api-contract": {89 "content_fingerprint": self.component,90 "pathspecs": ["src/example.py"],91 "workspace": workspace,92 }93 },94 "workspace": workspace,95 "unfiltered": {96 "status_sha256": status_sha256,97 "workspace": workspace,98 },99 }100 self._write_json(self.review_state_path, self.review_state)101 self.packet = self._packet()102 self._write_packet(self.packet_path, self.packet)103 104 def tearDown(self) -> None:105 self.temporary_directory.cleanup()106 107 def _write_json(self, path: Path, value: object) -> None:108 path.write_text(json.dumps(value, indent=2, sort_keys=True))109 110 def _write_packet(self, path: Path, packet: dict[str, object]) -> None:111 self._write_json(Path(packet["ledger"]["path"]), packet["ledger"])112 self._write_json(path, packet)113 114 def _write_review_state(115 self, state: dict[str, object], packet: dict[str, object] | None = None116 ) -> None:117 packet = packet or copy.deepcopy(self.packet)118 self._write_json(self.review_state_path, state)119 state_artifact = next(120 artifact121 for artifact in packet["evidence_artifacts"]122 if artifact["id"] == "E-STATE"123 )124 state_artifact["sha256"] = hashlib.sha256(125 self.review_state_path.read_bytes()126 ).hexdigest()127 self._write_packet(self.packet_path, packet)128 129 def _validate_packet(self, path: Path | None = None) -> dict[str, object]:130 return validate_packet(path or self.packet_path, "task-123", self.ledger_path)131 132 def _validate_output(self, reviewer_id: str) -> dict[str, object]:133 return validate_reviewer_output(134 self.packet_path,135 reviewer_id,136 self.output_path,137 "task-123",138 self.ledger_path,139 )140 141 def _packet(self) -> dict[str, object]:142 packet: dict[str, Any] = {143 "schema_version": 1,144 "packet_overage_reason": "none",145 "task": {146 "id": "task-123",147 "original_requirement": "Preserve behavior and improve review convergence.",148 "risk_tier": "normal",149 "risk_reason": "Repository workflow only.",150 },151 "scope_contract": {152 "required_behavior": "Validate the review packet before dispatch.",153 "compatibility_requirements": "Preserve the existing fingerprint format.",154 "unsupported_cases": "Arbitrary Markdown parsing is unsupported.",155 "supported_alternative": "Use the reviewer brief manually.",156 },157 "repository": {158 "target": "origin/main",159 "merge_base": "1" * 40,160 "head": "2" * 40,161 "release_boundary": "v0.19.4",162 "status_evidence_id": "E-STATUS",163 "exclusions": [],164 "complete_diff_command": "git diff base...HEAD -- src/example.py",165 },166 "ledger": {167 "path": str(self.ledger_path),168 "task_id": "task-123",169 "round_fingerprint": self.combined,170 "authorized_round_budgets": [6],171 "current_round": 1,172 "remaining_budget": 5,173 "root_causes": [174 {175 "id": "ROOT_EXISTING",176 "status": "open",177 "inventory_ids": ["INV-1"],178 "contract_evidence_ids": ["E-DIFF"],179 },180 {181 "id": "ROOT_CLOSED",182 "status": "closed",183 "inventory_ids": ["INV-2"],184 "contract_evidence_ids": ["E-ROOT"],185 },186 ],187 },188 "manifests": {189 "task": str(self.task_manifest),190 "components": {"api-contract": str(self.component_manifest)},191 "dependency_map": {192 "api-contract": [193 {194 "pathspec": "src/example.py",195 "reason": "The source file defines the reviewed API contract.",196 }197 ]198 },199 },200 "review_state": {201 "evidence_id": "E-STATE",202 "revalidation_command": (203 "PYTHONDONTWRITEBYTECODE=1 uv run python review_state.py --base BASE"204 ),205 },206 "verification": {207 "preflight_results": [208 {209 "command": (210 "PYTHONDONTWRITEBYTECODE=1 uv run python -m unittest "211 "discover -s scripts"212 ),213 "result": "49 tests passed.",214 }215 ],216 "eligible_concurrent_gates": "none",217 "deferred_gates": (218 "pnpm i --frozen-lockfile; pnpm build; pnpm -r build-check; "219 'pnpm -r -F "@openai/*" dist:check; pnpm lint; pnpm test; '220 "pnpm format:check:changed"221 ),222 "credited_receipts": [],223 },224 "architecture_references": [],225 "evidence_artifacts": [226 {227 "id": "E-DIFF",228 "path": str(self.evidence),229 "sha256": hashlib.sha256(self.evidence.read_bytes()).hexdigest(),230 "role": "complete-diff",231 "purpose": "Complete raw diff.",232 },233 {234 "id": "E-ROOT",235 "path": str(self.root_evidence),236 "sha256": hashlib.sha256(237 self.root_evidence.read_bytes()238 ).hexdigest(),239 "role": "supporting",240 "purpose": "Existing root-cause evidence.",241 },242 {243 "id": "E-STATE",244 "path": str(self.review_state_path),245 "sha256": hashlib.sha256(246 self.review_state_path.read_bytes()247 ).hexdigest(),248 "role": "review-state",249 "purpose": "Authoritative review-state output.",250 },251 {252 "id": "E-STATUS",253 "path": str(self.status_path),254 "sha256": hashlib.sha256(self.status_path.read_bytes()).hexdigest(),255 "role": "repository-status",256 "purpose": "Unfiltered repository status.",257 },258 {259 "id": "E-NEW",260 "path": str(self.new_evidence),261 "sha256": hashlib.sha256(262 self.new_evidence.read_bytes()263 ).hexdigest(),264 "role": "supporting",265 "purpose": "Unowned evidence for a genuinely new root.",266 },267 ],268 "inventory": [269 {270 "id": "INV-1",271 "kind": "contract",272 "summary": "Public contract row.",273 "surface": "review packet validation",274 "producers": "implementer",275 "consumers": "packet validator and reviewers",276 "behavior": "invalid packets fail before dispatch",277 "exports": "review_protocol.py CLI",278 "adjacent": "reviewer-brief.md",279 "tests": "test_review_protocol.py",280 },281 {282 "id": "INV-2",283 "kind": "authority-data-flow",284 "summary": "Authority flow row.",285 "input_authority": "control-plane task ID and ledger path",286 "validation": "exact task, path, digest, and budget checks",287 "in_memory_state": "parsed packet and ledger",288 "persisted_state": "task-global ledger JSON",289 "retry_replay": "same external authority is supplied again",290 "output": "validated packet summary",291 "exception_exposure": "concise ProtocolError without packet contents",292 "cleanup_revocation": "not applicable",293 },294 ],295 "selected_high_risk_dimensions": [],296 "reviewer_assignments": [297 {298 "reviewer_id": "requirements",299 "primary_dimensions": ["requirement and scope"],300 "inventory_ids": ["INV-1"],301 "high_risk_dimensions": [],302 "expected_components": ["api-contract"],303 "evidence_ids": ["E-DIFF", "E-STATE", "E-STATUS"],304 },305 {306 "reviewer_id": "lifecycle",307 "primary_dimensions": ["security and protocol"],308 "inventory_ids": ["INV-2"],309 "high_risk_dimensions": [],310 "expected_components": ["api-contract"],311 "evidence_ids": ["E-DIFF", "E-STATE", "E-STATUS"],312 },313 ],314 }315 owned_evidence = {316 evidence_id317 for root in packet["ledger"]["root_causes"]318 for evidence_id in root["contract_evidence_ids"]319 }320 packet["ledger"]["contract_evidence_sha256"] = {321 artifact["id"]: artifact["sha256"]322 for artifact in packet["evidence_artifacts"]323 if artifact["id"] in owned_evidence324 }325 packet["ledger"]["inventory_sha256"] = {326 row["id"]: _inventory_digest(row) for row in packet["inventory"]327 }328 return packet329 330 def _receipt(self) -> dict[str, object]:331 fingerprints = {332 "combined": self.combined,333 "components": {"api-contract": self.component},334 "repository": self.repository,335 }336 return {337 "schema_version": 1,338 "command": (339 "PYTHONDONTWRITEBYTECODE=1 uv run python -m unittest discover -s scripts"340 ),341 "environment": "macOS, UV_DEFAULT_INDEX=https://pypi.org/simple",342 "exit_status": 0,343 "non_mutation_basis": "The command is documented as non-mutating.",344 "before": fingerprints,345 "after": copy.deepcopy(fingerprints),346 }347 348 def _output(self) -> dict[str, object]:349 return {350 "verdict": "clean",351 "reviewed_fingerprints": {352 "packet": hashlib.sha256(self.packet_path.read_bytes()).hexdigest(),353 "combined": self.combined,354 "components": {"api-contract": self.component},355 "repository": self.repository,356 },357 "checked_inventory_ids": ["INV-1"],358 "unchecked_inventory_ids": [],359 "high_risk_dimensions_checked": [],360 "focused_probes": [],361 "remaining_uncertainty": [],362 "findings": [],363 "sibling_scenario_scan": [],364 "inspection_call_count": 4,365 "inspection_budget_reason": "none",366 }367 368 def _finding(self, root_cause_id: str) -> dict[str, object]:369 return {370 "priority": "P2",371 "title": "Finding title",372 "location": "src/example.py:1",373 "failure_scenario": "The supported scenario fails.",374 "user_consequence": "The caller sees an error.",375 "support_basis": "Original requirement.",376 "baseline_patch_evidence": "The baseline succeeds.",377 "smallest_safe_correction": "Reuse the existing path.",378 "root_cause_id": root_cause_id,379 "root_cause_evidence": {380 "new_contract_evidence_ids": [],381 "new_inventory_ids": [],382 },383 }384 385 def test_valid_packet_reports_dispatch_digest_and_size(self) -> None:386 summary = self._validate_packet()387 388 self.assertEqual(summary["combined_fingerprint"], self.combined)389 self.assertEqual(summary["packet_size_bytes"], self.packet_path.stat().st_size)390 self.assertEqual(391 summary["packet_sha256"],392 hashlib.sha256(self.packet_path.read_bytes()).hexdigest(),393 )394 self.assertEqual(395 summary["ledger_sha256"], hashlib.sha256(self.ledger_path.read_bytes()).hexdigest()396 )397 398 def test_packet_rejects_duplicate_json_keys(self) -> None:399 packet_text = self.packet_path.read_text()400 self.packet_path.write_text(401 packet_text.replace(402 '"task": {\n "id": "task-123",',403 '"task": {\n "id": "task-123",\n "id": "task-123",',404 1,405 )406 )407 408 with self.assertRaisesRegex(ProtocolError, "Duplicate JSON key.*id"):409 self._validate_packet()410 411 def test_packet_rejects_non_finite_json_numbers(self) -> None:412 """Reject numeric constants and exponents that parse as non-finite."""413 for encoded_value in ("NaN", "1e999"):414 with self.subTest(encoded_value=encoded_value):415 packet = copy.deepcopy(self.packet)416 packet["ignored_number"] = 0417 self._write_packet(self.packet_path, packet)418 packet_text = self.packet_path.read_text().replace(419 '"ignored_number": 0',420 f'"ignored_number": {encoded_value}',421 1,422 )423 self.packet_path.write_text(packet_text)424 425 with self.assertRaisesRegex(ProtocolError, "Non-finite JSON number"):426 self._validate_packet()427 428 def test_packet_reports_json_parser_limits_as_protocol_errors(self) -> None:429 """Convert runtime parser limits into concise protocol failures."""430 for error in (ValueError("integer limit"), RecursionError("nesting limit")):431 with (432 self.subTest(error=type(error).__name__),433 mock.patch("review_protocol.json.loads", side_effect=error),434 self.assertRaisesRegex(ProtocolError, "Cannot read JSON object"),435 ):436 self._validate_packet()437 438 def test_packet_defers_broad_final_gates_until_clean_review(self) -> None:439 packet = copy.deepcopy(self.packet)440 packet["verification"]["eligible_concurrent_gates"] = "pnpm test"441 self._write_packet(self.packet_path, packet)442 with self.assertRaisesRegex(443 ProtocolError,444 "verification.eligible_concurrent_gates must be 'none'",445 ):446 self._validate_packet()447 448 packet["verification"]["eligible_concurrent_gates"] = "none"449 packet["verification"]["deferred_gates"] = "none"450 self._write_packet(self.packet_path, packet)451 with self.assertRaisesRegex(452 ProtocolError,453 "verification.deferred_gates must list the applicable broad final gates",454 ):455 self._validate_packet()456 457 def test_packet_fails_closed_on_missing_field_or_incomplete_assignment(458 self,459 ) -> None:460 cases = []461 missing = copy.deepcopy(self.packet)462 del missing["scope_contract"]463 cases.append(464 (missing, "Missing required packet field: scope_contract.required_behavior")465 )466 incomplete = copy.deepcopy(self.packet)467 incomplete["reviewer_assignments"][1]["inventory_ids"] = ["INV-1"]468 cases.append((incomplete, "must cover the exact inventory and dimensions"))469 470 for index, (packet, expected) in enumerate(cases):471 with self.subTest(expected=expected):472 path = self.root / f"invalid-{index}.json"473 self._write_packet(path, packet)474 with self.assertRaisesRegex(ProtocolError, re_escape(expected)):475 self._validate_packet(path)476 477 def test_packet_requires_indexed_ledger_evidence(self) -> None:478 packet = copy.deepcopy(self.packet)479 packet["ledger"]["root_causes"][0]["contract_evidence_ids"] = ["REQ-UNINDEXED"]480 self._write_packet(self.packet_path, packet)481 482 with self.assertRaisesRegex(ProtocolError, r"evidence=\['REQ-UNINDEXED'\]"):483 self._validate_packet()484 485 def test_packet_resolves_manifest_and_ledger_authority(self) -> None:486 missing_manifest = copy.deepcopy(self.packet)487 missing_manifest["manifests"]["task"] = str(self.root / "missing-task.paths")488 self._write_packet(self.packet_path, missing_manifest)489 with self.assertRaisesRegex(ProtocolError, "Cannot read manifests.task"):490 self._validate_packet()491 492 self.task_manifest.write_text("src/other.py\n")493 self._write_packet(self.packet_path, self.packet)494 with self.assertRaisesRegex(ProtocolError, "must match review_state.pathspecs"):495 self._validate_packet()496 self.task_manifest.write_text("src/example.py\n")497 498 missing_ledger = copy.deepcopy(self.packet)499 missing_ledger["ledger"]["path"] = str(self.root / "missing-ledger.json")500 self._write_json(self.packet_path, missing_ledger)501 with self.assertRaisesRegex(ProtocolError, "Cannot read ledger.path"):502 validate_packet(503 self.packet_path, "task-123", self.root / "missing-ledger.json"504 )505 506 divergent = copy.deepcopy(self.packet)507 self._write_packet(self.packet_path, divergent)508 ledger = copy.deepcopy(divergent["ledger"])509 ledger["remaining_budget"] = 99510 self._write_json(self.ledger_path, ledger)511 with self.assertRaisesRegex(512 ProtocolError, "must match the packet ledger exactly"513 ):514 self._validate_packet()515 516 self.ledger_path.write_text("{not json")517 with self.assertRaisesRegex(ProtocolError, "Cannot read JSON object"):518 self._validate_packet()519 520 def test_dependency_map_requires_exact_component_entries(self) -> None:521 """Require complete machine-readable component dependency boundaries."""522 valid_entry = {523 "pathspec": "src/example.py",524 "reason": "The source file defines the reviewed API contract.",525 }526 cases = (527 ("api-contract has no dependencies.", "dependency_map must be an object"),528 ({}, "must cover the exact component names"),529 ({"api-contract": []}, "must contain at least one dependency"),530 (531 {"api-contract": [{**valid_entry, "note": "unvalidated"}]},532 "unexpected=\\['note'\\]",533 ),534 (535 {"api-contract": [valid_entry, copy.deepcopy(valid_entry)]},536 "contains duplicate pathspec",537 ),538 )539 540 for dependency_map, expected in cases:541 with self.subTest(expected=expected):542 packet = copy.deepcopy(self.packet)543 packet["manifests"]["dependency_map"] = dependency_map544 self._write_packet(self.packet_path, packet)545 546 with self.assertRaisesRegex(ProtocolError, expected):547 self._validate_packet()548 549 def test_ledger_task_identity_must_match_packet(self) -> None:550 packet = copy.deepcopy(self.packet)551 packet["ledger"]["task_id"] = "another-task"552 self._write_packet(self.packet_path, packet)553 554 with self.assertRaisesRegex(ProtocolError, "ledger.task_id must match"):555 self._validate_packet()556 557 def test_control_plane_rejects_coordinated_task_and_ledger_replacement(558 self,559 ) -> None:560 packet = copy.deepcopy(self.packet)561 packet["task"]["id"] = "replacement-task"562 packet["ledger"]["task_id"] = "replacement-task"563 self._write_packet(self.packet_path, packet)564 565 with self.assertRaisesRegex(ProtocolError, "control-plane task ID"):566 self._validate_packet()567 568 replacement_ledger = self.root / "replacement-ledger.json"569 packet = copy.deepcopy(self.packet)570 packet["ledger"]["path"] = str(replacement_ledger)571 self._write_packet(self.packet_path, packet)572 with self.assertRaisesRegex(ProtocolError, "control-plane ledger path"):573 self._validate_packet()574 575 def test_ledger_budget_history_is_authoritative(self) -> None:576 packet = copy.deepcopy(self.packet)577 packet["ledger"]["remaining_budget"] = 99578 self._write_packet(self.packet_path, packet)579 580 with self.assertRaisesRegex(ProtocolError, "authorized budget history"):581 self._validate_packet()582 583 packet = copy.deepcopy(self.packet)584 packet["ledger"]["authorized_round_budgets"] = [True]585 packet["ledger"]["remaining_budget"] = 0586 self._write_packet(self.packet_path, packet)587 with self.assertRaisesRegex(ProtocolError, "must be a positive integer"):588 self._validate_packet()589 590 def test_ledger_round_fingerprint_matches_packet(self) -> None:591 packet = copy.deepcopy(self.packet)592 packet["ledger"]["round_fingerprint"] = "0" * 64593 self._write_packet(self.packet_path, packet)594 595 with self.assertRaisesRegex(ProtocolError, "must match the packet fingerprint"):596 self._validate_packet()597 598 def test_ledger_digest_maps_cover_exact_owned_ids(self) -> None:599 """Require digest bindings for exactly the IDs owned by roots."""600 packet = copy.deepcopy(self.packet)601 del packet["ledger"]["contract_evidence_sha256"]["E-ROOT"]602 self._write_packet(self.packet_path, packet)603 604 with self.assertRaisesRegex(ProtocolError, "must bind the exact owned IDs"):605 self._validate_packet()606 607 packet = copy.deepcopy(self.packet)608 packet["ledger"]["contract_evidence_sha256"]["E-NEW"] = hashlib.sha256(609 self.new_evidence.read_bytes()610 ).hexdigest()611 self._write_packet(self.packet_path, packet)612 613 with self.assertRaisesRegex(ProtocolError, r"unexpected=\['E-NEW'\]"):614 self._validate_packet()615 616 def test_ledger_digest_maps_match_indexed_content(self) -> None:617 """Reject ledger bindings that differ from current indexed content."""618 packet = copy.deepcopy(self.packet)619 packet["ledger"]["contract_evidence_sha256"]["E-ROOT"] = "0" * 64620 self._write_packet(self.packet_path, packet)621 622 with self.assertRaisesRegex(ProtocolError, "evidence digest mismatch for E-ROOT"):623 self._validate_packet()624 625 packet = copy.deepcopy(self.packet)626 packet["ledger"]["inventory_sha256"]["INV-2"] = "0" * 64627 self._write_packet(self.packet_path, packet)628 629 with self.assertRaisesRegex(ProtocolError, "inventory digest mismatch for INV-2"):630 self._validate_packet()631 632 def test_canonical_roots_cannot_alias_the_same_ownership(self) -> None:633 packet = copy.deepcopy(self.packet)634 alias = copy.deepcopy(packet["ledger"]["root_causes"][0])635 alias["id"] = "RENAMED_ROOT"636 packet["ledger"]["root_causes"].append(alias)637 self._write_packet(self.packet_path, packet)638 639 with self.assertRaisesRegex(ProtocolError, "overlap on inventory INV-1"):640 self._validate_packet()641 642 packet = copy.deepcopy(self.packet)643 alias = copy.deepcopy(packet["ledger"]["root_causes"][0])644 alias["id"] = "RENAMED_ROOT"645 alias["contract_evidence_ids"].append("E-NEW")646 packet["ledger"]["root_causes"].append(alias)647 self._write_packet(self.packet_path, packet)648 with self.assertRaisesRegex(ProtocolError, "overlap on inventory INV-1"):649 self._validate_packet()650 651 def test_prior_ledger_makes_history_append_only(self) -> None:652 prior_path = self.root / "prior-ledger.json"653 prior = copy.deepcopy(self.packet["ledger"])654 self._write_json(prior_path, prior)655 prior_digest = hashlib.sha256(prior_path.read_bytes()).hexdigest()656 657 current = copy.deepcopy(self.packet)658 current["ledger"]["current_round"] = 2659 current["ledger"]["remaining_budget"] = 4660 self._write_packet(self.packet_path, current)661 validate_packet(662 self.packet_path,663 "task-123",664 self.ledger_path,665 prior_path,666 prior_digest,667 )668 669 skipped = copy.deepcopy(self.packet)670 skipped["ledger"]["current_round"] = 3671 skipped["ledger"]["remaining_budget"] = 3672 self._write_packet(self.packet_path, skipped)673 with self.assertRaisesRegex(ProtocolError, "advance by exactly one"):674 validate_packet(675 self.packet_path,676 "task-123",677 self.ledger_path,678 prior_path,679 prior_digest,680 )681 682 reset = copy.deepcopy(current)683 reset["ledger"]["authorized_round_budgets"] = [2]684 reset["ledger"]["remaining_budget"] = 0685 self._write_packet(self.packet_path, reset)686 with self.assertRaisesRegex(ProtocolError, "must preserve the prior prefix"):687 validate_packet(688 self.packet_path,689 "task-123",690 self.ledger_path,691 prior_path,692 prior_digest,693 )694 695 removed_root = copy.deepcopy(current)696 removed_root["ledger"]["root_causes"] = [697 {698 "id": "REPLACEMENT_ROOT",699 "status": "open",700 "inventory_ids": ["INV-1", "INV-2"],701 "contract_evidence_ids": ["E-DIFF"],702 }703 ]704 del removed_root["ledger"]["contract_evidence_sha256"]["E-ROOT"]705 self._write_packet(self.packet_path, removed_root)706 with self.assertRaisesRegex(ProtocolError, "removed prior canonical root"):707 validate_packet(708 self.packet_path,709 "task-123",710 self.ledger_path,711 prior_path,712 prior_digest,713 )714 715 def test_prior_ledger_binds_owned_evidence_content(self) -> None:716 """Reject content replacement under a previously owned evidence ID."""717 prior_path = self.root / "prior-ledger.json"718 prior = copy.deepcopy(self.packet["ledger"])719 prior["contract_evidence_sha256"] = {720 "E-DIFF": hashlib.sha256(self.evidence.read_bytes()).hexdigest(),721 "E-ROOT": hashlib.sha256(self.root_evidence.read_bytes()).hexdigest(),722 }723 self._write_json(prior_path, prior)724 prior_digest = hashlib.sha256(prior_path.read_bytes()).hexdigest()725 726 self.root_evidence.write_text("replacement root evidence\n")727 packet = copy.deepcopy(self.packet)728 root_artifact = next(729 artifact for artifact in packet["evidence_artifacts"] if artifact["id"] == "E-ROOT"730 )731 replacement_digest = hashlib.sha256(self.root_evidence.read_bytes()).hexdigest()732 root_artifact["sha256"] = replacement_digest733 packet["ledger"]["contract_evidence_sha256"] = {734 "E-DIFF": hashlib.sha256(self.evidence.read_bytes()).hexdigest(),735 "E-ROOT": replacement_digest,736 }737 packet["ledger"]["current_round"] = 2738 packet["ledger"]["remaining_budget"] = 4739 self._write_packet(self.packet_path, packet)740 741 with self.assertRaisesRegex(ProtocolError, "changed prior evidence E-ROOT"):742 validate_packet(743 self.packet_path,744 "task-123",745 self.ledger_path,746 prior_path,747 prior_digest,748 )749 750 def test_prior_ledger_binds_owned_inventory_content(self) -> None:751 """Reject content replacement under a previously owned inventory ID."""752 prior_path = self.root / "prior-ledger.json"753 prior = copy.deepcopy(self.packet["ledger"])754 prior["inventory_sha256"] = {755 row["id"]: _inventory_digest(row) for row in self.packet["inventory"]756 }757 self._write_json(prior_path, prior)758 prior_digest = hashlib.sha256(prior_path.read_bytes()).hexdigest()759 760 packet = copy.deepcopy(self.packet)761 inventory_row = next(row for row in packet["inventory"] if row["id"] == "INV-2")762 inventory_row["validation"] = "replacement validation contract"763 packet["ledger"]["inventory_sha256"] = {764 row["id"]: _inventory_digest(row) for row in packet["inventory"]765 }766 packet["ledger"]["current_round"] = 2767 packet["ledger"]["remaining_budget"] = 4768 self._write_packet(self.packet_path, packet)769 770 with self.assertRaisesRegex(ProtocolError, "changed prior inventory INV-2"):771 validate_packet(772 self.packet_path,773 "task-123",774 self.ledger_path,775 prior_path,776 prior_digest,777 )778 779 def test_same_round_retry_requires_the_prior_fingerprint(self) -> None:780 prior_path = self.root / "prior-ledger.json"781 prior = copy.deepcopy(self.packet["ledger"])782 prior["round_fingerprint"] = "0" * 64783 self._write_json(prior_path, prior)784 prior_digest = hashlib.sha256(prior_path.read_bytes()).hexdigest()785 786 with self.assertRaisesRegex(ProtocolError, "same-round retry fingerprint"):787 validate_packet(788 self.packet_path,789 "task-123",790 self.ledger_path,791 prior_path,792 prior_digest,793 )794 795 advanced = copy.deepcopy(self.packet)796 advanced["ledger"]["current_round"] = 2797 advanced["ledger"]["remaining_budget"] = 4798 self._write_packet(self.packet_path, advanced)799 800 validate_packet(801 self.packet_path,802 "task-123",803 self.ledger_path,804 prior_path,805 prior_digest,806 )807 808 def test_same_round_retry_cannot_expand_the_budget_history(self) -> None:809 prior_path = self.root / "prior-ledger.json"810 prior = copy.deepcopy(self.packet["ledger"])811 self._write_json(prior_path, prior)812 prior_digest = hashlib.sha256(prior_path.read_bytes()).hexdigest()813 expanded = copy.deepcopy(self.packet)814 expanded["ledger"]["authorized_round_budgets"] = [6, 2]815 expanded["ledger"]["remaining_budget"] = 7816 self._write_packet(self.packet_path, expanded)817 818 with self.assertRaisesRegex(ProtocolError, "same-round retry budget history"):819 validate_packet(820 self.packet_path,821 "task-123",822 self.ledger_path,823 prior_path,824 prior_digest,825 )826 827 expanded["ledger"]["current_round"] = 2828 expanded["ledger"]["remaining_budget"] = 6829 self._write_packet(self.packet_path, expanded)830 validate_packet(831 self.packet_path,832 "task-123",833 self.ledger_path,834 prior_path,835 prior_digest,836 )837 838 def test_later_round_requires_digest_bound_prior_ledger(self) -> None:839 packet = copy.deepcopy(self.packet)840 packet["ledger"]["current_round"] = 2841 packet["ledger"]["remaining_budget"] = 4842 self._write_packet(self.packet_path, packet)843 844 with self.assertRaisesRegex(ProtocolError, "digest-bound prior ledger"):845 self._validate_packet()846 847 def test_current_ledger_cannot_authorize_its_own_history(self) -> None:848 packet = copy.deepcopy(self.packet)849 packet["ledger"]["authorized_round_budgets"] = [2]850 packet["ledger"]["current_round"] = 2851 packet["ledger"]["remaining_budget"] = 0852 self._write_packet(self.packet_path, packet)853 current_digest = hashlib.sha256(self.ledger_path.read_bytes()).hexdigest()854 855 with self.assertRaisesRegex(ProtocolError, "distinct from the current ledger"):856 validate_packet(857 self.packet_path,858 "task-123",859 self.ledger_path,860 self.ledger_path,861 current_digest,862 )863 864 def test_prior_ledger_hardlink_cannot_alias_current_ledger(self) -> None:865 packet = copy.deepcopy(self.packet)866 packet["ledger"]["authorized_round_budgets"] = [2]867 packet["ledger"]["current_round"] = 2868 packet["ledger"]["remaining_budget"] = 0869 self._write_packet(self.packet_path, packet)870 prior_path = self.root / "prior-ledger.json"871 os.link(self.ledger_path, prior_path)872 current_digest = hashlib.sha256(self.ledger_path.read_bytes()).hexdigest()873 874 with self.assertRaisesRegex(ProtocolError, "distinct from the current ledger"):875 validate_packet(876 self.packet_path,877 "task-123",878 self.ledger_path,879 prior_path,880 current_digest,881 )882 883 def test_inventory_requires_kind_specific_evidence(self) -> None:884 cases = (885 (0, "surface", "contract fields"),886 (1, "validation", "authority-data-flow"),887 )888 for index, field, expected in cases:889 with self.subTest(field=field):890 packet = copy.deepcopy(self.packet)891 del packet["inventory"][index][field]892 path = self.root / f"missing-inventory-{index}.json"893 self._write_packet(path, packet)894 with self.assertRaisesRegex(ProtocolError, expected):895 self._validate_packet(path)896 897 def test_every_inventory_requires_one_canonical_root_owner(self) -> None:898 packet = copy.deepcopy(self.packet)899 orphan = copy.deepcopy(packet["inventory"][0])900 orphan["id"] = "INV-ORPHAN"901 packet["inventory"].append(orphan)902 packet["reviewer_assignments"][0]["inventory_ids"].append("INV-ORPHAN")903 self._write_packet(self.packet_path, packet)904 905 with self.assertRaisesRegex(ProtocolError, r"unowned=\['INV-ORPHAN'\]"):906 self._validate_packet()907 908 def test_unfiltered_changed_paths_require_explicit_exclusions(self) -> None:909 state = copy.deepcopy(self.review_state)910 state["unfiltered"]["workspace"] = copy.deepcopy(911 state["unfiltered"]["workspace"]912 )913 state["unfiltered"]["workspace"].insert(914 0,915 {916 "path": "notes/unrelated.txt",917 "kind": "file",918 "executable": False,919 "sha256": "e" * 64,920 },921 )922 state["repository_fingerprint"] = _repository_fingerprint(923 content_fingerprint=state["content_fingerprint"],924 head=state["head"],925 status_sha256=state["status_sha256"],926 tracked_diff_sha256=state["tracked_diff_sha256"],927 complete_diff_sha256=state["complete_diff_sha256"],928 unfiltered_status_sha256=state["unfiltered"]["status_sha256"],929 unfiltered_content_fingerprint=_content_fingerprint(930 state["base"], state["unfiltered"]["workspace"]931 ),932 )933 self._write_json(self.review_state_path, state)934 packet = copy.deepcopy(self.packet)935 state_artifact = next(936 artifact937 for artifact in packet["evidence_artifacts"]938 if artifact["id"] == "E-STATE"939 )940 state_artifact["sha256"] = hashlib.sha256(941 self.review_state_path.read_bytes()942 ).hexdigest()943 self._write_packet(self.packet_path, packet)944 945 with self.assertRaisesRegex(ProtocolError, "must exactly account"):946 self._validate_packet()947 948 packet["repository"]["exclusions"] = [949 {"path": "notes/unrelated.txt", "reason": "Unrelated user-owned note."}950 ]951 self._write_packet(self.packet_path, packet)952 self._validate_packet()953 954 def test_preflight_results_require_exact_command_result_records(self) -> None:955 packet = copy.deepcopy(self.packet)956 packet["verification"]["preflight_results"] = "Tests passed."957 self._write_packet(self.packet_path, packet)958 with self.assertRaisesRegex(959 ProtocolError, "preflight_results must be an array"960 ):961 self._validate_packet()962 963 packet["verification"]["preflight_results"] = [964 {"command": "uv run pytest <path>", "result": "passed"}965 ]966 self._write_packet(self.packet_path, packet)967 with self.assertRaisesRegex(968 ProtocolError, "command contains a placeholder token"969 ):970 self._validate_packet()971 972 packet = copy.deepcopy(self.packet)973 packet["verification"]["preflight_results"][0]["details"] = "Unvalidated metadata."974 self._write_packet(self.packet_path, packet)975 with self.assertRaisesRegex(ProtocolError, "unexpected=\\['details'\\]"):976 self._validate_packet()977 978 def test_preflight_commands_must_be_unique(self) -> None:979 """Reject repeated preflight records for the same exact command."""980 packet = copy.deepcopy(self.packet)981 duplicate = copy.deepcopy(packet["verification"]["preflight_results"][0])982 duplicate["result"] = "The same command passed again."983 packet["verification"]["preflight_results"].append(duplicate)984 self._write_packet(self.packet_path, packet)985 986 with self.assertRaisesRegex(ProtocolError, "Duplicate preflight command"):987 self._validate_packet()988 989 def test_every_reviewer_receives_all_components_and_complete_diff(self) -> None:990 cases = []991 no_components = copy.deepcopy(self.packet)992 no_components["reviewer_assignments"][0]["expected_components"] = []993 cases.append((no_components, "must receive every component"))994 no_evidence = copy.deepcopy(self.packet)995 no_evidence["reviewer_assignments"][0]["evidence_ids"] = []996 cases.append((no_evidence, "must receive every component"))997 998 for index, (packet, expected) in enumerate(cases):999 with self.subTest(expected=expected):1000 path = self.root / f"incomplete-reviewer-{index}.json"1001 self._write_packet(path, packet)1002 with self.assertRaisesRegex(ProtocolError, re_escape(expected)):1003 self._validate_packet(path)1004 1005 def test_reviewer_assignments_require_distinct_specialties(self) -> None:1006 """Require the two reviewers to have complementary specialties."""1007 packet = copy.deepcopy(self.packet)1008 packet["reviewer_assignments"][1]["primary_dimensions"] = ["requirement and scope"]1009 self._write_packet(self.packet_path, packet)1010 1011 with self.assertRaisesRegex(ProtocolError, "overlapping primary specialty"):1012 self._validate_packet()1013 1014 packet = copy.deepcopy(self.packet)1015 packet["selected_high_risk_dimensions"] = ["persistence"]1016 for assignment in packet["reviewer_assignments"]:1017 assignment["high_risk_dimensions"] = ["persistence"]1018 self._write_packet(self.packet_path, packet)1019 1020 with self.assertRaisesRegex(ProtocolError, "overlapping high-risk specialty"):1021 self._validate_packet()1022 1023 def test_packet_and_ledger_reject_json_booleans_as_integers(self) -> None:1024 cases = []1025 schema = copy.deepcopy(self.packet)1026 schema["schema_version"] = True1027 cases.append((schema, "schema_version must be integer 1"))1028 current_round = copy.deepcopy(self.packet)1029 current_round["ledger"]["current_round"] = True1030 cases.append((current_round, "current_round must be a positive integer"))1031 1032 for index, (packet, expected) in enumerate(cases):1033 with self.subTest(expected=expected):1034 path = self.root / f"boolean-integer-{index}.json"1035 self._write_packet(path, packet)1036 with self.assertRaisesRegex(ProtocolError, re_escape(expected)):1037 self._validate_packet(path)1038 1039 def test_packet_rejects_changed_evidence(self) -> None:1040 self.evidence.write_text("changed\n")1041 1042 with self.assertRaisesRegex(ProtocolError, "digest mismatch"):1043 self._validate_packet()1044 1045 @unittest.skipIf(os.name == "nt", "Symlink creation requires platform privileges.")1046 def test_packet_rejects_aliased_evidence_paths(self) -> None:1047 alias = self.root / "root-evidence-alias.txt"1048 alias.symlink_to(self.root_evidence)1049 packet = copy.deepcopy(self.packet)1050 packet["evidence_artifacts"].append(1051 {1052 "id": "E-ROOT-ALIAS",1053 "path": str(alias),1054 "sha256": hashlib.sha256(self.root_evidence.read_bytes()).hexdigest(),1055 "role": "supporting",1056 "purpose": "Alias of existing root-cause evidence.",1057 }1058 )1059 self._write_packet(self.packet_path, packet)1060 1061 with self.assertRaisesRegex(ProtocolError, "Duplicate evidence artifact file identity"):1062 self._validate_packet()1063 1064 def test_copied_evidence_does_not_reopen_a_closed_root(self) -> None:1065 prior_path = self.root / "prior-ledger.json"1066 self._write_json(prior_path, self.packet["ledger"])1067 prior_digest = hashlib.sha256(prior_path.read_bytes()).hexdigest()1068 copied_evidence = self.root / "copied-root-evidence.txt"1069 copied_evidence.write_bytes(self.root_evidence.read_bytes())1070 packet = copy.deepcopy(self.packet)1071 packet["evidence_artifacts"].append(1072 {1073 "id": "E-COPY",1074 "path": str(copied_evidence),1075 "sha256": hashlib.sha256(copied_evidence.read_bytes()).hexdigest(),1076 "role": "supporting",1077 "purpose": "Byte copy of existing root-cause evidence.",1078 }1079 )1080 closed_root = next(1081 root for root in packet["ledger"]["root_causes"] if root["id"] == "ROOT_CLOSED"1082 )1083 closed_root["status"] = "open"1084 closed_root["contract_evidence_ids"].append("E-COPY")1085 packet["ledger"]["contract_evidence_sha256"]["E-COPY"] = hashlib.sha256(1086 copied_evidence.read_bytes()1087 ).hexdigest()1088 packet["ledger"]["current_round"] = 21089 packet["ledger"]["remaining_budget"] = 41090 self._write_packet(self.packet_path, packet)1091 1092 with self.assertRaisesRegex(ProtocolError, "without content-new evidence"):1093 validate_packet(1094 self.packet_path,1095 "task-123",1096 self.ledger_path,1097 prior_path,1098 prior_digest,1099 )1100 1101 @unittest.skipUnless(Path("/dev/null").exists(), "Requires a POSIX device path.")1102 def test_packet_rejects_non_regular_evidence_files(self) -> None:1103 packet = copy.deepcopy(self.packet)1104 packet["evidence_artifacts"][0]["path"] = "/dev/null"1105 packet["evidence_artifacts"][0]["sha256"] = hashlib.sha256(b"").hexdigest()1106 self._write_packet(self.packet_path, packet)1107 1108 with self.assertRaisesRegex(ProtocolError, "must be a regular file"):1109 self._validate_packet()1110 1111 @unittest.skipUnless(hasattr(os, "mkfifo"), "Requires POSIX FIFO support.")1112 def test_artifact_file_type_is_verified_after_open(self) -> None:1113 fifo = self.root / "artifact.pipe"1114 os.mkfifo(fifo)1115 regular_stat = self.packet_path.stat()1116 1117 with (1118 mock.patch.object(Path, "stat", return_value=regular_stat),1119 mock.patch.object(Path, "read_bytes", return_value=b"not from the FIFO"),1120 self.assertRaisesRegex(ProtocolError, "must be a regular file"),1121 ):1122 _read_bytes(str(fifo), "artifact")1123 1124 def test_complete_diff_must_match_review_state(self) -> None:1125 partial_diff = self.root / "partial.diff"1126 partial_diff.write_text("partial diff\n")1127 packet = copy.deepcopy(self.packet)1128 packet["evidence_artifacts"][0]["path"] = str(partial_diff)1129 packet["evidence_artifacts"][0]["sha256"] = hashlib.sha256(1130 partial_diff.read_bytes()1131 ).hexdigest()1132 self._write_packet(self.packet_path, packet)1133 1134 with self.assertRaisesRegex(1135 ProtocolError, "must match review_state.complete_diff_sha256"1136 ):1137 self._validate_packet()1138 1139 def test_complete_diff_paths_must_match_task_workspace(self) -> None:1140 state = copy.deepcopy(self.review_state)1141 state["complete_diff_paths"] = []1142 self._write_review_state(state)1143 1144 with self.assertRaisesRegex(1145 ProtocolError, "must exactly match the task workspace"1146 ):1147 self._validate_packet()1148 1149 def test_review_state_artifact_is_digest_bound(self) -> None:1150 packet = copy.deepcopy(self.packet)1151 state_artifact = next(1152 artifact1153 for artifact in packet["evidence_artifacts"]1154 if artifact["id"] == "E-STATE"1155 )1156 state_artifact["sha256"] = "0" * 641157 self._write_packet(self.packet_path, packet)1158 1159 with self.assertRaisesRegex(1160 ProtocolError, "evidence artifact E-STATE digest mismatch"1161 ):1162 self._validate_packet()1163 1164 def test_review_state_revalidation_command_disables_bytecode_writes(self) -> None:1165 packet = copy.deepcopy(self.packet)1166 packet["review_state"]["revalidation_command"] = (1167 "python3 review_state.py --base BASE"1168 )1169 self._write_packet(self.packet_path, packet)1170 1171 with self.assertRaisesRegex(1172 ProtocolError, "must disable Python bytecode writes"1173 ):1174 self._validate_packet()1175 1176 def test_review_state_requires_complete_typed_workspace_entries(self) -> None:1177 state = copy.deepcopy(self.review_state)1178 del state["workspace"][0]["executable"]1179 self._write_review_state(state)1180 1181 with self.assertRaisesRegex(ProtocolError, "file schema: missing"):1182 self._validate_packet()1183 1184 def test_review_state_rejects_unknown_fields_for_every_workspace_kind(self) -> None:1185 entries = (1186 {1187 "path": "file",1188 "kind": "file",1189 "executable": False,1190 "sha256": "a" * 64,1191 },1192 {"path": "link", "kind": "symlink", "sha256": "b" * 64},1193 {1194 "path": "gitlink",1195 "kind": "gitlink",1196 "head": "c" * 40,1197 },1198 {"path": "directory", "kind": "directory"},1199 {"path": "missing", "kind": "missing"},1200 )1201 for entry in entries:1202 with self.subTest(kind=entry["kind"]):1203 entry_with_unknown = {**entry, "authority": "unsupported"}1204 with self.assertRaisesRegex(1205 ProtocolError, r"unexpected=\['authority'\]"1206 ):1207 _workspace_entries([entry_with_unknown], "review_state.workspace")1208 1209 def test_review_state_accepts_complete_gitlink_entry(self) -> None:1210 state = copy.deepcopy(self.review_state)1211 workspace = [1212 {1213 "path": "src/example.py",1214 "kind": "gitlink",1215 "head": "c" * 40,1216 }1217 ]1218 combined = _content_fingerprint(state["base"], workspace)1219 state["fingerprint"] = combined1220 state["content_fingerprint"] = combined1221 state["workspace"] = workspace1222 state["unfiltered"]["workspace"] = workspace1223 state["components"]["api-contract"]["content_fingerprint"] = combined1224 state["components"]["api-contract"]["workspace"] = workspace1225 state["repository_fingerprint"] = _repository_fingerprint(1226 content_fingerprint=combined,1227 head=state["head"],1228 status_sha256=state["status_sha256"],1229 tracked_diff_sha256=state["tracked_diff_sha256"],1230 complete_diff_sha256=state["complete_diff_sha256"],1231 unfiltered_status_sha256=state["unfiltered"]["status_sha256"],1232 unfiltered_content_fingerprint=combined,1233 )1234 packet = copy.deepcopy(self.packet)1235 packet["ledger"]["round_fingerprint"] = combined1236 self._write_review_state(state, packet)1237 1238 summary = self._validate_packet()1239 1240 self.assertEqual(summary["combined_fingerprint"], combined)1241 1242 def test_review_state_artifact_rejects_unknown_workspace_fields(self) -> None:1243 state = copy.deepcopy(self.review_state)1244 state["workspace"][0]["authority"] = "unsupported"1245 self._write_review_state(state)1246 1247 with self.assertRaisesRegex(ProtocolError, r"unexpected=\['authority'\]"):1248 self._validate_packet()1249 1250 def test_review_state_requires_component_workspace(self) -> None:1251 state = copy.deepcopy(self.review_state)1252 del state["components"]["api-contract"]["workspace"]1253 self._write_review_state(state)1254 1255 with self.assertRaisesRegex(ProtocolError, "workspace must be an array"):1256 self._validate_packet()1257 1258 def test_review_state_recomputes_content_and_repository_fingerprints(self) -> None:1259 content_state = copy.deepcopy(self.review_state)1260 content_state["workspace"][0]["sha256"] = "e" * 641261 self._write_review_state(content_state)1262 with self.assertRaisesRegex(ProtocolError, "does not match its workspace"):1263 self._validate_packet()1264 1265 repository_state = copy.deepcopy(self.review_state)1266 repository_state["status_sha256"] = "e" * 641267 self._write_review_state(repository_state)1268 with self.assertRaisesRegex(1269 ProtocolError, "repository_fingerprint does not match"1270 ):1271 self._validate_packet()1272 1273 def test_review_state_descriptor_rejects_copied_authority(self) -> None:1274 packet = copy.deepcopy(self.packet)1275 packet["review_state"]["content_fingerprint"] = "0" * 641276 self._write_packet(self.packet_path, packet)1277 1278 with self.assertRaisesRegex(ProtocolError, "must contain only evidence_id"):1279 self._validate_packet()1280 1281 def test_oversized_packet_requires_reason(self) -> None:1282 packet = copy.deepcopy(self.packet)1283 packet["task"]["original_requirement"] = "x" * (12 * 1024)1284 self._write_packet(self.packet_path, packet)1285 1286 with self.assertRaisesRegex(ProtocolError, "provide an overage reason"):1287 self._validate_packet()1288 1289 packet["packet_overage_reason"] = (1290 "The requirement is retained verbatim for review."1291 )1292 self._write_packet(self.packet_path, packet)1293 self._validate_packet()1294 1295 def test_exact_fingerprint_receipt_is_reusable(self) -> None:1296 receipt = self._receipt()1297 receipt["command"] = "make tests > /tmp/tests.log"1298 1299 validate_receipt_data(1300 receipt, self.combined, {"api-contract": self.component}, self.repository1301 )1302 1303 receipt["after"]["combined"] = "d" * 641304 with self.assertRaisesRegex(ProtocolError, "after fingerprints do not match"):1305 validate_receipt_data(1306 receipt,1307 self.combined,1308 {"api-contract": self.component},1309 self.repository,1310 )1311 1312 def test_receipt_rejects_repository_fingerprint_drift(self) -> None:1313 receipt = self._receipt()1314 receipt["after"]["repository"] = "d" * 641315 1316 with self.assertRaisesRegex(ProtocolError, "after fingerprints do not match"):1317 validate_receipt_data(1318 receipt,1319 self.combined,1320 {"api-contract": self.component},1321 self.repository,1322 )1323 1324 def test_commands_allow_redirection_but_reject_placeholder_tokens(self) -> None:1325 packet = copy.deepcopy(self.packet)1326 packet["verification"]["preflight_results"] = [1327 {1328 "command": "sort < /tmp/input.txt > /tmp/output.txt",1329 "result": "passed",1330 }1331 ]1332 self._write_packet(self.packet_path, packet)1333 self._validate_packet()1334 1335 output = self._output()1336 output["focused_probes"] = [1337 {"command": "git diff > /tmp/review.diff", "result": "captured"}1338 ]1339 self._write_json(self.output_path, output)1340 self._validate_output("requirements")1341 1342 receipt = self._receipt()1343 receipt["command"] = "make tests <focused probe>"1344 with self.assertRaisesRegex(ProtocolError, "placeholder token"):1345 validate_receipt_data(1346 receipt,1347 self.combined,1348 {"api-contract": self.component},1349 self.repository,1350 )1351 1352 def test_receipt_rejects_boolean_exit_status(self) -> None:1353 receipt = self._receipt()1354 receipt["exit_status"] = False1355 1356 with self.assertRaisesRegex(ProtocolError, "exit_status 0"):1357 validate_receipt_data(1358 receipt,1359 self.combined,1360 {"api-contract": self.component},1361 self.repository,1362 )1363 1364 def test_receipt_rejects_unknown_fields(self) -> None:1365 """Reject conflicting evidence outside the receipt schema."""1366 receipt = self._receipt()1367 receipt["exit_code"] = 11368 1369 with self.assertRaisesRegex(ProtocolError, "Verification receipt.*unexpected"):1370 validate_receipt_data(1371 receipt, self.combined, {"api-contract": self.component}, self.repository1372 )1373 1374 def test_packet_validates_every_credited_receipt(self) -> None:1375 self._write_json(self.receipt_path, self._receipt())1376 packet = copy.deepcopy(self.packet)1377 packet["verification"]["credited_receipts"] = [1378 {1379 "path": str(self.receipt_path),1380 "sha256": hashlib.sha256(self.receipt_path.read_bytes()).hexdigest(),1381 }1382 ]1383 self._write_packet(self.packet_path, packet)1384 1385 self._validate_packet()1386 1387 receipt = self._receipt()1388 receipt["exit_status"] = 11389 self._write_json(self.receipt_path, receipt)1390 packet["verification"]["credited_receipts"][0]["sha256"] = hashlib.sha256(1391 self.receipt_path.read_bytes()1392 ).hexdigest()1393 self._write_packet(self.packet_path, packet)1394 with self.assertRaisesRegex(ProtocolError, "exit_status 0"):1395 self._validate_packet()1396 1397 @unittest.skipIf(os.name == "nt", "Symlink creation requires platform privileges.")1398 def test_packet_rejects_aliased_credited_receipts(self) -> None:1399 self._write_json(self.receipt_path, self._receipt())1400 alias = self.root / "receipt-alias.json"1401 alias.symlink_to(self.receipt_path)1402 digest = hashlib.sha256(self.receipt_path.read_bytes()).hexdigest()1403 packet = copy.deepcopy(self.packet)1404 packet["verification"]["credited_receipts"] = [1405 {"path": str(self.receipt_path), "sha256": digest},1406 {"path": str(alias), "sha256": digest},1407 ]1408 self._write_packet(self.packet_path, packet)1409 1410 with self.assertRaisesRegex(ProtocolError, "Duplicate credited receipt file identity"):1411 self._validate_packet()1412 1413 def test_packet_rejects_copied_credited_receipts(self) -> None:1414 self._write_json(self.receipt_path, self._receipt())1415 copied_receipt = self.root / "copied-receipt.json"1416 copied_receipt.write_bytes(self.receipt_path.read_bytes())1417 digest = hashlib.sha256(self.receipt_path.read_bytes()).hexdigest()1418 packet = copy.deepcopy(self.packet)1419 packet["verification"]["credited_receipts"] = [1420 {"path": str(self.receipt_path), "sha256": digest},1421 {"path": str(copied_receipt), "sha256": digest},1422 ]1423 self._write_packet(self.packet_path, packet)1424 1425 with self.assertRaisesRegex(ProtocolError, "Duplicate credited receipt digest"):1426 self._validate_packet()1427 1428 def test_packet_rejects_multiple_receipts_for_one_command(self) -> None:1429 """Reject distinct receipt files that credit the same command."""1430 first_receipt = self._receipt()1431 self._write_json(self.receipt_path, first_receipt)1432 second_receipt = copy.deepcopy(first_receipt)1433 second_receipt["environment"] = "The same gate rerun in a fresh local process."1434 second_receipt_path = self.root / "second-receipt.json"1435 self._write_json(second_receipt_path, second_receipt)1436 packet = copy.deepcopy(self.packet)1437 packet["verification"]["credited_receipts"] = [1438 {1439 "path": str(self.receipt_path),1440 "sha256": hashlib.sha256(self.receipt_path.read_bytes()).hexdigest(),1441 },1442 {1443 "path": str(second_receipt_path),1444 "sha256": hashlib.sha256(second_receipt_path.read_bytes()).hexdigest(),1445 },1446 ]1447 self._write_packet(self.packet_path, packet)1448 1449 with self.assertRaisesRegex(ProtocolError, "Duplicate credited receipt command"):1450 self._validate_packet()1451 1452 def test_packet_rejects_receipt_for_unrelated_successful_command(self) -> None:1453 receipt = self._receipt()1454 receipt["command"] = "true"1455 self._write_json(self.receipt_path, receipt)1456 packet = copy.deepcopy(self.packet)1457 packet["verification"]["credited_receipts"] = [1458 {1459 "path": str(self.receipt_path),1460 "sha256": hashlib.sha256(self.receipt_path.read_bytes()).hexdigest(),1461 }1462 ]1463 self._write_packet(self.packet_path, packet)1464 1465 with self.assertRaisesRegex(1466 ProtocolError, "must exactly match a packet preflight command"1467 ):1468 self._validate_packet()1469 1470 def test_packet_binds_credited_receipt_digest(self) -> None:1471 receipt = self._receipt()1472 self._write_json(self.receipt_path, receipt)1473 packet = copy.deepcopy(self.packet)1474 packet["verification"]["credited_receipts"] = [1475 {1476 "path": str(self.receipt_path),1477 "sha256": hashlib.sha256(self.receipt_path.read_bytes()).hexdigest(),1478 }1479 ]1480 self._write_packet(self.packet_path, packet)1481 self._validate_packet()1482 1483 receipt["command"] = "make lint"1484 self._write_json(self.receipt_path, receipt)1485 with self.assertRaisesRegex(1486 ProtocolError, r"credited_receipts\[0\] digest mismatch"1487 ):1488 self._validate_packet()1489 1490 def test_receipt_cli_rejects_unindexed_replacement(self) -> None:1491 receipt = self._receipt()1492 self._write_json(self.receipt_path, receipt)1493 packet = copy.deepcopy(self.packet)1494 packet["verification"]["credited_receipts"] = [1495 {1496 "path": str(self.receipt_path),1497 "sha256": hashlib.sha256(self.receipt_path.read_bytes()).hexdigest(),1498 }1499 ]1500 self._write_packet(self.packet_path, packet)1501 1502 unindexed = self.root / "unindexed-receipt.json"1503 receipt["command"] = "command-that-never-ran"1504 self._write_json(unindexed, receipt)1505 completed = subprocess.run(1506 (1507 sys.executable,1508 str(Path(__file__).with_name("review_protocol.py")),1509 "receipt",1510 "--packet",1511 str(self.packet_path),1512 "--receipt",1513 str(unindexed),1514 "--task-id",1515 "task-123",1516 "--ledger",1517 str(self.ledger_path),1518 ),1519 capture_output=True,1520 text=True,1521 )1522 1523 self.assertEqual(completed.returncode, 2)1524 self.assertIn("receipt path is not indexed", completed.stderr)1525 1526 def test_receipt_validation_rechecks_the_indexed_file(self) -> None:1527 """Reject a receipt replaced after packet validation."""1528 receipt = self._receipt()1529 self._write_json(self.receipt_path, receipt)1530 packet = copy.deepcopy(self.packet)1531 packet["verification"]["credited_receipts"] = [1532 {1533 "path": str(self.receipt_path),1534 "sha256": hashlib.sha256(self.receipt_path.read_bytes()).hexdigest(),1535 }1536 ]1537 self._write_packet(self.packet_path, packet)1538 original_validate_packet = validate_packet1539 1540 def validate_then_replace_receipt(*args: Any, **kwargs: Any) -> dict[str, Any]:1541 summary = original_validate_packet(*args, **kwargs)1542 receipt["environment"] = "A replacement environment after packet validation."1543 self._write_json(self.receipt_path, receipt)1544 return summary1545 1546 with (1547 mock.patch(1548 "review_protocol.validate_packet", side_effect=validate_then_replace_receipt1549 ),1550 self.assertRaisesRegex(ProtocolError, "Receipt changed"),1551 ):1552 _validate_credited_receipt(1553 self.packet_path,1554 self.receipt_path,1555 "task-123",1556 self.ledger_path,1557 )1558 1559 def test_clean_output_must_match_assignment_and_fingerprint(self) -> None:1560 output = self._output()1561 self._write_json(self.output_path, output)1562 1563 summary = self._validate_output("requirements")1564 self.assertEqual(summary["verdict"], "clean")1565 1566 output["checked_inventory_ids"] = []1567 self._write_json(self.output_path, output)1568 with self.assertRaisesRegex(ProtocolError, "inventory accounting differs"):1569 self._validate_output("requirements")1570 1571 def test_reviewer_output_rejects_repository_only_drift(self) -> None:1572 output = self._output()1573 changed_status = b"M src/example.py\0"1574 self.status_path.write_bytes(changed_status)1575 changed_status_sha256 = hashlib.sha256(changed_status).hexdigest()1576 packet = copy.deepcopy(self.packet)1577 status_artifact = next(1578 artifact1579 for artifact in packet["evidence_artifacts"]1580 if artifact["id"] == "E-STATUS"1581 )1582 status_artifact["sha256"] = changed_status_sha2561583 state = copy.deepcopy(self.review_state)1584 state["status_sha256"] = changed_status_sha2561585 state["unfiltered"]["status_sha256"] = changed_status_sha2561586 state["repository_fingerprint"] = _repository_fingerprint(1587 content_fingerprint=self.combined,1588 head=state["head"],1589 status_sha256=changed_status_sha256,1590 tracked_diff_sha256=state["tracked_diff_sha256"],1591 complete_diff_sha256=state["complete_diff_sha256"],1592 unfiltered_status_sha256=changed_status_sha256,1593 unfiltered_content_fingerprint=self.combined,1594 )1595 self._write_review_state(state, packet)1596 self._write_json(self.output_path, output)1597 1598 with self.assertRaisesRegex(ProtocolError, "fingerprints do not match"):1599 self._validate_output("requirements")1600 1601 def test_reviewer_output_rejects_unknown_fields(self) -> None:1602 """Reject reviewer conclusions outside the documented schema."""1603 output = self._output()1604 output["issues"] = [{"title": "Ignored finding"}]1605 self._write_json(self.output_path, output)1606 1607 with self.assertRaisesRegex(ProtocolError, "Reviewer output.*unexpected"):1608 self._validate_output("requirements")1609 1610 def test_finding_and_root_evidence_reject_unknown_fields(self) -> None:1611 """Reject finding data that the protocol would otherwise ignore."""1612 for field_path in ("finding", "root_evidence"):1613 with self.subTest(field_path=field_path):1614 output = self._output()1615 output["verdict"] = "findings require fixes"1616 finding = self._finding("ROOT_EXISTING")1617 if field_path == "finding":1618 finding["alternative_root"] = "ROOT_CLOSED"1619 else:1620 finding["root_cause_evidence"]["note"] = "Ignored evidence metadata."1621 output["findings"] = [finding]1622 self._write_json(self.output_path, output)1623 1624 with self.assertRaisesRegex(ProtocolError, "unexpected"):1625 self._validate_output("requirements")1626 1627 def test_reviewer_output_is_bound_to_the_exact_packet(self) -> None:1628 output = self._output()1629 self._write_json(self.output_path, output)1630 packet = copy.deepcopy(self.packet)1631 packet["task"]["original_requirement"] = "A changed review requirement."1632 self._write_packet(self.packet_path, packet)1633 1634 with self.assertRaisesRegex(ProtocolError, "packet digest"):1635 self._validate_output("requirements")1636 1637 def test_reviewer_output_rechecks_current_ledger_after_packet_validation(self) -> None:1638 """Reject a current ledger replaced after packet validation."""1639 output = self._output()1640 self._write_json(self.output_path, output)1641 original_validate_packet = validate_packet1642 1643 def validate_then_replace_ledger(*args: Any, **kwargs: Any) -> dict[str, Any]:1644 summary = original_validate_packet(*args, **kwargs)1645 replacement = copy.deepcopy(self.packet["ledger"])1646 replacement["remaining_budget"] = 991647 self._write_json(self.ledger_path, replacement)1648 return summary1649 1650 with (1651 mock.patch("review_protocol.validate_packet", side_effect=validate_then_replace_ledger),1652 self.assertRaisesRegex(ProtocolError, "Current ledger changed"),1653 ):1654 self._validate_output("requirements")1655 1656 def test_reviewer_output_rechecks_prior_ledger_after_packet_validation(self) -> None:1657 """Reject a prior ledger replaced after packet validation."""1658 prior_path = self.root / "prior-ledger.json"1659 self._write_json(prior_path, self.packet["ledger"])1660 prior_digest = hashlib.sha256(prior_path.read_bytes()).hexdigest()1661 packet = copy.deepcopy(self.packet)1662 packet["ledger"]["current_round"] = 21663 packet["ledger"]["remaining_budget"] = 41664 self._write_packet(self.packet_path, packet)1665 self._write_json(self.output_path, self._output())1666 original_validate_packet = validate_packet1667 1668 def validate_then_replace_prior(*args: Any, **kwargs: Any) -> dict[str, Any]:1669 summary = original_validate_packet(*args, **kwargs)1670 replacement = copy.deepcopy(self.packet["ledger"])1671 replacement["root_causes"] = []1672 self._write_json(prior_path, replacement)1673 return summary1674 1675 with (1676 mock.patch("review_protocol.validate_packet", side_effect=validate_then_replace_prior),1677 self.assertRaisesRegex(ProtocolError, "Prior ledger changed"),1678 ):1679 validate_reviewer_output(1680 self.packet_path,1681 "requirements",1682 self.output_path,1683 "task-123",1684 self.ledger_path,1685 prior_path,1686 prior_digest,1687 )1688 1689 def test_unknown_root_must_be_new_proposal_with_evidence(self) -> None:1690 output = self._output()1691 output["verdict"] = "findings require fixes"1692 output["findings"] = [self._finding("renamed-root")]1693 self._write_json(self.output_path, output)1694 1695 with self.assertRaisesRegex(ProtocolError, "canonical root ID or propose NEW"):1696 self._validate_output("requirements")1697 1698 output["findings"][0]["root_cause_id"] = "NEW:new-boundary"1699 output["findings"][0]["root_cause_evidence"]["new_inventory_ids"] = ["INV-1"]1700 self._write_json(self.output_path, output)1701 with self.assertRaisesRegex(ProtocolError, "cannot reuse canonical inventory"):1702 self._validate_output("requirements")1703 1704 output["findings"][0]["root_cause_evidence"]["new_inventory_ids"] = []1705 output["findings"][0]["root_cause_evidence"]["new_contract_evidence_ids"] = [1706 "E-NEW"1707 ]1708 self._write_json(self.output_path, output)1709 self._validate_output("requirements")1710 1711 def test_copied_evidence_does_not_support_a_new_root(self) -> None:1712 copied_evidence = self.root / "copied-root-evidence.txt"1713 copied_evidence.write_bytes(self.root_evidence.read_bytes())1714 packet = copy.deepcopy(self.packet)1715 packet["evidence_artifacts"].append(1716 {1717 "id": "E-COPY",1718 "path": str(copied_evidence),1719 "sha256": hashlib.sha256(copied_evidence.read_bytes()).hexdigest(),1720 "role": "supporting",1721 "purpose": "Byte copy of existing root-cause evidence.",1722 }1723 )1724 self._write_packet(self.packet_path, packet)1725 output = self._output()1726 output["verdict"] = "findings require fixes"1727 finding = self._finding("NEW:copied-evidence")1728 finding["root_cause_evidence"]["new_contract_evidence_ids"] = ["E-COPY"]1729 output["findings"] = [finding]1730 self._write_json(self.output_path, output)1731 1732 with self.assertRaisesRegex(ProtocolError, "without content-new evidence"):1733 self._validate_output("requirements")1734 1735 def test_copied_inventory_does_not_reopen_a_closed_root(self) -> None:1736 """Reject a renamed copy of inventory as closed-root evidence."""1737 prior_path = self.root / "prior-ledger.json"1738 self._write_json(prior_path, self.packet["ledger"])1739 prior_digest = hashlib.sha256(prior_path.read_bytes()).hexdigest()1740 packet = copy.deepcopy(self.packet)1741 copied_inventory = copy.deepcopy(packet["inventory"][1])1742 copied_inventory["id"] = "INV-COPY"1743 packet["inventory"].append(copied_inventory)1744 packet["ledger"]["inventory_sha256"]["INV-COPY"] = _inventory_digest(copied_inventory)1745 closed_root = next(1746 root for root in packet["ledger"]["root_causes"] if root["id"] == "ROOT_CLOSED"1747 )1748 closed_root["status"] = "open"1749 closed_root["inventory_ids"].append("INV-COPY")1750 packet["reviewer_assignments"][1]["inventory_ids"].append("INV-COPY")1751 packet["ledger"]["current_round"] = 21752 packet["ledger"]["remaining_budget"] = 41753 self._write_packet(self.packet_path, packet)1754 1755 with self.assertRaisesRegex(ProtocolError, "without content-new evidence"):1756 validate_packet(1757 self.packet_path,1758 "task-123",1759 self.ledger_path,1760 prior_path,1761 prior_digest,1762 )1763 1764 def test_distinct_new_roots_cannot_share_one_evidence_digest(self) -> None:1765 """Require distinct new roots to own distinct evidence content."""1766 output = self._output()1767 output["verdict"] = "findings require fixes"1768 first = self._finding("NEW:first-root")1769 first["root_cause_evidence"]["new_contract_evidence_ids"] = ["E-NEW"]1770 second = self._finding("NEW:second-root")1771 second["root_cause_evidence"]["new_contract_evidence_ids"] = ["E-NEW"]1772 output["findings"] = [first, second]1773 self._write_json(self.output_path, output)1774 1775 with self.assertRaisesRegex(ProtocolError, "reuses evidence owned by proposed root"):1776 self._validate_output("requirements")1777 1778 def test_closed_root_requires_new_evidence(self) -> None:1779 output = self._output()1780 output["verdict"] = "findings require fixes"1781 output["checked_inventory_ids"] = ["INV-2"]1782 output["findings"] = [self._finding("ROOT_CLOSED")]1783 self._write_json(self.output_path, output)1784 1785 with self.assertRaisesRegex(ProtocolError, "reopens closed root"):1786 self._validate_output("lifecycle")1787 1788 output["findings"][0]["root_cause_evidence"]["new_inventory_ids"] = ["INV-2"]1789 self._write_json(self.output_path, output)1790 self._validate_output("lifecycle")1791 1792 output["findings"][0]["root_cause_evidence"]["new_inventory_ids"] = ["INV-1"]1793 self._write_json(self.output_path, output)1794 with self.assertRaisesRegex(ProtocolError, "owned by another canonical root"):1795 self._validate_output("lifecycle")1796 1797 output["findings"][0]["root_cause_evidence"]["new_inventory_ids"] = []1798 output["findings"][0]["root_cause_evidence"]["new_contract_evidence_ids"] = [1799 "DOES-NOT-EXIST"1800 ]1801 self._write_json(self.output_path, output)1802 with self.assertRaisesRegex(ProtocolError, "unindexed root evidence"):1803 self._validate_output("lifecycle")1804 1805 output["findings"][0]["root_cause_evidence"]["new_contract_evidence_ids"] = []1806 output["findings"][0]["root_cause_evidence"]["new_inventory_ids"] = [1807 "INV-MISSING"1808 ]1809 self._write_json(self.output_path, output)1810 with self.assertRaisesRegex(ProtocolError, "unindexed root evidence"):1811 self._validate_output("lifecycle")1812 1813 output["findings"][0]["root_cause_evidence"]["new_inventory_ids"] = []1814 output["findings"][0]["root_cause_evidence"]["new_contract_evidence_ids"] = [1815 "E-DIFF"1816 ]1817 self._write_json(self.output_path, output)1818 with self.assertRaisesRegex(1819 ProtocolError, "must be new in the current ledger round"1820 ):1821 self._validate_output("lifecycle")1822 1823 output["findings"][0]["root_cause_evidence"]["new_contract_evidence_ids"] = [1824 "E-ROOT"1825 ]1826 self._write_json(self.output_path, output)1827 self._validate_output("lifecycle")1828 1829 def test_sibling_scan_requires_known_root_and_inventory(self) -> None:1830 output = self._output()1831 output["sibling_scenario_scan"] = [1832 {1833 "root_cause_id": "RENAMED_ROOT",1834 "inventory_ids": ["INV-1"],1835 "result": "No sibling failure.",1836 }1837 ]1838 self._write_json(self.output_path, output)1839 1840 with self.assertRaisesRegex(1841 ProtocolError, "must reference a canonical or proposed root"1842 ):1843 self._validate_output("requirements")1844 1845 output["sibling_scenario_scan"][0]["root_cause_id"] = "ROOT_EXISTING"1846 output["sibling_scenario_scan"][0]["inventory_ids"] = ["INV-MISSING"]1847 self._write_json(self.output_path, output)1848 with self.assertRaisesRegex(ProtocolError, "unknown inventory IDs"):1849 self._validate_output("requirements")1850 1851 def test_newly_promoted_root_uses_current_round_ownership(self) -> None:1852 prior_path = self.root / "prior-ledger.json"1853 prior = copy.deepcopy(self.packet["ledger"])1854 prior["root_causes"] = [prior["root_causes"][0]]1855 prior["contract_evidence_sha256"] = {"E-DIFF": prior["contract_evidence_sha256"]["E-DIFF"]}1856 prior["inventory_sha256"] = {"INV-1": prior["inventory_sha256"]["INV-1"]}1857 self._write_json(prior_path, prior)1858 prior_digest = hashlib.sha256(prior_path.read_bytes()).hexdigest()1859 1860 packet = copy.deepcopy(self.packet)1861 packet["ledger"]["current_round"] = 21862 packet["ledger"]["remaining_budget"] = 41863 self._write_packet(self.packet_path, packet)1864 output = self._output()1865 output["verdict"] = "findings require fixes"1866 output["checked_inventory_ids"] = ["INV-2"]1867 finding = self._finding("ROOT_CLOSED")1868 finding["root_cause_evidence"]["new_contract_evidence_ids"] = ["E-ROOT"]1869 output["findings"] = [finding]1870 self._write_json(self.output_path, output)1871 1872 validate_reviewer_output(1873 self.packet_path,1874 "lifecycle",1875 self.output_path,1876 "task-123",1877 self.ledger_path,1878 prior_path,1879 prior_digest,1880 )1881 1882 output["findings"][0]["root_cause_evidence"]["new_contract_evidence_ids"] = [1883 "E-NEW"1884 ]1885 self._write_json(self.output_path, output)1886 with self.assertRaisesRegex(1887 ProtocolError, "must be new in the current ledger round"1888 ):1889 validate_reviewer_output(1890 self.packet_path,1891 "lifecycle",1892 self.output_path,1893 "task-123",1894 self.ledger_path,1895 prior_path,1896 prior_digest,1897 )1898 1899 def test_reviewer_output_rejects_boolean_inspection_count(self) -> None:1900 output = self._output()1901 output["inspection_call_count"] = False1902 self._write_json(self.output_path, output)1903 1904 with self.assertRaisesRegex(ProtocolError, "nonnegative integer"):1905 self._validate_output("requirements")1906 1907 def test_cli_reports_protocol_errors_without_traceback(self) -> None:1908 invalid = copy.deepcopy(self.packet)1909 invalid["schema_version"] = 21910 self._write_packet(self.packet_path, invalid)1911 1912 completed = subprocess.run(1913 (1914 sys.executable,1915 str(Path(__file__).with_name("review_protocol.py")),1916 "packet",1917 "--packet",1918 str(self.packet_path),1919 "--task-id",1920 "task-123",1921 "--ledger",1922 str(self.ledger_path),1923 ),1924 capture_output=True,1925 text=True,1926 )1927 1928 self.assertEqual(completed.returncode, 2)1929 self.assertIn("schema_version must be integer 1", completed.stderr)1930 self.assertNotIn("Traceback", completed.stderr)1931 1932 def test_cli_reports_invalid_artifact_paths_without_traceback(self) -> None:1933 invalid = copy.deepcopy(self.packet)1934 invalid["evidence_artifacts"][0]["path"] = "/tmp/invalid\0path"1935 self._write_packet(self.packet_path, invalid)1936 1937 completed = subprocess.run(1938 (1939 sys.executable,1940 str(Path(__file__).with_name("review_protocol.py")),1941 "packet",1942 "--packet",1943 str(self.packet_path),1944 "--task-id",1945 "task-123",1946 "--ledger",1947 str(self.ledger_path),1948 ),1949 capture_output=True,1950 text=True,1951 )1952 1953 self.assertEqual(completed.returncode, 2)1954 self.assertIn("Cannot read evidence artifact E-DIFF.path", completed.stderr)1955 self.assertNotIn("Traceback", completed.stderr)1956 1957 1958def re_escape(value: str) -> str:1959 """Escape a literal string for assertRaisesRegex without importing re in each test."""1960 import re1961 1962 return re.escape(value)1963 1964 1965if __name__ == "__main__":1966 unittest.main()1967