scripts/test_review_protocol.py
scripts/test_review_protocol.pyBrowse 8 files
16,360 tokens
79,279 bytes
Token encoding: o200k_base
Snapshot 1d17ca4
← 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 artifact for artifact in packet["evidence_artifacts"] if artifact["id"] == "E-STATE"121 )122 state_artifact["sha256"] = hashlib.sha256(self.review_state_path.read_bytes()).hexdigest()123 self._write_packet(self.packet_path, packet)124 125 def _validate_packet(self, path: Path | None = None) -> dict[str, object]:126 return validate_packet(path or self.packet_path, "task-123", self.ledger_path)127 128 def _validate_output(self, reviewer_id: str) -> dict[str, object]:129 return validate_reviewer_output(130 self.packet_path,131 reviewer_id,132 self.output_path,133 "task-123",134 self.ledger_path,135 )136 137 def _packet(self) -> dict[str, object]:138 packet: dict[str, Any] = {139 "schema_version": 1,140 "packet_overage_reason": "none",141 "task": {142 "id": "task-123",143 "original_requirement": "Preserve behavior and improve review convergence.",144 "risk_tier": "normal",145 "risk_reason": "Repository workflow only.",146 },147 "scope_contract": {148 "required_behavior": "Validate the review packet before dispatch.",149 "compatibility_requirements": "Preserve the existing fingerprint format.",150 "unsupported_cases": "Arbitrary Markdown parsing is unsupported.",151 "supported_alternative": "Use the reviewer brief manually.",152 },153 "repository": {154 "target": "origin/main",155 "merge_base": "1" * 40,156 "head": "2" * 40,157 "release_boundary": "v0.19.4",158 "status_evidence_id": "E-STATUS",159 "exclusions": [],160 "complete_diff_command": "git diff base...HEAD -- src/example.py",161 },162 "ledger": {163 "path": str(self.ledger_path),164 "task_id": "task-123",165 "round_fingerprint": self.combined,166 "authorized_round_budgets": [6],167 "current_round": 1,168 "remaining_budget": 5,169 "root_causes": [170 {171 "id": "ROOT_EXISTING",172 "status": "open",173 "inventory_ids": ["INV-1"],174 "contract_evidence_ids": ["E-DIFF"],175 },176 {177 "id": "ROOT_CLOSED",178 "status": "closed",179 "inventory_ids": ["INV-2"],180 "contract_evidence_ids": ["E-ROOT"],181 },182 ],183 },184 "manifests": {185 "task": str(self.task_manifest),186 "components": {"api-contract": str(self.component_manifest)},187 "dependency_map": {188 "api-contract": [189 {190 "pathspec": "src/example.py",191 "reason": "The source file defines the reviewed API contract.",192 }193 ]194 },195 },196 "review_state": {197 "evidence_id": "E-STATE",198 "revalidation_command": "uv run python review_state.py --base BASE",199 },200 "verification": {201 "preflight_results": [202 {203 "command": "uv run python -m unittest discover -s scripts",204 "result": "49 tests passed.",205 }206 ],207 "eligible_concurrent_gates": "none",208 "deferred_gates": "make lint; make typecheck; make tests",209 "credited_receipts": [],210 },211 "architecture_references": [],212 "evidence_artifacts": [213 {214 "id": "E-DIFF",215 "path": str(self.evidence),216 "sha256": hashlib.sha256(self.evidence.read_bytes()).hexdigest(),217 "role": "complete-diff",218 "purpose": "Complete raw diff.",219 },220 {221 "id": "E-ROOT",222 "path": str(self.root_evidence),223 "sha256": hashlib.sha256(self.root_evidence.read_bytes()).hexdigest(),224 "role": "supporting",225 "purpose": "Existing root-cause evidence.",226 },227 {228 "id": "E-STATE",229 "path": str(self.review_state_path),230 "sha256": hashlib.sha256(self.review_state_path.read_bytes()).hexdigest(),231 "role": "review-state",232 "purpose": "Authoritative review-state output.",233 },234 {235 "id": "E-STATUS",236 "path": str(self.status_path),237 "sha256": hashlib.sha256(self.status_path.read_bytes()).hexdigest(),238 "role": "repository-status",239 "purpose": "Unfiltered repository status.",240 },241 {242 "id": "E-NEW",243 "path": str(self.new_evidence),244 "sha256": hashlib.sha256(self.new_evidence.read_bytes()).hexdigest(),245 "role": "supporting",246 "purpose": "Unowned evidence for a genuinely new root.",247 },248 ],249 "inventory": [250 {251 "id": "INV-1",252 "kind": "contract",253 "summary": "Public contract row.",254 "surface": "review packet validation",255 "producers": "implementer",256 "consumers": "packet validator and reviewers",257 "behavior": "invalid packets fail before dispatch",258 "exports": "review_protocol.py CLI",259 "adjacent": "reviewer-brief.md",260 "tests": "test_review_protocol.py",261 },262 {263 "id": "INV-2",264 "kind": "authority-data-flow",265 "summary": "Authority flow row.",266 "input_authority": "control-plane task ID and ledger path",267 "validation": "exact task, path, digest, and budget checks",268 "in_memory_state": "parsed packet and ledger",269 "persisted_state": "task-global ledger JSON",270 "retry_replay": "same external authority is supplied again",271 "output": "validated packet summary",272 "exception_exposure": "concise ProtocolError without packet contents",273 "cleanup_revocation": "not applicable",274 },275 ],276 "selected_high_risk_dimensions": [],277 "reviewer_assignments": [278 {279 "reviewer_id": "requirements",280 "primary_dimensions": ["requirement and scope"],281 "inventory_ids": ["INV-1"],282 "high_risk_dimensions": [],283 "expected_components": ["api-contract"],284 "evidence_ids": ["E-DIFF", "E-STATE", "E-STATUS"],285 },286 {287 "reviewer_id": "lifecycle",288 "primary_dimensions": ["security and protocol"],289 "inventory_ids": ["INV-2"],290 "high_risk_dimensions": [],291 "expected_components": ["api-contract"],292 "evidence_ids": ["E-DIFF", "E-STATE", "E-STATUS"],293 },294 ],295 }296 owned_evidence = {297 evidence_id298 for root in packet["ledger"]["root_causes"]299 for evidence_id in root["contract_evidence_ids"]300 }301 packet["ledger"]["contract_evidence_sha256"] = {302 artifact["id"]: artifact["sha256"]303 for artifact in packet["evidence_artifacts"]304 if artifact["id"] in owned_evidence305 }306 packet["ledger"]["inventory_sha256"] = {307 row["id"]: _inventory_digest(row) for row in packet["inventory"]308 }309 return packet310 311 def _receipt(self) -> dict[str, object]:312 fingerprints = {313 "combined": self.combined,314 "components": {"api-contract": self.component},315 "repository": self.repository,316 }317 return {318 "schema_version": 1,319 "command": "uv run python -m unittest discover -s scripts",320 "environment": "macOS, UV_DEFAULT_INDEX=https://pypi.org/simple",321 "exit_status": 0,322 "non_mutation_basis": "The command is documented as non-mutating.",323 "before": fingerprints,324 "after": copy.deepcopy(fingerprints),325 }326 327 def _output(self) -> dict[str, object]:328 return {329 "verdict": "clean",330 "reviewed_fingerprints": {331 "packet": hashlib.sha256(self.packet_path.read_bytes()).hexdigest(),332 "combined": self.combined,333 "components": {"api-contract": self.component},334 },335 "checked_inventory_ids": ["INV-1"],336 "unchecked_inventory_ids": [],337 "high_risk_dimensions_checked": [],338 "focused_probes": [],339 "remaining_uncertainty": [],340 "findings": [],341 "sibling_scenario_scan": [],342 "inspection_call_count": 4,343 "inspection_budget_reason": "none",344 }345 346 def _finding(self, root_cause_id: str) -> dict[str, object]:347 return {348 "priority": "P2",349 "title": "Finding title",350 "location": "src/example.py:1",351 "failure_scenario": "The supported scenario fails.",352 "user_consequence": "The caller sees an error.",353 "support_basis": "Original requirement.",354 "baseline_patch_evidence": "The baseline succeeds.",355 "smallest_safe_correction": "Reuse the existing path.",356 "root_cause_id": root_cause_id,357 "root_cause_evidence": {358 "new_contract_evidence_ids": [],359 "new_inventory_ids": [],360 },361 }362 363 def test_valid_packet_reports_dispatch_digest_and_size(self) -> None:364 summary = self._validate_packet()365 366 self.assertEqual(summary["combined_fingerprint"], self.combined)367 self.assertEqual(summary["packet_size_bytes"], self.packet_path.stat().st_size)368 self.assertEqual(369 summary["packet_sha256"], hashlib.sha256(self.packet_path.read_bytes()).hexdigest()370 )371 self.assertEqual(372 summary["ledger_sha256"], hashlib.sha256(self.ledger_path.read_bytes()).hexdigest()373 )374 375 def test_packet_rejects_duplicate_json_keys(self) -> None:376 packet_text = self.packet_path.read_text()377 self.packet_path.write_text(378 packet_text.replace(379 '"task": {\n "id": "task-123",',380 '"task": {\n "id": "task-123",\n "id": "task-123",',381 1,382 )383 )384 385 with self.assertRaisesRegex(ProtocolError, "Duplicate JSON key.*id"):386 self._validate_packet()387 388 def test_packet_rejects_non_finite_json_numbers(self) -> None:389 """Reject numeric constants and exponents that parse as non-finite."""390 for encoded_value in ("NaN", "1e999"):391 with self.subTest(encoded_value=encoded_value):392 packet = copy.deepcopy(self.packet)393 packet["ignored_number"] = 0394 self._write_packet(self.packet_path, packet)395 packet_text = self.packet_path.read_text().replace(396 '"ignored_number": 0',397 f'"ignored_number": {encoded_value}',398 1,399 )400 self.packet_path.write_text(packet_text)401 402 with self.assertRaisesRegex(ProtocolError, "Non-finite JSON number"):403 self._validate_packet()404 405 def test_packet_reports_json_parser_limits_as_protocol_errors(self) -> None:406 """Convert runtime parser limits into concise protocol failures."""407 for error in (ValueError("integer limit"), RecursionError("nesting limit")):408 with (409 self.subTest(error=type(error).__name__),410 mock.patch("review_protocol.json.loads", side_effect=error),411 self.assertRaisesRegex(ProtocolError, "Cannot read JSON object"),412 ):413 self._validate_packet()414 415 def test_packet_defers_broad_final_gates_until_clean_review(self) -> None:416 packet = copy.deepcopy(self.packet)417 packet["verification"]["eligible_concurrent_gates"] = "make tests"418 self._write_packet(self.packet_path, packet)419 with self.assertRaisesRegex(420 ProtocolError,421 "verification.eligible_concurrent_gates must be 'none'",422 ):423 self._validate_packet()424 425 packet["verification"]["eligible_concurrent_gates"] = "none"426 packet["verification"]["deferred_gates"] = "none"427 self._write_packet(self.packet_path, packet)428 with self.assertRaisesRegex(429 ProtocolError,430 "verification.deferred_gates must list the applicable broad final gates",431 ):432 self._validate_packet()433 434 def test_packet_fails_closed_on_missing_field_or_incomplete_assignment(self) -> None:435 cases = []436 missing = copy.deepcopy(self.packet)437 del missing["scope_contract"]438 cases.append((missing, "Missing required packet field: scope_contract.required_behavior"))439 incomplete = copy.deepcopy(self.packet)440 incomplete["reviewer_assignments"][1]["inventory_ids"] = ["INV-1"]441 cases.append((incomplete, "must cover the exact inventory and dimensions"))442 443 for index, (packet, expected) in enumerate(cases):444 with self.subTest(expected=expected):445 path = self.root / f"invalid-{index}.json"446 self._write_packet(path, packet)447 with self.assertRaisesRegex(ProtocolError, re_escape(expected)):448 self._validate_packet(path)449 450 def test_packet_requires_indexed_ledger_evidence(self) -> None:451 packet = copy.deepcopy(self.packet)452 packet["ledger"]["root_causes"][0]["contract_evidence_ids"] = ["REQ-UNINDEXED"]453 self._write_packet(self.packet_path, packet)454 455 with self.assertRaisesRegex(ProtocolError, r"evidence=\['REQ-UNINDEXED'\]"):456 self._validate_packet()457 458 def test_packet_resolves_manifest_and_ledger_authority(self) -> None:459 missing_manifest = copy.deepcopy(self.packet)460 missing_manifest["manifests"]["task"] = str(self.root / "missing-task.paths")461 self._write_packet(self.packet_path, missing_manifest)462 with self.assertRaisesRegex(ProtocolError, "Cannot read manifests.task"):463 self._validate_packet()464 465 self.task_manifest.write_text("src/other.py\n")466 self._write_packet(self.packet_path, self.packet)467 with self.assertRaisesRegex(ProtocolError, "must match review_state.pathspecs"):468 self._validate_packet()469 self.task_manifest.write_text("src/example.py\n")470 471 missing_ledger = copy.deepcopy(self.packet)472 missing_ledger["ledger"]["path"] = str(self.root / "missing-ledger.json")473 self._write_json(self.packet_path, missing_ledger)474 with self.assertRaisesRegex(ProtocolError, "Cannot read ledger.path"):475 validate_packet(self.packet_path, "task-123", self.root / "missing-ledger.json")476 477 divergent = copy.deepcopy(self.packet)478 self._write_packet(self.packet_path, divergent)479 ledger = copy.deepcopy(divergent["ledger"])480 ledger["remaining_budget"] = 99481 self._write_json(self.ledger_path, ledger)482 with self.assertRaisesRegex(ProtocolError, "must match the packet ledger exactly"):483 self._validate_packet()484 485 self.ledger_path.write_text("{not json")486 with self.assertRaisesRegex(ProtocolError, "Cannot read JSON object"):487 self._validate_packet()488 489 def test_dependency_map_requires_exact_component_entries(self) -> None:490 """Require complete machine-readable component dependency boundaries."""491 valid_entry = {492 "pathspec": "src/example.py",493 "reason": "The source file defines the reviewed API contract.",494 }495 cases = (496 ("api-contract has no dependencies.", "dependency_map must be an object"),497 ({}, "must cover the exact component names"),498 ({"api-contract": []}, "must contain at least one dependency"),499 (500 {"api-contract": [{**valid_entry, "note": "unvalidated"}]},501 "unexpected=\\['note'\\]",502 ),503 (504 {"api-contract": [valid_entry, copy.deepcopy(valid_entry)]},505 "contains duplicate pathspec",506 ),507 )508 509 for dependency_map, expected in cases:510 with self.subTest(expected=expected):511 packet = copy.deepcopy(self.packet)512 packet["manifests"]["dependency_map"] = dependency_map513 self._write_packet(self.packet_path, packet)514 515 with self.assertRaisesRegex(ProtocolError, expected):516 self._validate_packet()517 518 def test_ledger_task_identity_must_match_packet(self) -> None:519 packet = copy.deepcopy(self.packet)520 packet["ledger"]["task_id"] = "another-task"521 self._write_packet(self.packet_path, packet)522 523 with self.assertRaisesRegex(ProtocolError, "ledger.task_id must match"):524 self._validate_packet()525 526 def test_control_plane_rejects_coordinated_task_and_ledger_replacement(self) -> None:527 packet = copy.deepcopy(self.packet)528 packet["task"]["id"] = "replacement-task"529 packet["ledger"]["task_id"] = "replacement-task"530 self._write_packet(self.packet_path, packet)531 532 with self.assertRaisesRegex(ProtocolError, "control-plane task ID"):533 self._validate_packet()534 535 replacement_ledger = self.root / "replacement-ledger.json"536 packet = copy.deepcopy(self.packet)537 packet["ledger"]["path"] = str(replacement_ledger)538 self._write_packet(self.packet_path, packet)539 with self.assertRaisesRegex(ProtocolError, "control-plane ledger path"):540 self._validate_packet()541 542 def test_ledger_budget_history_is_authoritative(self) -> None:543 packet = copy.deepcopy(self.packet)544 packet["ledger"]["remaining_budget"] = 99545 self._write_packet(self.packet_path, packet)546 547 with self.assertRaisesRegex(ProtocolError, "authorized budget history"):548 self._validate_packet()549 550 packet = copy.deepcopy(self.packet)551 packet["ledger"]["authorized_round_budgets"] = [True]552 packet["ledger"]["remaining_budget"] = 0553 self._write_packet(self.packet_path, packet)554 with self.assertRaisesRegex(ProtocolError, "must be a positive integer"):555 self._validate_packet()556 557 def test_ledger_round_fingerprint_matches_packet(self) -> None:558 packet = copy.deepcopy(self.packet)559 packet["ledger"]["round_fingerprint"] = "0" * 64560 self._write_packet(self.packet_path, packet)561 562 with self.assertRaisesRegex(ProtocolError, "must match the packet fingerprint"):563 self._validate_packet()564 565 def test_ledger_digest_maps_cover_exact_owned_ids(self) -> None:566 """Require digest bindings for exactly the IDs owned by roots."""567 packet = copy.deepcopy(self.packet)568 del packet["ledger"]["contract_evidence_sha256"]["E-ROOT"]569 self._write_packet(self.packet_path, packet)570 571 with self.assertRaisesRegex(ProtocolError, "must bind the exact owned IDs"):572 self._validate_packet()573 574 packet = copy.deepcopy(self.packet)575 packet["ledger"]["contract_evidence_sha256"]["E-NEW"] = hashlib.sha256(576 self.new_evidence.read_bytes()577 ).hexdigest()578 self._write_packet(self.packet_path, packet)579 580 with self.assertRaisesRegex(ProtocolError, r"unexpected=\['E-NEW'\]"):581 self._validate_packet()582 583 def test_ledger_digest_maps_match_indexed_content(self) -> None:584 """Reject ledger bindings that differ from current indexed content."""585 packet = copy.deepcopy(self.packet)586 packet["ledger"]["contract_evidence_sha256"]["E-ROOT"] = "0" * 64587 self._write_packet(self.packet_path, packet)588 589 with self.assertRaisesRegex(ProtocolError, "evidence digest mismatch for E-ROOT"):590 self._validate_packet()591 592 packet = copy.deepcopy(self.packet)593 packet["ledger"]["inventory_sha256"]["INV-2"] = "0" * 64594 self._write_packet(self.packet_path, packet)595 596 with self.assertRaisesRegex(ProtocolError, "inventory digest mismatch for INV-2"):597 self._validate_packet()598 599 def test_canonical_roots_cannot_alias_the_same_ownership(self) -> None:600 packet = copy.deepcopy(self.packet)601 alias = copy.deepcopy(packet["ledger"]["root_causes"][0])602 alias["id"] = "RENAMED_ROOT"603 packet["ledger"]["root_causes"].append(alias)604 self._write_packet(self.packet_path, packet)605 606 with self.assertRaisesRegex(ProtocolError, "overlap on inventory INV-1"):607 self._validate_packet()608 609 packet = copy.deepcopy(self.packet)610 alias = copy.deepcopy(packet["ledger"]["root_causes"][0])611 alias["id"] = "RENAMED_ROOT"612 alias["contract_evidence_ids"].append("E-NEW")613 packet["ledger"]["root_causes"].append(alias)614 self._write_packet(self.packet_path, packet)615 with self.assertRaisesRegex(ProtocolError, "overlap on inventory INV-1"):616 self._validate_packet()617 618 def test_prior_ledger_makes_history_append_only(self) -> None:619 prior_path = self.root / "prior-ledger.json"620 prior = copy.deepcopy(self.packet["ledger"])621 self._write_json(prior_path, prior)622 prior_digest = hashlib.sha256(prior_path.read_bytes()).hexdigest()623 624 current = copy.deepcopy(self.packet)625 current["ledger"]["current_round"] = 2626 current["ledger"]["remaining_budget"] = 4627 self._write_packet(self.packet_path, current)628 validate_packet(629 self.packet_path,630 "task-123",631 self.ledger_path,632 prior_path,633 prior_digest,634 )635 636 skipped = copy.deepcopy(self.packet)637 skipped["ledger"]["current_round"] = 3638 skipped["ledger"]["remaining_budget"] = 3639 self._write_packet(self.packet_path, skipped)640 with self.assertRaisesRegex(ProtocolError, "advance by exactly one"):641 validate_packet(642 self.packet_path,643 "task-123",644 self.ledger_path,645 prior_path,646 prior_digest,647 )648 649 reset = copy.deepcopy(current)650 reset["ledger"]["authorized_round_budgets"] = [2]651 reset["ledger"]["remaining_budget"] = 0652 self._write_packet(self.packet_path, reset)653 with self.assertRaisesRegex(ProtocolError, "must preserve the prior prefix"):654 validate_packet(655 self.packet_path,656 "task-123",657 self.ledger_path,658 prior_path,659 prior_digest,660 )661 662 removed_root = copy.deepcopy(current)663 removed_root["ledger"]["root_causes"] = [664 {665 "id": "REPLACEMENT_ROOT",666 "status": "open",667 "inventory_ids": ["INV-1", "INV-2"],668 "contract_evidence_ids": ["E-DIFF"],669 }670 ]671 del removed_root["ledger"]["contract_evidence_sha256"]["E-ROOT"]672 self._write_packet(self.packet_path, removed_root)673 with self.assertRaisesRegex(ProtocolError, "removed prior canonical root"):674 validate_packet(675 self.packet_path,676 "task-123",677 self.ledger_path,678 prior_path,679 prior_digest,680 )681 682 def test_prior_ledger_binds_owned_evidence_content(self) -> None:683 """Reject content replacement under a previously owned evidence ID."""684 prior_path = self.root / "prior-ledger.json"685 prior = copy.deepcopy(self.packet["ledger"])686 prior["contract_evidence_sha256"] = {687 "E-DIFF": hashlib.sha256(self.evidence.read_bytes()).hexdigest(),688 "E-ROOT": hashlib.sha256(self.root_evidence.read_bytes()).hexdigest(),689 }690 self._write_json(prior_path, prior)691 prior_digest = hashlib.sha256(prior_path.read_bytes()).hexdigest()692 693 self.root_evidence.write_text("replacement root evidence\n")694 packet = copy.deepcopy(self.packet)695 root_artifact = next(696 artifact for artifact in packet["evidence_artifacts"] if artifact["id"] == "E-ROOT"697 )698 replacement_digest = hashlib.sha256(self.root_evidence.read_bytes()).hexdigest()699 root_artifact["sha256"] = replacement_digest700 packet["ledger"]["contract_evidence_sha256"] = {701 "E-DIFF": hashlib.sha256(self.evidence.read_bytes()).hexdigest(),702 "E-ROOT": replacement_digest,703 }704 packet["ledger"]["current_round"] = 2705 packet["ledger"]["remaining_budget"] = 4706 self._write_packet(self.packet_path, packet)707 708 with self.assertRaisesRegex(ProtocolError, "changed prior evidence E-ROOT"):709 validate_packet(710 self.packet_path,711 "task-123",712 self.ledger_path,713 prior_path,714 prior_digest,715 )716 717 def test_prior_ledger_binds_owned_inventory_content(self) -> None:718 """Reject content replacement under a previously owned inventory ID."""719 prior_path = self.root / "prior-ledger.json"720 prior = copy.deepcopy(self.packet["ledger"])721 prior["inventory_sha256"] = {722 row["id"]: _inventory_digest(row) for row in self.packet["inventory"]723 }724 self._write_json(prior_path, prior)725 prior_digest = hashlib.sha256(prior_path.read_bytes()).hexdigest()726 727 packet = copy.deepcopy(self.packet)728 inventory_row = next(row for row in packet["inventory"] if row["id"] == "INV-2")729 inventory_row["validation"] = "replacement validation contract"730 packet["ledger"]["inventory_sha256"] = {731 row["id"]: _inventory_digest(row) for row in packet["inventory"]732 }733 packet["ledger"]["current_round"] = 2734 packet["ledger"]["remaining_budget"] = 4735 self._write_packet(self.packet_path, packet)736 737 with self.assertRaisesRegex(ProtocolError, "changed prior inventory INV-2"):738 validate_packet(739 self.packet_path,740 "task-123",741 self.ledger_path,742 prior_path,743 prior_digest,744 )745 746 def test_same_round_retry_requires_the_prior_fingerprint(self) -> None:747 prior_path = self.root / "prior-ledger.json"748 prior = copy.deepcopy(self.packet["ledger"])749 prior["round_fingerprint"] = "0" * 64750 self._write_json(prior_path, prior)751 prior_digest = hashlib.sha256(prior_path.read_bytes()).hexdigest()752 753 with self.assertRaisesRegex(ProtocolError, "same-round retry fingerprint"):754 validate_packet(755 self.packet_path,756 "task-123",757 self.ledger_path,758 prior_path,759 prior_digest,760 )761 762 advanced = copy.deepcopy(self.packet)763 advanced["ledger"]["current_round"] = 2764 advanced["ledger"]["remaining_budget"] = 4765 self._write_packet(self.packet_path, advanced)766 767 validate_packet(768 self.packet_path,769 "task-123",770 self.ledger_path,771 prior_path,772 prior_digest,773 )774 775 def test_same_round_retry_cannot_expand_the_budget_history(self) -> None:776 prior_path = self.root / "prior-ledger.json"777 prior = copy.deepcopy(self.packet["ledger"])778 self._write_json(prior_path, prior)779 prior_digest = hashlib.sha256(prior_path.read_bytes()).hexdigest()780 expanded = copy.deepcopy(self.packet)781 expanded["ledger"]["authorized_round_budgets"] = [6, 2]782 expanded["ledger"]["remaining_budget"] = 7783 self._write_packet(self.packet_path, expanded)784 785 with self.assertRaisesRegex(ProtocolError, "same-round retry budget history"):786 validate_packet(787 self.packet_path,788 "task-123",789 self.ledger_path,790 prior_path,791 prior_digest,792 )793 794 expanded["ledger"]["current_round"] = 2795 expanded["ledger"]["remaining_budget"] = 6796 self._write_packet(self.packet_path, expanded)797 validate_packet(798 self.packet_path,799 "task-123",800 self.ledger_path,801 prior_path,802 prior_digest,803 )804 805 def test_later_round_requires_digest_bound_prior_ledger(self) -> None:806 packet = copy.deepcopy(self.packet)807 packet["ledger"]["current_round"] = 2808 packet["ledger"]["remaining_budget"] = 4809 self._write_packet(self.packet_path, packet)810 811 with self.assertRaisesRegex(ProtocolError, "digest-bound prior ledger"):812 self._validate_packet()813 814 def test_current_ledger_cannot_authorize_its_own_history(self) -> None:815 packet = copy.deepcopy(self.packet)816 packet["ledger"]["authorized_round_budgets"] = [2]817 packet["ledger"]["current_round"] = 2818 packet["ledger"]["remaining_budget"] = 0819 self._write_packet(self.packet_path, packet)820 current_digest = hashlib.sha256(self.ledger_path.read_bytes()).hexdigest()821 822 with self.assertRaisesRegex(ProtocolError, "distinct from the current ledger"):823 validate_packet(824 self.packet_path,825 "task-123",826 self.ledger_path,827 self.ledger_path,828 current_digest,829 )830 831 def test_prior_ledger_hardlink_cannot_alias_current_ledger(self) -> None:832 packet = copy.deepcopy(self.packet)833 packet["ledger"]["authorized_round_budgets"] = [2]834 packet["ledger"]["current_round"] = 2835 packet["ledger"]["remaining_budget"] = 0836 self._write_packet(self.packet_path, packet)837 prior_path = self.root / "prior-ledger.json"838 os.link(self.ledger_path, prior_path)839 current_digest = hashlib.sha256(self.ledger_path.read_bytes()).hexdigest()840 841 with self.assertRaisesRegex(ProtocolError, "distinct from the current ledger"):842 validate_packet(843 self.packet_path,844 "task-123",845 self.ledger_path,846 prior_path,847 current_digest,848 )849 850 def test_inventory_requires_kind_specific_evidence(self) -> None:851 cases = ((0, "surface", "contract fields"), (1, "validation", "authority-data-flow"))852 for index, field, expected in cases:853 with self.subTest(field=field):854 packet = copy.deepcopy(self.packet)855 del packet["inventory"][index][field]856 path = self.root / f"missing-inventory-{index}.json"857 self._write_packet(path, packet)858 with self.assertRaisesRegex(ProtocolError, expected):859 self._validate_packet(path)860 861 def test_every_inventory_requires_one_canonical_root_owner(self) -> None:862 packet = copy.deepcopy(self.packet)863 orphan = copy.deepcopy(packet["inventory"][0])864 orphan["id"] = "INV-ORPHAN"865 packet["inventory"].append(orphan)866 packet["reviewer_assignments"][0]["inventory_ids"].append("INV-ORPHAN")867 self._write_packet(self.packet_path, packet)868 869 with self.assertRaisesRegex(ProtocolError, r"unowned=\['INV-ORPHAN'\]"):870 self._validate_packet()871 872 def test_unfiltered_changed_paths_require_explicit_exclusions(self) -> None:873 state = copy.deepcopy(self.review_state)874 state["unfiltered"]["workspace"] = copy.deepcopy(state["unfiltered"]["workspace"])875 state["unfiltered"]["workspace"].insert(876 0,877 {878 "path": "notes/unrelated.txt",879 "kind": "file",880 "executable": False,881 "sha256": "e" * 64,882 },883 )884 state["repository_fingerprint"] = _repository_fingerprint(885 content_fingerprint=state["content_fingerprint"],886 head=state["head"],887 status_sha256=state["status_sha256"],888 tracked_diff_sha256=state["tracked_diff_sha256"],889 complete_diff_sha256=state["complete_diff_sha256"],890 unfiltered_status_sha256=state["unfiltered"]["status_sha256"],891 unfiltered_content_fingerprint=_content_fingerprint(892 state["base"], state["unfiltered"]["workspace"]893 ),894 )895 self._write_json(self.review_state_path, state)896 packet = copy.deepcopy(self.packet)897 state_artifact = next(898 artifact for artifact in packet["evidence_artifacts"] if artifact["id"] == "E-STATE"899 )900 state_artifact["sha256"] = hashlib.sha256(self.review_state_path.read_bytes()).hexdigest()901 self._write_packet(self.packet_path, packet)902 903 with self.assertRaisesRegex(ProtocolError, "must exactly account"):904 self._validate_packet()905 906 packet["repository"]["exclusions"] = [907 {"path": "notes/unrelated.txt", "reason": "Unrelated user-owned note."}908 ]909 self._write_packet(self.packet_path, packet)910 self._validate_packet()911 912 def test_preflight_results_require_exact_command_result_records(self) -> None:913 packet = copy.deepcopy(self.packet)914 packet["verification"]["preflight_results"] = "Tests passed."915 self._write_packet(self.packet_path, packet)916 with self.assertRaisesRegex(ProtocolError, "preflight_results must be an array"):917 self._validate_packet()918 919 packet["verification"]["preflight_results"] = [920 {"command": "uv run pytest <path>", "result": "passed"}921 ]922 self._write_packet(self.packet_path, packet)923 with self.assertRaisesRegex(ProtocolError, "command contains a placeholder token"):924 self._validate_packet()925 926 packet = copy.deepcopy(self.packet)927 packet["verification"]["preflight_results"][0]["details"] = "Unvalidated metadata."928 self._write_packet(self.packet_path, packet)929 with self.assertRaisesRegex(ProtocolError, "unexpected=\\['details'\\]"):930 self._validate_packet()931 932 def test_preflight_commands_must_be_unique(self) -> None:933 """Reject repeated preflight records for the same exact command."""934 packet = copy.deepcopy(self.packet)935 duplicate = copy.deepcopy(packet["verification"]["preflight_results"][0])936 duplicate["result"] = "The same command passed again."937 packet["verification"]["preflight_results"].append(duplicate)938 self._write_packet(self.packet_path, packet)939 940 with self.assertRaisesRegex(ProtocolError, "Duplicate preflight command"):941 self._validate_packet()942 943 def test_every_reviewer_receives_all_components_and_complete_diff(self) -> None:944 cases = []945 no_components = copy.deepcopy(self.packet)946 no_components["reviewer_assignments"][0]["expected_components"] = []947 cases.append((no_components, "must receive every component"))948 no_evidence = copy.deepcopy(self.packet)949 no_evidence["reviewer_assignments"][0]["evidence_ids"] = []950 cases.append((no_evidence, "must receive every component"))951 952 for index, (packet, expected) in enumerate(cases):953 with self.subTest(expected=expected):954 path = self.root / f"incomplete-reviewer-{index}.json"955 self._write_packet(path, packet)956 with self.assertRaisesRegex(ProtocolError, re_escape(expected)):957 self._validate_packet(path)958 959 def test_reviewer_assignments_require_distinct_specialties(self) -> None:960 """Require the two reviewers to have complementary specialties."""961 packet = copy.deepcopy(self.packet)962 packet["reviewer_assignments"][1]["primary_dimensions"] = ["requirement and scope"]963 self._write_packet(self.packet_path, packet)964 965 with self.assertRaisesRegex(ProtocolError, "overlapping primary specialty"):966 self._validate_packet()967 968 packet = copy.deepcopy(self.packet)969 packet["selected_high_risk_dimensions"] = ["persistence"]970 for assignment in packet["reviewer_assignments"]:971 assignment["high_risk_dimensions"] = ["persistence"]972 self._write_packet(self.packet_path, packet)973 974 with self.assertRaisesRegex(ProtocolError, "overlapping high-risk specialty"):975 self._validate_packet()976 977 def test_packet_and_ledger_reject_json_booleans_as_integers(self) -> None:978 cases = []979 schema = copy.deepcopy(self.packet)980 schema["schema_version"] = True981 cases.append((schema, "schema_version must be integer 1"))982 current_round = copy.deepcopy(self.packet)983 current_round["ledger"]["current_round"] = True984 cases.append((current_round, "current_round must be a positive integer"))985 986 for index, (packet, expected) in enumerate(cases):987 with self.subTest(expected=expected):988 path = self.root / f"boolean-integer-{index}.json"989 self._write_packet(path, packet)990 with self.assertRaisesRegex(ProtocolError, re_escape(expected)):991 self._validate_packet(path)992 993 def test_packet_rejects_changed_evidence(self) -> None:994 self.evidence.write_text("changed\n")995 996 with self.assertRaisesRegex(ProtocolError, "digest mismatch"):997 self._validate_packet()998 999 @unittest.skipIf(os.name == "nt", "Symlink creation requires platform privileges.")1000 def test_packet_rejects_aliased_evidence_paths(self) -> None:1001 alias = self.root / "root-evidence-alias.txt"1002 alias.symlink_to(self.root_evidence)1003 packet = copy.deepcopy(self.packet)1004 packet["evidence_artifacts"].append(1005 {1006 "id": "E-ROOT-ALIAS",1007 "path": str(alias),1008 "sha256": hashlib.sha256(self.root_evidence.read_bytes()).hexdigest(),1009 "role": "supporting",1010 "purpose": "Alias of existing root-cause evidence.",1011 }1012 )1013 self._write_packet(self.packet_path, packet)1014 1015 with self.assertRaisesRegex(ProtocolError, "Duplicate evidence artifact file identity"):1016 self._validate_packet()1017 1018 def test_copied_evidence_does_not_reopen_a_closed_root(self) -> None:1019 prior_path = self.root / "prior-ledger.json"1020 self._write_json(prior_path, self.packet["ledger"])1021 prior_digest = hashlib.sha256(prior_path.read_bytes()).hexdigest()1022 copied_evidence = self.root / "copied-root-evidence.txt"1023 copied_evidence.write_bytes(self.root_evidence.read_bytes())1024 packet = copy.deepcopy(self.packet)1025 packet["evidence_artifacts"].append(1026 {1027 "id": "E-COPY",1028 "path": str(copied_evidence),1029 "sha256": hashlib.sha256(copied_evidence.read_bytes()).hexdigest(),1030 "role": "supporting",1031 "purpose": "Byte copy of existing root-cause evidence.",1032 }1033 )1034 closed_root = next(1035 root for root in packet["ledger"]["root_causes"] if root["id"] == "ROOT_CLOSED"1036 )1037 closed_root["status"] = "open"1038 closed_root["contract_evidence_ids"].append("E-COPY")1039 packet["ledger"]["contract_evidence_sha256"]["E-COPY"] = hashlib.sha256(1040 copied_evidence.read_bytes()1041 ).hexdigest()1042 packet["ledger"]["current_round"] = 21043 packet["ledger"]["remaining_budget"] = 41044 self._write_packet(self.packet_path, packet)1045 1046 with self.assertRaisesRegex(ProtocolError, "without content-new evidence"):1047 validate_packet(1048 self.packet_path,1049 "task-123",1050 self.ledger_path,1051 prior_path,1052 prior_digest,1053 )1054 1055 @unittest.skipUnless(Path("/dev/null").exists(), "Requires a POSIX device path.")1056 def test_packet_rejects_non_regular_evidence_files(self) -> None:1057 packet = copy.deepcopy(self.packet)1058 packet["evidence_artifacts"][0]["path"] = "/dev/null"1059 packet["evidence_artifacts"][0]["sha256"] = hashlib.sha256(b"").hexdigest()1060 self._write_packet(self.packet_path, packet)1061 1062 with self.assertRaisesRegex(ProtocolError, "must be a regular file"):1063 self._validate_packet()1064 1065 @unittest.skipUnless(hasattr(os, "mkfifo"), "Requires POSIX FIFO support.")1066 def test_artifact_file_type_is_verified_after_open(self) -> None:1067 fifo = self.root / "artifact.pipe"1068 os.mkfifo(fifo)1069 regular_stat = self.packet_path.stat()1070 1071 with (1072 mock.patch.object(Path, "stat", return_value=regular_stat),1073 mock.patch.object(Path, "read_bytes", return_value=b"not from the FIFO"),1074 self.assertRaisesRegex(ProtocolError, "must be a regular file"),1075 ):1076 _read_bytes(str(fifo), "artifact")1077 1078 def test_complete_diff_must_match_review_state(self) -> None:1079 partial_diff = self.root / "partial.diff"1080 partial_diff.write_text("partial diff\n")1081 packet = copy.deepcopy(self.packet)1082 packet["evidence_artifacts"][0]["path"] = str(partial_diff)1083 packet["evidence_artifacts"][0]["sha256"] = hashlib.sha256(1084 partial_diff.read_bytes()1085 ).hexdigest()1086 self._write_packet(self.packet_path, packet)1087 1088 with self.assertRaisesRegex(ProtocolError, "must match review_state.complete_diff_sha256"):1089 self._validate_packet()1090 1091 def test_complete_diff_paths_must_match_task_workspace(self) -> None:1092 state = copy.deepcopy(self.review_state)1093 state["complete_diff_paths"] = []1094 self._write_review_state(state)1095 1096 with self.assertRaisesRegex(ProtocolError, "must exactly match the task workspace"):1097 self._validate_packet()1098 1099 def test_review_state_artifact_is_digest_bound(self) -> None:1100 packet = copy.deepcopy(self.packet)1101 state_artifact = next(1102 artifact for artifact in packet["evidence_artifacts"] if artifact["id"] == "E-STATE"1103 )1104 state_artifact["sha256"] = "0" * 641105 self._write_packet(self.packet_path, packet)1106 1107 with self.assertRaisesRegex(ProtocolError, "evidence artifact E-STATE digest mismatch"):1108 self._validate_packet()1109 1110 def test_review_state_requires_complete_typed_workspace_entries(self) -> None:1111 state = copy.deepcopy(self.review_state)1112 del state["workspace"][0]["executable"]1113 self._write_review_state(state)1114 1115 with self.assertRaisesRegex(ProtocolError, "file schema: missing"):1116 self._validate_packet()1117 1118 def test_review_state_rejects_unknown_fields_for_every_workspace_kind(self) -> None:1119 entries = (1120 {1121 "path": "file",1122 "kind": "file",1123 "executable": False,1124 "sha256": "a" * 64,1125 },1126 {"path": "link", "kind": "symlink", "sha256": "b" * 64},1127 {1128 "path": "gitlink",1129 "kind": "gitlink",1130 "head": "c" * 40,1131 },1132 {"path": "directory", "kind": "directory"},1133 {"path": "missing", "kind": "missing"},1134 )1135 for entry in entries:1136 with self.subTest(kind=entry["kind"]):1137 entry_with_unknown = {**entry, "authority": "unsupported"}1138 with self.assertRaisesRegex(ProtocolError, r"unexpected=\['authority'\]"):1139 _workspace_entries([entry_with_unknown], "review_state.workspace")1140 1141 def test_review_state_accepts_complete_gitlink_entry(self) -> None:1142 state = copy.deepcopy(self.review_state)1143 workspace = [1144 {1145 "path": "src/example.py",1146 "kind": "gitlink",1147 "head": "c" * 40,1148 }1149 ]1150 combined = _content_fingerprint(state["base"], workspace)1151 state["fingerprint"] = combined1152 state["content_fingerprint"] = combined1153 state["workspace"] = workspace1154 state["unfiltered"]["workspace"] = workspace1155 state["components"]["api-contract"]["content_fingerprint"] = combined1156 state["components"]["api-contract"]["workspace"] = workspace1157 state["repository_fingerprint"] = _repository_fingerprint(1158 content_fingerprint=combined,1159 head=state["head"],1160 status_sha256=state["status_sha256"],1161 tracked_diff_sha256=state["tracked_diff_sha256"],1162 complete_diff_sha256=state["complete_diff_sha256"],1163 unfiltered_status_sha256=state["unfiltered"]["status_sha256"],1164 unfiltered_content_fingerprint=combined,1165 )1166 packet = copy.deepcopy(self.packet)1167 packet["ledger"]["round_fingerprint"] = combined1168 self._write_review_state(state, packet)1169 1170 summary = self._validate_packet()1171 1172 self.assertEqual(summary["combined_fingerprint"], combined)1173 1174 def test_review_state_artifact_rejects_unknown_workspace_fields(self) -> None:1175 state = copy.deepcopy(self.review_state)1176 state["workspace"][0]["authority"] = "unsupported"1177 self._write_review_state(state)1178 1179 with self.assertRaisesRegex(ProtocolError, r"unexpected=\['authority'\]"):1180 self._validate_packet()1181 1182 def test_review_state_requires_component_workspace(self) -> None:1183 state = copy.deepcopy(self.review_state)1184 del state["components"]["api-contract"]["workspace"]1185 self._write_review_state(state)1186 1187 with self.assertRaisesRegex(ProtocolError, "workspace must be an array"):1188 self._validate_packet()1189 1190 def test_review_state_recomputes_content_and_repository_fingerprints(self) -> None:1191 content_state = copy.deepcopy(self.review_state)1192 content_state["workspace"][0]["sha256"] = "e" * 641193 self._write_review_state(content_state)1194 with self.assertRaisesRegex(ProtocolError, "does not match its workspace"):1195 self._validate_packet()1196 1197 repository_state = copy.deepcopy(self.review_state)1198 repository_state["status_sha256"] = "e" * 641199 self._write_review_state(repository_state)1200 with self.assertRaisesRegex(ProtocolError, "repository_fingerprint does not match"):1201 self._validate_packet()1202 1203 def test_review_state_descriptor_rejects_copied_authority(self) -> None:1204 packet = copy.deepcopy(self.packet)1205 packet["review_state"]["content_fingerprint"] = "0" * 641206 self._write_packet(self.packet_path, packet)1207 1208 with self.assertRaisesRegex(ProtocolError, "must contain only evidence_id"):1209 self._validate_packet()1210 1211 def test_oversized_packet_requires_reason(self) -> None:1212 packet = copy.deepcopy(self.packet)1213 packet["task"]["original_requirement"] = "x" * (12 * 1024)1214 self._write_packet(self.packet_path, packet)1215 1216 with self.assertRaisesRegex(ProtocolError, "provide an overage reason"):1217 self._validate_packet()1218 1219 packet["packet_overage_reason"] = "The requirement is retained verbatim for review."1220 self._write_packet(self.packet_path, packet)1221 self._validate_packet()1222 1223 def test_exact_fingerprint_receipt_is_reusable(self) -> None:1224 receipt = self._receipt()1225 receipt["command"] = "make tests > /tmp/tests.log"1226 1227 validate_receipt_data(1228 receipt, self.combined, {"api-contract": self.component}, self.repository1229 )1230 1231 receipt["after"]["combined"] = "d" * 641232 with self.assertRaisesRegex(ProtocolError, "after fingerprints do not match"):1233 validate_receipt_data(1234 receipt, self.combined, {"api-contract": self.component}, self.repository1235 )1236 1237 def test_receipt_rejects_repository_fingerprint_drift(self) -> None:1238 receipt = self._receipt()1239 receipt["after"]["repository"] = "d" * 641240 1241 with self.assertRaisesRegex(ProtocolError, "after fingerprints do not match"):1242 validate_receipt_data(1243 receipt, self.combined, {"api-contract": self.component}, self.repository1244 )1245 1246 def test_commands_allow_redirection_but_reject_placeholder_tokens(self) -> None:1247 packet = copy.deepcopy(self.packet)1248 packet["verification"]["preflight_results"] = [1249 {1250 "command": "sort < /tmp/input.txt > /tmp/output.txt",1251 "result": "passed",1252 }1253 ]1254 self._write_packet(self.packet_path, packet)1255 self._validate_packet()1256 1257 output = self._output()1258 output["focused_probes"] = [1259 {"command": "git diff > /tmp/review.diff", "result": "captured"}1260 ]1261 self._write_json(self.output_path, output)1262 self._validate_output("requirements")1263 1264 receipt = self._receipt()1265 receipt["command"] = "make tests <focused probe>"1266 with self.assertRaisesRegex(ProtocolError, "placeholder token"):1267 validate_receipt_data(1268 receipt, self.combined, {"api-contract": self.component}, self.repository1269 )1270 1271 def test_receipt_rejects_boolean_exit_status(self) -> None:1272 receipt = self._receipt()1273 receipt["exit_status"] = False1274 1275 with self.assertRaisesRegex(ProtocolError, "exit_status 0"):1276 validate_receipt_data(1277 receipt, self.combined, {"api-contract": self.component}, self.repository1278 )1279 1280 def test_receipt_rejects_unknown_fields(self) -> None:1281 """Reject conflicting evidence outside the receipt schema."""1282 receipt = self._receipt()1283 receipt["exit_code"] = 11284 1285 with self.assertRaisesRegex(ProtocolError, "Verification receipt.*unexpected"):1286 validate_receipt_data(1287 receipt, self.combined, {"api-contract": self.component}, self.repository1288 )1289 1290 def test_packet_validates_every_credited_receipt(self) -> None:1291 self._write_json(self.receipt_path, self._receipt())1292 packet = copy.deepcopy(self.packet)1293 packet["verification"]["credited_receipts"] = [1294 {1295 "path": str(self.receipt_path),1296 "sha256": hashlib.sha256(self.receipt_path.read_bytes()).hexdigest(),1297 }1298 ]1299 self._write_packet(self.packet_path, packet)1300 1301 self._validate_packet()1302 1303 receipt = self._receipt()1304 receipt["exit_status"] = 11305 self._write_json(self.receipt_path, receipt)1306 packet["verification"]["credited_receipts"][0]["sha256"] = hashlib.sha256(1307 self.receipt_path.read_bytes()1308 ).hexdigest()1309 self._write_packet(self.packet_path, packet)1310 with self.assertRaisesRegex(ProtocolError, "exit_status 0"):1311 self._validate_packet()1312 1313 @unittest.skipIf(os.name == "nt", "Symlink creation requires platform privileges.")1314 def test_packet_rejects_aliased_credited_receipts(self) -> None:1315 self._write_json(self.receipt_path, self._receipt())1316 alias = self.root / "receipt-alias.json"1317 alias.symlink_to(self.receipt_path)1318 digest = hashlib.sha256(self.receipt_path.read_bytes()).hexdigest()1319 packet = copy.deepcopy(self.packet)1320 packet["verification"]["credited_receipts"] = [1321 {"path": str(self.receipt_path), "sha256": digest},1322 {"path": str(alias), "sha256": digest},1323 ]1324 self._write_packet(self.packet_path, packet)1325 1326 with self.assertRaisesRegex(ProtocolError, "Duplicate credited receipt file identity"):1327 self._validate_packet()1328 1329 def test_packet_rejects_copied_credited_receipts(self) -> None:1330 self._write_json(self.receipt_path, self._receipt())1331 copied_receipt = self.root / "copied-receipt.json"1332 copied_receipt.write_bytes(self.receipt_path.read_bytes())1333 digest = hashlib.sha256(self.receipt_path.read_bytes()).hexdigest()1334 packet = copy.deepcopy(self.packet)1335 packet["verification"]["credited_receipts"] = [1336 {"path": str(self.receipt_path), "sha256": digest},1337 {"path": str(copied_receipt), "sha256": digest},1338 ]1339 self._write_packet(self.packet_path, packet)1340 1341 with self.assertRaisesRegex(ProtocolError, "Duplicate credited receipt digest"):1342 self._validate_packet()1343 1344 def test_packet_rejects_multiple_receipts_for_one_command(self) -> None:1345 """Reject distinct receipt files that credit the same command."""1346 first_receipt = self._receipt()1347 self._write_json(self.receipt_path, first_receipt)1348 second_receipt = copy.deepcopy(first_receipt)1349 second_receipt["environment"] = "The same gate rerun in a fresh local process."1350 second_receipt_path = self.root / "second-receipt.json"1351 self._write_json(second_receipt_path, second_receipt)1352 packet = copy.deepcopy(self.packet)1353 packet["verification"]["credited_receipts"] = [1354 {1355 "path": str(self.receipt_path),1356 "sha256": hashlib.sha256(self.receipt_path.read_bytes()).hexdigest(),1357 },1358 {1359 "path": str(second_receipt_path),1360 "sha256": hashlib.sha256(second_receipt_path.read_bytes()).hexdigest(),1361 },1362 ]1363 self._write_packet(self.packet_path, packet)1364 1365 with self.assertRaisesRegex(ProtocolError, "Duplicate credited receipt command"):1366 self._validate_packet()1367 1368 def test_packet_rejects_receipt_for_unrelated_successful_command(self) -> None:1369 receipt = self._receipt()1370 receipt["command"] = "true"1371 self._write_json(self.receipt_path, receipt)1372 packet = copy.deepcopy(self.packet)1373 packet["verification"]["credited_receipts"] = [1374 {1375 "path": str(self.receipt_path),1376 "sha256": hashlib.sha256(self.receipt_path.read_bytes()).hexdigest(),1377 }1378 ]1379 self._write_packet(self.packet_path, packet)1380 1381 with self.assertRaisesRegex(ProtocolError, "must exactly match a packet preflight command"):1382 self._validate_packet()1383 1384 def test_packet_binds_credited_receipt_digest(self) -> None:1385 receipt = self._receipt()1386 self._write_json(self.receipt_path, receipt)1387 packet = copy.deepcopy(self.packet)1388 packet["verification"]["credited_receipts"] = [1389 {1390 "path": str(self.receipt_path),1391 "sha256": hashlib.sha256(self.receipt_path.read_bytes()).hexdigest(),1392 }1393 ]1394 self._write_packet(self.packet_path, packet)1395 self._validate_packet()1396 1397 receipt["command"] = "make lint"1398 self._write_json(self.receipt_path, receipt)1399 with self.assertRaisesRegex(ProtocolError, r"credited_receipts\[0\] digest mismatch"):1400 self._validate_packet()1401 1402 def test_receipt_cli_rejects_unindexed_replacement(self) -> None:1403 receipt = self._receipt()1404 self._write_json(self.receipt_path, receipt)1405 packet = copy.deepcopy(self.packet)1406 packet["verification"]["credited_receipts"] = [1407 {1408 "path": str(self.receipt_path),1409 "sha256": hashlib.sha256(self.receipt_path.read_bytes()).hexdigest(),1410 }1411 ]1412 self._write_packet(self.packet_path, packet)1413 1414 unindexed = self.root / "unindexed-receipt.json"1415 receipt["command"] = "command-that-never-ran"1416 self._write_json(unindexed, receipt)1417 completed = subprocess.run(1418 (1419 sys.executable,1420 str(Path(__file__).with_name("review_protocol.py")),1421 "receipt",1422 "--packet",1423 str(self.packet_path),1424 "--receipt",1425 str(unindexed),1426 "--task-id",1427 "task-123",1428 "--ledger",1429 str(self.ledger_path),1430 ),1431 capture_output=True,1432 text=True,1433 )1434 1435 self.assertEqual(completed.returncode, 2)1436 self.assertIn("receipt path is not indexed", completed.stderr)1437 1438 def test_receipt_validation_rechecks_the_indexed_file(self) -> None:1439 """Reject a receipt replaced after packet validation."""1440 receipt = self._receipt()1441 self._write_json(self.receipt_path, receipt)1442 packet = copy.deepcopy(self.packet)1443 packet["verification"]["credited_receipts"] = [1444 {1445 "path": str(self.receipt_path),1446 "sha256": hashlib.sha256(self.receipt_path.read_bytes()).hexdigest(),1447 }1448 ]1449 self._write_packet(self.packet_path, packet)1450 original_validate_packet = validate_packet1451 1452 def validate_then_replace_receipt(*args: Any, **kwargs: Any) -> dict[str, Any]:1453 summary = original_validate_packet(*args, **kwargs)1454 receipt["environment"] = "A replacement environment after packet validation."1455 self._write_json(self.receipt_path, receipt)1456 return summary1457 1458 with (1459 mock.patch(1460 "review_protocol.validate_packet", side_effect=validate_then_replace_receipt1461 ),1462 self.assertRaisesRegex(ProtocolError, "Receipt changed"),1463 ):1464 _validate_credited_receipt(1465 self.packet_path,1466 self.receipt_path,1467 "task-123",1468 self.ledger_path,1469 )1470 1471 def test_clean_output_must_match_assignment_and_fingerprint(self) -> None:1472 output = self._output()1473 self._write_json(self.output_path, output)1474 1475 summary = self._validate_output("requirements")1476 self.assertEqual(summary["verdict"], "clean")1477 1478 output["checked_inventory_ids"] = []1479 self._write_json(self.output_path, output)1480 with self.assertRaisesRegex(ProtocolError, "inventory accounting differs"):1481 self._validate_output("requirements")1482 1483 def test_reviewer_output_rejects_unknown_fields(self) -> None:1484 """Reject reviewer conclusions outside the documented schema."""1485 output = self._output()1486 output["issues"] = [{"title": "Ignored finding"}]1487 self._write_json(self.output_path, output)1488 1489 with self.assertRaisesRegex(ProtocolError, "Reviewer output.*unexpected"):1490 self._validate_output("requirements")1491 1492 def test_finding_and_root_evidence_reject_unknown_fields(self) -> None:1493 """Reject finding data that the protocol would otherwise ignore."""1494 for field_path in ("finding", "root_evidence"):1495 with self.subTest(field_path=field_path):1496 output = self._output()1497 output["verdict"] = "findings require fixes"1498 finding = self._finding("ROOT_EXISTING")1499 if field_path == "finding":1500 finding["alternative_root"] = "ROOT_CLOSED"1501 else:1502 finding["root_cause_evidence"]["note"] = "Ignored evidence metadata."1503 output["findings"] = [finding]1504 self._write_json(self.output_path, output)1505 1506 with self.assertRaisesRegex(ProtocolError, "unexpected"):1507 self._validate_output("requirements")1508 1509 def test_reviewer_output_is_bound_to_the_exact_packet(self) -> None:1510 output = self._output()1511 self._write_json(self.output_path, output)1512 packet = copy.deepcopy(self.packet)1513 packet["task"]["original_requirement"] = "A changed review requirement."1514 self._write_packet(self.packet_path, packet)1515 1516 with self.assertRaisesRegex(ProtocolError, "packet digest"):1517 self._validate_output("requirements")1518 1519 def test_reviewer_output_rechecks_current_ledger_after_packet_validation(self) -> None:1520 """Reject a current ledger replaced after packet validation."""1521 output = self._output()1522 self._write_json(self.output_path, output)1523 original_validate_packet = validate_packet1524 1525 def validate_then_replace_ledger(*args: Any, **kwargs: Any) -> dict[str, Any]:1526 summary = original_validate_packet(*args, **kwargs)1527 replacement = copy.deepcopy(self.packet["ledger"])1528 replacement["remaining_budget"] = 991529 self._write_json(self.ledger_path, replacement)1530 return summary1531 1532 with (1533 mock.patch("review_protocol.validate_packet", side_effect=validate_then_replace_ledger),1534 self.assertRaisesRegex(ProtocolError, "Current ledger changed"),1535 ):1536 self._validate_output("requirements")1537 1538 def test_reviewer_output_rechecks_prior_ledger_after_packet_validation(self) -> None:1539 """Reject a prior ledger replaced after packet validation."""1540 prior_path = self.root / "prior-ledger.json"1541 self._write_json(prior_path, self.packet["ledger"])1542 prior_digest = hashlib.sha256(prior_path.read_bytes()).hexdigest()1543 packet = copy.deepcopy(self.packet)1544 packet["ledger"]["current_round"] = 21545 packet["ledger"]["remaining_budget"] = 41546 self._write_packet(self.packet_path, packet)1547 self._write_json(self.output_path, self._output())1548 original_validate_packet = validate_packet1549 1550 def validate_then_replace_prior(*args: Any, **kwargs: Any) -> dict[str, Any]:1551 summary = original_validate_packet(*args, **kwargs)1552 replacement = copy.deepcopy(self.packet["ledger"])1553 replacement["root_causes"] = []1554 self._write_json(prior_path, replacement)1555 return summary1556 1557 with (1558 mock.patch("review_protocol.validate_packet", side_effect=validate_then_replace_prior),1559 self.assertRaisesRegex(ProtocolError, "Prior ledger changed"),1560 ):1561 validate_reviewer_output(1562 self.packet_path,1563 "requirements",1564 self.output_path,1565 "task-123",1566 self.ledger_path,1567 prior_path,1568 prior_digest,1569 )1570 1571 def test_unknown_root_must_be_new_proposal_with_evidence(self) -> None:1572 output = self._output()1573 output["verdict"] = "findings require fixes"1574 output["findings"] = [self._finding("renamed-root")]1575 self._write_json(self.output_path, output)1576 1577 with self.assertRaisesRegex(ProtocolError, "canonical root ID or propose NEW"):1578 self._validate_output("requirements")1579 1580 output["findings"][0]["root_cause_id"] = "NEW:new-boundary"1581 output["findings"][0]["root_cause_evidence"]["new_inventory_ids"] = ["INV-1"]1582 self._write_json(self.output_path, output)1583 with self.assertRaisesRegex(ProtocolError, "cannot reuse canonical inventory"):1584 self._validate_output("requirements")1585 1586 output["findings"][0]["root_cause_evidence"]["new_inventory_ids"] = []1587 output["findings"][0]["root_cause_evidence"]["new_contract_evidence_ids"] = ["E-NEW"]1588 self._write_json(self.output_path, output)1589 self._validate_output("requirements")1590 1591 def test_copied_evidence_does_not_support_a_new_root(self) -> None:1592 copied_evidence = self.root / "copied-root-evidence.txt"1593 copied_evidence.write_bytes(self.root_evidence.read_bytes())1594 packet = copy.deepcopy(self.packet)1595 packet["evidence_artifacts"].append(1596 {1597 "id": "E-COPY",1598 "path": str(copied_evidence),1599 "sha256": hashlib.sha256(copied_evidence.read_bytes()).hexdigest(),1600 "role": "supporting",1601 "purpose": "Byte copy of existing root-cause evidence.",1602 }1603 )1604 self._write_packet(self.packet_path, packet)1605 output = self._output()1606 output["verdict"] = "findings require fixes"1607 finding = self._finding("NEW:copied-evidence")1608 finding["root_cause_evidence"]["new_contract_evidence_ids"] = ["E-COPY"]1609 output["findings"] = [finding]1610 self._write_json(self.output_path, output)1611 1612 with self.assertRaisesRegex(ProtocolError, "without content-new evidence"):1613 self._validate_output("requirements")1614 1615 def test_copied_inventory_does_not_reopen_a_closed_root(self) -> None:1616 """Reject a renamed copy of inventory as closed-root evidence."""1617 prior_path = self.root / "prior-ledger.json"1618 self._write_json(prior_path, self.packet["ledger"])1619 prior_digest = hashlib.sha256(prior_path.read_bytes()).hexdigest()1620 packet = copy.deepcopy(self.packet)1621 copied_inventory = copy.deepcopy(packet["inventory"][1])1622 copied_inventory["id"] = "INV-COPY"1623 packet["inventory"].append(copied_inventory)1624 packet["ledger"]["inventory_sha256"]["INV-COPY"] = _inventory_digest(copied_inventory)1625 closed_root = next(1626 root for root in packet["ledger"]["root_causes"] if root["id"] == "ROOT_CLOSED"1627 )1628 closed_root["status"] = "open"1629 closed_root["inventory_ids"].append("INV-COPY")1630 packet["reviewer_assignments"][1]["inventory_ids"].append("INV-COPY")1631 packet["ledger"]["current_round"] = 21632 packet["ledger"]["remaining_budget"] = 41633 self._write_packet(self.packet_path, packet)1634 1635 with self.assertRaisesRegex(ProtocolError, "without content-new evidence"):1636 validate_packet(1637 self.packet_path,1638 "task-123",1639 self.ledger_path,1640 prior_path,1641 prior_digest,1642 )1643 1644 def test_distinct_new_roots_cannot_share_one_evidence_digest(self) -> None:1645 """Require distinct new roots to own distinct evidence content."""1646 output = self._output()1647 output["verdict"] = "findings require fixes"1648 first = self._finding("NEW:first-root")1649 first["root_cause_evidence"]["new_contract_evidence_ids"] = ["E-NEW"]1650 second = self._finding("NEW:second-root")1651 second["root_cause_evidence"]["new_contract_evidence_ids"] = ["E-NEW"]1652 output["findings"] = [first, second]1653 self._write_json(self.output_path, output)1654 1655 with self.assertRaisesRegex(ProtocolError, "reuses evidence owned by proposed root"):1656 self._validate_output("requirements")1657 1658 def test_closed_root_requires_new_evidence(self) -> None:1659 output = self._output()1660 output["verdict"] = "findings require fixes"1661 output["checked_inventory_ids"] = ["INV-2"]1662 output["findings"] = [self._finding("ROOT_CLOSED")]1663 self._write_json(self.output_path, output)1664 1665 with self.assertRaisesRegex(ProtocolError, "reopens closed root"):1666 self._validate_output("lifecycle")1667 1668 output["findings"][0]["root_cause_evidence"]["new_inventory_ids"] = ["INV-2"]1669 self._write_json(self.output_path, output)1670 self._validate_output("lifecycle")1671 1672 output["findings"][0]["root_cause_evidence"]["new_inventory_ids"] = ["INV-1"]1673 self._write_json(self.output_path, output)1674 with self.assertRaisesRegex(ProtocolError, "owned by another canonical root"):1675 self._validate_output("lifecycle")1676 1677 output["findings"][0]["root_cause_evidence"]["new_inventory_ids"] = []1678 output["findings"][0]["root_cause_evidence"]["new_contract_evidence_ids"] = [1679 "DOES-NOT-EXIST"1680 ]1681 self._write_json(self.output_path, output)1682 with self.assertRaisesRegex(ProtocolError, "unindexed root evidence"):1683 self._validate_output("lifecycle")1684 1685 output["findings"][0]["root_cause_evidence"]["new_contract_evidence_ids"] = []1686 output["findings"][0]["root_cause_evidence"]["new_inventory_ids"] = ["INV-MISSING"]1687 self._write_json(self.output_path, output)1688 with self.assertRaisesRegex(ProtocolError, "unindexed root evidence"):1689 self._validate_output("lifecycle")1690 1691 output["findings"][0]["root_cause_evidence"]["new_inventory_ids"] = []1692 output["findings"][0]["root_cause_evidence"]["new_contract_evidence_ids"] = ["E-DIFF"]1693 self._write_json(self.output_path, output)1694 with self.assertRaisesRegex(ProtocolError, "must be new in the current ledger round"):1695 self._validate_output("lifecycle")1696 1697 output["findings"][0]["root_cause_evidence"]["new_contract_evidence_ids"] = ["E-ROOT"]1698 self._write_json(self.output_path, output)1699 self._validate_output("lifecycle")1700 1701 def test_sibling_scan_requires_known_root_and_inventory(self) -> None:1702 output = self._output()1703 output["sibling_scenario_scan"] = [1704 {1705 "root_cause_id": "RENAMED_ROOT",1706 "inventory_ids": ["INV-1"],1707 "result": "No sibling failure.",1708 }1709 ]1710 self._write_json(self.output_path, output)1711 1712 with self.assertRaisesRegex(ProtocolError, "must reference a canonical or proposed root"):1713 self._validate_output("requirements")1714 1715 output["sibling_scenario_scan"][0]["root_cause_id"] = "ROOT_EXISTING"1716 output["sibling_scenario_scan"][0]["inventory_ids"] = ["INV-MISSING"]1717 self._write_json(self.output_path, output)1718 with self.assertRaisesRegex(ProtocolError, "unknown inventory IDs"):1719 self._validate_output("requirements")1720 1721 def test_newly_promoted_root_uses_current_round_ownership(self) -> None:1722 prior_path = self.root / "prior-ledger.json"1723 prior = copy.deepcopy(self.packet["ledger"])1724 prior["root_causes"] = [prior["root_causes"][0]]1725 prior["contract_evidence_sha256"] = {"E-DIFF": prior["contract_evidence_sha256"]["E-DIFF"]}1726 prior["inventory_sha256"] = {"INV-1": prior["inventory_sha256"]["INV-1"]}1727 self._write_json(prior_path, prior)1728 prior_digest = hashlib.sha256(prior_path.read_bytes()).hexdigest()1729 1730 packet = copy.deepcopy(self.packet)1731 packet["ledger"]["current_round"] = 21732 packet["ledger"]["remaining_budget"] = 41733 self._write_packet(self.packet_path, packet)1734 output = self._output()1735 output["verdict"] = "findings require fixes"1736 output["checked_inventory_ids"] = ["INV-2"]1737 finding = self._finding("ROOT_CLOSED")1738 finding["root_cause_evidence"]["new_contract_evidence_ids"] = ["E-ROOT"]1739 output["findings"] = [finding]1740 self._write_json(self.output_path, output)1741 1742 validate_reviewer_output(1743 self.packet_path,1744 "lifecycle",1745 self.output_path,1746 "task-123",1747 self.ledger_path,1748 prior_path,1749 prior_digest,1750 )1751 1752 output["findings"][0]["root_cause_evidence"]["new_contract_evidence_ids"] = ["E-NEW"]1753 self._write_json(self.output_path, output)1754 with self.assertRaisesRegex(ProtocolError, "must be new in the current ledger round"):1755 validate_reviewer_output(1756 self.packet_path,1757 "lifecycle",1758 self.output_path,1759 "task-123",1760 self.ledger_path,1761 prior_path,1762 prior_digest,1763 )1764 1765 def test_reviewer_output_rejects_boolean_inspection_count(self) -> None:1766 output = self._output()1767 output["inspection_call_count"] = False1768 self._write_json(self.output_path, output)1769 1770 with self.assertRaisesRegex(ProtocolError, "nonnegative integer"):1771 self._validate_output("requirements")1772 1773 def test_cli_reports_protocol_errors_without_traceback(self) -> None:1774 invalid = copy.deepcopy(self.packet)1775 invalid["schema_version"] = 21776 self._write_packet(self.packet_path, invalid)1777 1778 completed = subprocess.run(1779 (1780 sys.executable,1781 str(Path(__file__).with_name("review_protocol.py")),1782 "packet",1783 "--packet",1784 str(self.packet_path),1785 "--task-id",1786 "task-123",1787 "--ledger",1788 str(self.ledger_path),1789 ),1790 capture_output=True,1791 text=True,1792 )1793 1794 self.assertEqual(completed.returncode, 2)1795 self.assertIn("schema_version must be integer 1", completed.stderr)1796 self.assertNotIn("Traceback", completed.stderr)1797 1798 def test_cli_reports_invalid_artifact_paths_without_traceback(self) -> None:1799 invalid = copy.deepcopy(self.packet)1800 invalid["evidence_artifacts"][0]["path"] = "/tmp/invalid\0path"1801 self._write_packet(self.packet_path, invalid)1802 1803 completed = subprocess.run(1804 (1805 sys.executable,1806 str(Path(__file__).with_name("review_protocol.py")),1807 "packet",1808 "--packet",1809 str(self.packet_path),1810 "--task-id",1811 "task-123",1812 "--ledger",1813 str(self.ledger_path),1814 ),1815 capture_output=True,1816 text=True,1817 )1818 1819 self.assertEqual(completed.returncode, 2)1820 self.assertIn("Cannot read evidence artifact E-DIFF.path", completed.stderr)1821 self.assertNotIn("Traceback", completed.stderr)1822 1823 1824def re_escape(value: str) -> str:1825 """Escape a literal string for assertRaisesRegex without importing re in each test."""1826 import re1827 1828 return re.escape(value)1829 1830 1831if __name__ == "__main__":1832 unittest.main()1833