Make ContractClock persistence atomic

This commit is contained in:
2026-07-18 04:31:55 -03:00
parent 3a3df9d577
commit e03a616ecf
5 changed files with 78 additions and 35 deletions

View File

@@ -82,21 +82,30 @@ def test_acceptance_success_reload_failure_and_receipt_persistence():
saved = {}
receipts = []
def fake_persist_contract(tenant, contract_id, title, payload, receipt_id):
def fake_persist_contract_with_receipt(
tenant,
contract_id,
title,
payload,
receipt_id,
skill_name,
status,
):
assert tenant == "user:123"
saved[(tenant, contract_id)] = {
"payload": payload,
"receipt_id": receipt_id,
"updated_at": "2026-01-01T00:00:00+00:00",
}
receipts.append((tenant, receipt_id, skill_name, contract_id, status))
def fake_load_contract(tenant, contract_id):
return saved.get((tenant, contract_id))
def fake_persist_receipt(tenant, receipt_id, skill_name, contract_id, payload, status):
receipts.append((tenant, receipt_id, skill_name, contract_id, status))
with patch("agent.persist_contract", side_effect=fake_persist_contract), patch("agent.load_contract", side_effect=fake_load_contract), patch("agent.persist_receipt", side_effect=fake_persist_receipt):
with patch(
"agent.persist_contract_with_receipt",
side_effect=fake_persist_contract_with_receipt,
), patch("agent.load_contract", side_effect=fake_load_contract):
result = asyncio.run(agent.analyze_contract(ctx(), contract_id="studio-contract-v1", text=SUCCESS_TEXT, title="Studio Renewal Agreement"))
assert result.ok is True
assert result.persisted is True
@@ -124,7 +133,7 @@ def test_acceptance_success_reload_failure_and_receipt_persistence():
def test_browser_upload_bridge_bounds_and_reuses_workflow():
agent = ContractClockStudioV1()
encoded = base64.b64encode(SUCCESS_TEXT.encode("utf-8")).decode("ascii")
with patch("agent.persist_contract", return_value=None), patch("agent.persist_receipt", return_value=None):
with patch("agent.persist_contract_with_receipt", return_value=None):
result = asyncio.run(
agent.analyze_contract_browser_upload(
ctx(),
@@ -146,6 +155,31 @@ def test_browser_upload_bridge_bounds_and_reuses_workflow():
assert bad.ok is False and bad.code == "invalid_base64"
def test_tools_return_structured_auth_failure_for_missing_principal():
anonymous = LocalRunContext(
auth=PlatformUserAuth(sub=""),
caller="tester",
)
agent = ContractClockStudioV1()
analyzed = asyncio.run(
agent.analyze_contract(
anonymous,
contract_id="studio-contract-v1",
text=SUCCESS_TEXT,
title="Studio Renewal Agreement",
)
)
loaded = asyncio.run(
agent.get_contract_deadlines(anonymous, contract_id="studio-contract-v1")
)
listed = asyncio.run(agent.list_contracts(anonymous))
assert analyzed.ok is False and analyzed.code == "auth_required"
assert loaded.ok is False and loaded.code == "auth_required"
assert listed.ok is False and listed.code == "auth_required"
def test_receipt_id_is_deterministic_without_secrets():
payload = {"contract_id": "studio-contract-v1", "deadlines": [{"kind": "renewal", "date": "2026-12-31"}]}
receipt = production_receipt_id("user:123", "analyze_contract", payload)