"""R2 — provenance manifest acceptance checks. Acceptance predicate (roadmap R2): a manifest is present + schema-valid after a run, carries a non-empty seed, and >=1 persona fingerprint per participating node. The fingerprint parity constant below is locked identically in hh/src/persona.rs::tests::fingerprint_of_known_vector. """ import json import pytest from cmd_chat.sor.provenance import ( Node, RunManifest, node_fingerprint, validate_manifest, write_manifest, ) # Known 32-byte pubkey (bytes 0..32) -> base64 -> fingerprint. Same vector as # the Rust persona test. KNOWN_PUB_B64 = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=" KNOWN_FINGERPRINT = "630dcd29" def _fixture_manifest(root): return RunManifest( run_id="20260719T000000Z", sor_seed=123456789, topology="1-house", selector="static", churn_schedule_id="fixture-churn-0", nodes=[ Node(role="host", persona_pub_b64=KNOWN_PUB_B64, engine="docker"), Node(role="hop", persona_pub_b64="QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVphYmNk", engine="docker"), ], engine_kind="docker", engine_image_digest="sha256:deadbeef", worktree_root=root, ) def test_fingerprint_parity_with_rust(): assert node_fingerprint(KNOWN_PUB_B64) == KNOWN_FINGERPRINT def test_manifest_written_and_schema_valid(tmp_path): run_dir = tmp_path / "sor-runs" / "20260719T000000Z" doc = write_manifest(run_dir, _fixture_manifest(tmp_path)) # present on disk path = run_dir / "manifest.json" assert path.is_file() on_disk = json.loads(path.read_text()) assert on_disk == doc # schema-valid, non-empty seed, >=1 fingerprint per node (the R2 predicate) validate_manifest(on_disk) assert on_disk["sor_seed"] == 123456789 assert len(on_disk["nodes"]) >= 1 for n in on_disk["nodes"]: assert n["fingerprint"] assert on_disk["nodes"][0]["fingerprint"] == KNOWN_FINGERPRINT def test_manifest_is_immutable(tmp_path): run_dir = tmp_path / "run" write_manifest(run_dir, _fixture_manifest(tmp_path)) with pytest.raises(FileExistsError): write_manifest(run_dir, _fixture_manifest(tmp_path)) def test_containment_rejects_local_engine(tmp_path): m = _fixture_manifest(tmp_path) m.engine_kind = "local" with pytest.raises(ValueError): write_manifest(tmp_path / "run2", m) def test_containment_rejects_local_node_engine(tmp_path): m = _fixture_manifest(tmp_path) m.nodes[1].engine = "local" with pytest.raises(ValueError): write_manifest(tmp_path / "run3", m) def test_invalid_pubkey_rejected(): with pytest.raises(ValueError): node_fingerprint("!!! not base64 !!!") def test_validator_catches_empty_nodes(): with pytest.raises(ValueError): validate_manifest( { "schema_version": "sor-manifest/1", "run_id": "x", "created_utc": "t", "sor_seed": 1, "topology": "1-house", "selector": "static", "churn_schedule_id": "c", "nodes": [], "engine": {"kind": "docker", "image_digest": None}, "git": {}, "deps": {}, "timestamps": {"start_utc": "t", "stop_utc": None}, "events": {"path": "events.jsonl", "sha256": None}, } )