Add recipient-scoped review discovery and settle initial lifecycle contracts

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a10780-6d41-7952-9718-dfbc1f41236f
This commit is contained in:
tegwick 2026-10-05 01:03:05 +02:00
parent dfcef879d0
commit da2c5a720f
9 changed files with 224 additions and 10 deletions

View file

@ -8,12 +8,14 @@ import pytest
from informed_decision.disposition import Actor, ActorKind, Verb, DispositionRefused
from informed_decision.lifecycle import Attribution, LifecycleRepository
from informed_decision.lifecycle_service import Caller, LifecycleService, LifecycleError
from informed_decision.policy import build_request
from informed_decision.provenance import Claim, Route
from informed_decision.records import memo_from
from informed_decision.store import Conflict, Store
from test_intake import envelope
from test_intake_service import setup, request
from test_policy_client import policy
from test_browser_auth import signing_key
@pytest.fixture
@ -376,3 +378,79 @@ def test_retained_handoff_api_is_revision_scoped_and_reauthorizes(handoff):
assert request(intake, receipt['envelope'])[0] == 409
app.policy.transport.change = lambda data: data.update(effect='deny')
assert call(api, path, query='version=1')[:2] == (403, {'error': 'policy_denied'})
def test_overview_uses_retained_intake_context_and_fails_closed_per_row(handoff):
from informed_decision.review import ReviewController
from informed_decision.approval_client import ApprovalEngineError
from test_policy_client import human
app, actor, memo, options, intake, calls = handoff
receipt = prepare(handoff)
assert request(intake, receipt['envelope'])[0] == 201
store = app.repository.store
stored = store.memo(receipt['review_memo_id'])
session = replace(human(), subject=stored.binding.principal.id)
class Engine:
failure = False
def get_approval(self, approval_id):
if self.failure:
raise ApprovalEngineError(503, 'unavailable')
return {'status': 'requested', 'binding': {'digest': stored.approval_binding_digest,
'actor': 'informed-decision', 'human_control': True}}
engine = Engine()
controller = ReviewController(store, policy(), lambda session: engine, intake_attributes=True)
[row] = controller.overview(session)
assert row.question == memo.question
sent = controller.policy.transport.calls[-1][3]
assert sent['action'] == 'list'
assert sent['context']['review_recipient'] == session.subject
assert sent['context']['intake_submitter'] == 'state-hub'
assert 'review_recipient' not in build_request(
session, stored, 'read', 'v1', intake_context=store.intake_context(stored))['context']
assert store.evidence() == []
engine.failure = True
[row] = controller.overview(session)
assert row.group == 'unavailable' and row.question is None
assert len(controller.policy.transport.calls) == 1
def test_real_discovery_policy_lists_only_recipient_and_keeps_fresh_read(handoff, tmp_path, signing_key):
import os
from pathlib import Path
from informed_decision.policy import build_request
from informed_decision.review import ReviewController, ReviewError
from review_components import flex_component
from test_policy_client import human
binary, source = os.environ.get('INFD_FLEX_AUTH_BINARY'), os.environ.get('INFD_FLEX_AUTH_SOURCE')
if not binary or not source:
pytest.skip('actual owner discovery policy required')
fixture = Path(source) / 'examples/informed-decision-lifecycle-review'
if json.loads((fixture / 'pin.json').read_text())['version'] != 'v13':
pytest.skip('owner discovery v13 required')
app, actor, memo, options, intake, calls = handoff
options['memo_class'] = 'decision-record'
receipt = prepare(handoff)
# Admit this class in the synthetic intake registry used by the fixture.
registry = json.loads(intake.submitters_path.read_text())
for value in registry.values():
value['memo_classes'].append('decision-record')
intake.submitters_path.write_text(json.dumps(registry))
assert request(intake, receipt['envelope'])[0] == 201
store = app.repository.store
stored = store.memo(receipt['review_memo_id'])
session = replace(human(), subject=stored.binding.principal.id,
assurance={**human().assurance, 'at': int(time.time()) - 3600})
class Engine:
def get_approval(self, approval_id):
return {'status': 'requested', 'binding': {'digest': stored.approval_binding_digest,
'actor': 'informed-decision', 'human_control': True}}
with flex_component(binary, tmp_path, signing_key, fixture=fixture) as (pdp, calls):
controller = ReviewController(store, pdp, lambda session: Engine(), intake_attributes=True)
[row] = controller.overview(session)
assert row.question == memo.question and row.group == 'open'
with pytest.raises(ReviewError, match='policy_denied'):
controller.open(session, stored.id)
req = build_request(session, stored, 'list', pdp.version, intake_context=store.intake_context(stored))
req['context']['review_recipient'] = 'another-human'
assert pdp.check(req).outcome == 'policy_denied'
assert store.evidence() == []