feat(review): add multi-owner contracts and receipts
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a023c0-a0a3-7c03-b395-5a0d2757214d
This commit is contained in:
parent
52aefe39e4
commit
598f6418e7
13 changed files with 1401 additions and 9 deletions
264
tests/test_review_contracts.py
Normal file
264
tests/test_review_contracts.py
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
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:")
|
||||
|
|
@ -185,7 +185,7 @@ def test_every_work_record_foreign_key_cascades_on_update():
|
|||
for foreign_key in table.foreign_keys
|
||||
if foreign_key.target_fullname in {"workplans.id", "tasks.id"}
|
||||
]
|
||||
assert len(references) == 20
|
||||
assert len(references) == 22
|
||||
assert all(foreign_key.onupdate == "CASCADE" for foreign_key in references)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue