import hashlib import json import pytest from tests.conftest import ( create_test_domain, create_test_repo, create_test_topic, create_test_workplan, ) def _digest(value): return hashlib.sha256( json.dumps( value, sort_keys=True, separators=(",", ":"), ensure_ascii=True ).encode() ).hexdigest() def _contract(artifact_digest="a" * 64): return { "schema_version": "review-contract/v1", "contract_key": "TEST-REVIEW-1", "subject": {"kind": "task", "id": "TEST-WP-1-T01"}, "allowed_dispositions": ["approve", "request_changes"], "artifacts": { "docs/runbook.md": {"algorithm": "sha256", "digest": artifact_digest} }, "owners": [ { "id": "railiance-infra", "artifact_ids": ["docs/runbook.md"], "assertions": [ { "id": "safe-boundary", "statement": "No live action is authorized.", } ], "check_ids": ["runbook-lint"], } ], "gates": [ { "id": "TEST-WP-1-T01", "policy": "all_required", "owners": ["railiance-infra"], } ], } def _source(repo, path, revision): return {"repo": repo, "path": path, "revision": revision} def _approval(contract, revision="2" * 40): return { "owner_id": "railiance-infra", "actor": "operator:test", "disposition": "approve", "contract_digest": _digest(contract), "artifact_hashes": { "docs/runbook.md": contract["artifacts"]["docs/runbook.md"]["digest"] }, "assertion_ids": ["safe-boundary"], "checks": [{"id": "runbook-lint", "passed": True, "read_only": True}], "source": _source("railiance-infra", "reviews/receipt.json", revision), } @pytest.mark.asyncio async def test_review_round_trip_and_idempotency(client): contract = _contract() projected = await client.post( "/review-contracts/projections", json={ "contract": contract, "source": _source("test-owner", "interfaces/review.json", "1" * 40), }, ) assert projected.status_code == 201, projected.text assert projected.json()["contract_digest"] == _digest(contract) missing = await client.get("/review-contracts/TEST-REVIEW-1/aggregate") assert missing.json()["satisfied"] is False assert missing.json()["authorizes_execution"] is False assert missing.json()["owners"][0]["status"] == "missing" approval = _approval(contract) first = await client.post("/review-contracts/TEST-REVIEW-1/receipts", json=approval) second = await client.post( "/review-contracts/TEST-REVIEW-1/receipts", json=approval ) assert first.status_code == second.status_code == 201 assert first.json()["id"] == second.json()["id"] assert first.json()["submitted_at"].endswith("+00:00") or first.json()[ "submitted_at" ].endswith("Z") result = (await client.get("/review-contracts/TEST-REVIEW-1/aggregate")).json() assert result["satisfied"] is True assert result["owners"][0]["status"] == "approved" @pytest.mark.asyncio async def test_receipt_rejects_wrong_owner_and_artifact_drift(client): contract = _contract() await client.post( "/review-contracts/projections", json={ "contract": contract, "source": _source("test-owner", "interfaces/review.json", "1" * 40), }, ) wrong_owner = _approval(contract) wrong_owner["owner_id"] = "somebody-else" assert ( await client.post("/review-contracts/TEST-REVIEW-1/receipts", json=wrong_owner) ).status_code == 422 drift = _approval(contract) drift["artifact_hashes"]["docs/runbook.md"] = "f" * 64 assert ( await client.post("/review-contracts/TEST-REVIEW-1/receipts", json=drift) ).status_code == 422 @pytest.mark.asyncio async def test_new_contract_revision_marks_prior_receipt_stale(client): old = _contract() await client.post( "/review-contracts/projections", json={ "contract": old, "source": _source("test-owner", "interfaces/review.json", "1" * 40), }, ) await client.post("/review-contracts/TEST-REVIEW-1/receipts", json=_approval(old)) new = _contract("b" * 64) replaced = await client.post( "/review-contracts/projections", json={ "contract": new, "source": _source("test-owner", "interfaces/review.json", "3" * 40), }, ) assert replaced.status_code == 201, replaced.text state = (await client.get("/review-contracts/TEST-REVIEW-1/aggregate")).json() assert state["owners"][0]["status"] == "stale" assert state["satisfied"] is False @pytest.mark.asyncio async def test_request_changes_supersedes_approval(client): contract = _contract() await client.post( "/review-contracts/projections", json={ "contract": contract, "source": _source("test-owner", "interfaces/review.json", "1" * 40), }, ) await client.post( "/review-contracts/TEST-REVIEW-1/receipts", json=_approval(contract) ) change = { "owner_id": "railiance-infra", "actor": "operator:test", "disposition": "request_changes", "contract_digest": _digest(contract), "note": "Clarify the recovery owner.", "source": _source("railiance-infra", "reviews/request-changes.json", "4" * 40), } assert ( await client.post("/review-contracts/TEST-REVIEW-1/receipts", json=change) ).status_code == 201 state = (await client.get("/review-contracts/TEST-REVIEW-1/aggregate")).json() assert state["owners"][0]["status"] == "request_changes" assert state["satisfied"] is False @pytest.mark.asyncio async def test_required_review_blocks_legacy_decision_resolution(client): domain = await create_test_domain(client) topic = await create_test_topic(client, domain_slug=domain["slug"]) repo = await create_test_repo(client, domain_slug=domain["slug"]) workplan = await create_test_workplan(client, repo["id"], topic["id"]) decision = ( await client.post( "/decisions/", json={"title": "Run attended procedure", "workplan_id": workplan["id"]}, ) ).json() contract = _contract() projected = await client.post( "/review-contracts/projections", json={ "contract": contract, "source": _source("test-owner", "interfaces/review.json", "1" * 40), "decision_id": decision["id"], "required_for_decision": True, }, ) assert projected.status_code == 201, projected.text blocked = await client.post( f"/decisions/{decision['id']}/resolve", json={ "rationale": "Proceed", "decided_by": "operator:test", "write_log": False, }, ) assert blocked.status_code == 409 await client.post( "/review-contracts/TEST-REVIEW-1/receipts", json=_approval(contract) ) resolved = await client.post( f"/decisions/{decision['id']}/resolve", json={ "rationale": "Reviewed", "decided_by": "operator:test", "write_log": False, }, ) assert resolved.status_code == 200 @pytest.mark.asyncio async def test_legacy_wp0024_contract_adapter(client): legacy = { "interface": "railiance.owner-review", "version": 1, "workplan_id": "RAILIANCE-WP-0024", "decisions": ["approve", "request-changes"], "task_owners": {"RAILIANCE-WP-0024-T03": ["railiance-infra"]}, "owners": { "railiance-infra": { "artifacts": ["docs/runbook.md"], "assertions": ["No live execution is authorized."], "checks": ["node-reboot-preflight"], } }, "artifact_sha256": {"docs/runbook.md": "a" * 64}, } result = await client.post( "/review-contracts/projections", json={ "contract": legacy, "source": _source( "railiance-platform", "interfaces/RAILIANCE-WP-0024-owner-reviews.json", "5" * 40, ), }, ) assert result.status_code == 201, result.text body = result.json() assert body["contract_key"] == "RAILIANCE-WP-0024" assert body["contract_digest"] == _digest(legacy) assert body["document"]["owners"][0]["assertions"][0]["id"].startswith("assertion:")