Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a10780-6d41-7952-9718-dfbc1f41236f
456 lines
24 KiB
Python
456 lines
24 KiB
Python
import base64
|
|
from dataclasses import replace
|
|
import json
|
|
import time
|
|
|
|
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
|
|
def handoff(setup):
|
|
intake, store, calls, configuration = setup
|
|
env = envelope()
|
|
memo = memo_from(env['memo'])
|
|
app = LifecycleService(LifecycleRepository(store), policy())
|
|
actor = Caller(Attribution('tenant:platform', Actor('author-agent', ActorKind.AGENT)), time.time()+3600, '{}')
|
|
documents = [{'digest': memo.packet[0].hash, 'media_type': 'text/markdown',
|
|
'content_base64': base64.b64encode(env['packet'][0]['content'].encode()).decode()}]
|
|
app.register(actor, memo, key='register', documents=documents)
|
|
app.publish(actor, memo.id, 1, audience={'subjects': ['keycape-human'], 'groups': []}, key='publish')
|
|
options = {'submitter': 'state-hub', 'memo_class': env['memo_class'], 'review_group': env['review_group'],
|
|
'approval_intent': env['approval_intent']}
|
|
return app, actor, memo, options, intake, calls
|
|
|
|
|
|
def prepare(handoff):
|
|
app, actor, memo, options, intake, calls = handoff
|
|
return app.request_review(actor, memo.id, 1, options=options, key='request-review')
|
|
|
|
|
|
def present(store, review_id):
|
|
return store.present(review_id, principal_sub='keycape-human',
|
|
tenant=Claim('tenant:platform', Route.REGISTRATION),
|
|
principal_type=Claim('human', Route.AUTHENTICATION))
|
|
|
|
|
|
def test_durable_handoff_reuses_intake_and_exposes_revision_outcomes(handoff):
|
|
app, actor, memo, options, intake, calls = handoff
|
|
receipt = prepare(handoff)
|
|
app.repository.store = Store(app.repository.store.path)
|
|
assert prepare(handoff) == receipt
|
|
assert app.outcomes(actor, memo.id, 1)['review']['intake_state'] == 'not_submitted'
|
|
assert request(intake, receipt['envelope'])[0] == 201
|
|
assert request(intake, receipt['envelope'])[0] == 200
|
|
assert len(calls) == 1
|
|
store = app.repository.store
|
|
review = store.memo(receipt['review_memo_id'])
|
|
assert review.brief == memo.brief and review.packet == memo.packet
|
|
assert store.memo(memo.id).approval_id is None
|
|
p = present(store, review.id)
|
|
human = Actor('keycape-human', ActorKind.PERSON)
|
|
store.acknowledge(p.id, human, review.required_ack_ids)
|
|
d = store.record_disposition(p.id, human, Verb.DECLINE, operation_id='answer', reasons=('wrong_scope',))
|
|
result = app.outcomes(actor, memo.id, 1)
|
|
assert result['review']['path'] == '/review?memo_id=' + review.id
|
|
assert result['dispositions'][0]['disposition']['id'] == d.id
|
|
assert result['dispositions'][0]['submission_state'] is None
|
|
app.revise(actor, memo.next_version(brief='Refined'), expected_version=1, summary='Update', key='revise')
|
|
assert app.outcomes(actor, memo.id, 2) == {'review': None, 'dispositions': []}
|
|
assert app.outcomes(actor, memo.id, 1)['dispositions'] == result['dispositions']
|
|
|
|
|
|
def test_altered_handoff_and_wrong_submitter_fail_before_create(handoff):
|
|
app, actor, memo, options, intake, calls = handoff
|
|
receipt = prepare(handoff)
|
|
changed = json.loads(json.dumps(receipt['envelope']))
|
|
changed['memo']['brief'] += ' Altered'
|
|
assert request(intake, changed) == (409, {'error': 'review_handoff_conflict'})
|
|
data = json.loads(intake.submitters_path.read_text())
|
|
for row in data.values():
|
|
row['submitter_id'] = 'another'
|
|
intake.submitters_path.write_text(json.dumps(data))
|
|
assert request(intake, receipt['envelope'])[0] == 409
|
|
assert not calls
|
|
|
|
|
|
def test_revision_invalidates_presented_review_and_prepared_accept(handoff):
|
|
app, actor, memo, options, intake, calls = handoff
|
|
receipt = prepare(handoff)
|
|
assert request(intake, receipt['envelope'])[0] == 201
|
|
store = app.repository.store
|
|
p = present(store, receipt['review_memo_id'])
|
|
human = Actor('keycape-human', ActorKind.PERSON)
|
|
store.acknowledge(p.id, human, memo.required_ack_ids)
|
|
d = store.record_disposition(p.id, human, Verb.ACCEPT, operation_id='accept')
|
|
app.revise(actor, memo.next_version(brief='Revised'), expected_version=1, summary='Update', key='revise')
|
|
with pytest.raises(Conflict):
|
|
store.begin_submission(d.id)
|
|
with pytest.raises(Conflict):
|
|
store.record_disposition(p.id, human, Verb.DECLINE, operation_id='late-answer', reasons=('wrong_scope',))
|
|
with pytest.raises(Conflict):
|
|
present(store, receipt['review_memo_id'])
|
|
assert request(intake, receipt['envelope'])[0] == 409
|
|
|
|
|
|
def test_unresolved_entry_freezes_source_and_snapshot_import_is_refused(handoff):
|
|
app, actor, memo, options, intake, calls = handoff
|
|
receipt = prepare(handoff)
|
|
assert request(intake, receipt['envelope'])[0] == 201
|
|
store = app.repository.store
|
|
review = store.memo(receipt['review_memo_id'])
|
|
with pytest.raises(Conflict):
|
|
store.save_memo(review.next_version(brief='Bypass'))
|
|
with pytest.raises(Conflict):
|
|
store.save_memo(review)
|
|
p = present(store, review.id)
|
|
human = Actor('keycape-human', ActorKind.PERSON)
|
|
store.acknowledge(p.id, human, memo.required_ack_ids)
|
|
d = store.record_disposition(p.id, human, Verb.ACCEPT, operation_id='accept')
|
|
attempt = store.begin_submission(d.id)
|
|
for unresolved in (False, True):
|
|
if unresolved:
|
|
store.finish_submission(d.id, attempt)
|
|
with pytest.raises(Conflict):
|
|
app.revise(actor, memo.next_version(brief='Update'), expected_version=1, summary='Update', key='revise')
|
|
assert store.memo(memo.id).version == 1
|
|
|
|
|
|
def test_denial_replay_conflict_and_agent_binding_guard(handoff):
|
|
app, actor, memo, options, intake, calls = handoff
|
|
receipt = prepare(handoff)
|
|
with pytest.raises(Conflict):
|
|
app.request_review(actor, memo.id, 1, options={**options, 'review_group': 'other'}, key='request-review')
|
|
with pytest.raises(Conflict):
|
|
app.request_review(actor, memo.id, 1, options=options, key='duplicate')
|
|
app.policy.transport.change = lambda data: data.update(effect='deny')
|
|
with pytest.raises(LifecycleError):
|
|
prepare(handoff)
|
|
with pytest.raises(LifecycleError):
|
|
app.outcomes(actor, memo.id, 1)
|
|
assert request(intake, receipt['envelope'])[0] == 201
|
|
p = present(app.repository.store, receipt['review_memo_id'])
|
|
with pytest.raises(DispositionRefused):
|
|
app.repository.store.record_disposition(p.id, Actor('keycape-human', ActorKind.AGENT), Verb.ACCEPT,
|
|
operation_id='agent-accept')
|
|
|
|
|
|
def test_create_reply_loss_does_not_retry_and_source_change_before_load_refuses(handoff):
|
|
app, actor, memo, options, intake, calls = handoff
|
|
receipt = prepare(handoff)
|
|
creator = intake.creator
|
|
def raced(body):
|
|
result = creator(body)
|
|
app.revise(actor, memo.next_version(brief='Changed during create'), expected_version=1, summary='Update', key='revise')
|
|
return result
|
|
intake.creator = raced
|
|
assert request(intake, receipt['envelope'])[0] == 409
|
|
assert request(intake, receipt['envelope'])[0] == 409
|
|
assert len(calls) == 1
|
|
assert app.outcomes(actor, memo.id, 1)['review']['intake_state'] == 'created'
|
|
with app.repository.store._connection() as db:
|
|
assert not db.execute('SELECT 1 FROM memos WHERE id=?', (receipt['review_memo_id'],)).fetchone()
|
|
|
|
from test_approval_component import component, signing_key
|
|
from test_durable_component import receiver
|
|
|
|
|
|
@pytest.mark.parametrize("via_dispatch", [False, True])
|
|
def test_actual_engine_human_entry_and_independent_audit_link_to_source_revision(handoff, component, receiver, tmp_path, via_dispatch):
|
|
import jwt
|
|
from informed_decision.intake_approval import IntakeApprovalClient
|
|
from informed_decision.approval_http import ApprovalHTTPClient
|
|
from informed_decision.review import ReviewController
|
|
from informed_decision.runtime import AuditPump
|
|
app, actor, memo, options, intake, calls = handoff
|
|
if via_dispatch:
|
|
import os, sys
|
|
source = os.environ.get('INFD_STATE_HUB_SOURCE')
|
|
if not source:
|
|
pytest.skip('State Hub source required for private dispatcher integration')
|
|
sys.path.insert(0, source)
|
|
from api.services.memo_dispatch import Dispatcher
|
|
options = {**options, 'memo_class': 'decision-record'}
|
|
grants = json.loads(intake.submitters_path.read_text())
|
|
for row in grants.values():
|
|
row['memo_classes'] = ['decision-record']
|
|
intake.submitters_path.write_text(json.dumps(grants))
|
|
client, engine, transport, human, signing = component
|
|
groups = json.loads(intake.groups_path.read_text())
|
|
groups[options['review_group']]['subject'] = human.subject
|
|
intake.groups_path.write_text(json.dumps(groups))
|
|
claims = jwt.decode(human.access_token, options={'verify_signature': False})
|
|
claims.update(sub='informed-decision', principal_type='service', scope='approval:create')
|
|
token = jwt.encode(claims, signing, algorithm='RS256', headers={'kid': 'key-1'})
|
|
intake.creator = IntakeApprovalClient('https://approval.test', lambda: token, transport=transport)
|
|
app.revise(actor, memo.next_version(brief='Refined for human acceptance'), expected_version=1,
|
|
summary='Responded to research', key='revise')
|
|
app.publish(actor, memo.id, 2, audience={'subjects': [human.subject], 'groups': []}, key='publish-second')
|
|
receipt = app.request_review(actor, memo.id, 2, options=options, key='review-second')
|
|
if via_dispatch:
|
|
attempts = []
|
|
now = [time.time()]
|
|
def send(method, url, body, headers):
|
|
assert headers['Authorization'] == 'Bearer projected-token'
|
|
result = request(intake, body)
|
|
attempts.append(result[0])
|
|
if len(attempts) == 1:
|
|
raise OSError('lost reply after native creation')
|
|
return result
|
|
worker = Dispatcher(tmp_path / 'private-dispatch', send=send,
|
|
read_token=lambda: 'projected-token', clock=lambda: now[0])
|
|
assert worker.enqueue(receipt['envelope']).status == 'pending'
|
|
assert not attempts
|
|
assert worker.once().status == 'pending'
|
|
now[0] += 10
|
|
worker = Dispatcher(tmp_path / 'private-dispatch', send=send,
|
|
read_token=lambda: 'projected-token', clock=lambda: now[0])
|
|
forwarded = worker.once()
|
|
assert forwarded.status == 'delivered' and attempts == [201, 200]
|
|
loaded = forwarded.receipt
|
|
assert worker.once() is None
|
|
else:
|
|
status, loaded = request(intake, receipt['envelope'])
|
|
assert status == 201, loaded
|
|
controller = ReviewController(app.repository.store, policy(),
|
|
lambda session: ApprovalHTTPClient('https://approval.test', session, transport=transport), intake_attributes=True)
|
|
page = controller.open(human, receipt['review_memo_id'])
|
|
context = controller.policy.transport.calls[-1][3]['context']
|
|
assert context['intake_submitter'] == 'state-hub'
|
|
assert context['approval_requester'] == 'informed-decision' and context['approval_human_control'] is True
|
|
controller.acknowledge(human, page.presentation.id, memo.required_ack_ids)
|
|
controller.act(human, page.presentation.id, Verb.ACCEPT, operation_id='human-accept')
|
|
result = app.outcomes(actor, memo.id, 2)
|
|
assert result['dispositions'][0]['submission_state'] == 'confirmed'
|
|
from informed_decision.web import App, SESSION_COOKIE
|
|
from test_browser_auth import call
|
|
class Login:
|
|
def session(self, sid):
|
|
return human if sid == 'fixture' else None
|
|
browser = App(Login(), controller, lifecycle=app)
|
|
page_response = call(browser, '/decisions/' + memo.id, query='version=2', cookie=SESSION_COOKIE + '=fixture')
|
|
assert page_response['status'] == 200
|
|
assert 'Open protected human review' in page_response['body']
|
|
assert 'approval entry confirmed' in page_response['body']
|
|
native_page = call(browser, '/presentations/' + page.presentation.id, cookie=SESSION_COOKIE + '=fixture')
|
|
assert native_page['status'] == 200 and 'decision revision 2' in native_page['body']
|
|
assert 'content_base64' not in page_response['body']
|
|
assert len(engine.get(loaded['approval_id']).entries) == 1
|
|
assert app.outcomes(actor, memo.id, 1)['dispositions'] == []
|
|
source = app.repository.store.presentation_content(page.presentation.id)['lifecycle_source']
|
|
assert source == {'memo_id': memo.id, 'version': 2, 'request_event_id': receipt['event_id']}
|
|
# Both lifecycle commitments and the existing presentation/disposition evidence drain.
|
|
pump = AuditPump(app.repository.store, receiver[0], lifecycle_delivery=True)
|
|
for _ in range(3):
|
|
pump.tick()
|
|
assert app.repository.store.lifecycle_counts()['pending'] == 0
|
|
assert all(row['state'] == 'delivered' for row in app.repository.store.outbox())
|
|
assert not any(url.endswith('/consume') for _, url in transport.calls)
|
|
|
|
|
|
def test_http_review_request_and_outcome_adapter(handoff):
|
|
from informed_decision.lifecycle_http import LifecycleAPI
|
|
from test_lifecycle_http import call
|
|
app, actor, memo, options, intake, calls = handoff
|
|
api = LifecycleAPI(app, lambda token: actor)
|
|
status, receipt, _ = call(api, f'/v1/decisions/{memo.id}/review-requests', method='POST',
|
|
body={'version': 1, 'options': options})
|
|
assert status == 200
|
|
assert request(intake, receipt['envelope'])[0] == 201
|
|
status, outcome, _ = call(api, f'/v1/decisions/{memo.id}/outcomes', query='version=1')
|
|
assert status == 200 and outcome['review']['intake_state'] == 'ready'
|
|
assert call(api, f'/v1/decisions/{memo.id}/outcomes')[0] == 400
|
|
|
|
|
|
def test_review_request_requires_publication_and_supported_profile(handoff):
|
|
app, actor, memo, options, intake, calls = handoff
|
|
app.revise(actor, memo.next_version(brief='Unpublished successor'), expected_version=1, summary='Update', key='revise')
|
|
with pytest.raises(Conflict, match='publish'):
|
|
app.request_review(actor, memo.id, 2, options=options, key='unpublished')
|
|
from informed_decision.memo import StepKind
|
|
app.revise(actor, app.retrieve(actor, memo.id).next_version(step_kind=StepKind.MITZEICHNUNG),
|
|
expected_version=2, summary='Other step', key='revise-step')
|
|
app.publish(actor, memo.id, 3, audience={'subjects': ['keycape-human'], 'groups': []}, key='publish-third')
|
|
with pytest.raises(ValueError, match='profile'):
|
|
app.request_review(actor, memo.id, 3, options=options, key='unsupported')
|
|
assert not calls
|
|
assert not any(event['kind'] == 'review_requested' for event in app.events(actor, memo.id))
|
|
|
|
|
|
def test_lost_create_reply_remains_unresolved_across_restart(handoff):
|
|
app, actor, memo, options, intake, calls = handoff
|
|
receipt = prepare(handoff)
|
|
def lost(body):
|
|
calls.append(body)
|
|
raise OSError('lost receiver reply')
|
|
intake.creator = lost
|
|
assert request(intake, receipt['envelope'])[0] == 503
|
|
intake.store = Store(intake.store.path)
|
|
assert request(intake, receipt['envelope']) == (409, {'error': 'approval_create_unresolved'})
|
|
assert len(calls) == 1
|
|
assert app.outcomes(actor, memo.id, 1)['review']['intake_state'] == 'creating'
|
|
|
|
|
|
def test_schema_four_upgrade_preserves_evidence_and_fences_old_readers(handoff):
|
|
app, actor, memo, options, intake, calls = handoff
|
|
store = app.repository.store
|
|
before = app.events(actor, memo.id)
|
|
# Reconstruct the prior schema's exact index/version boundary.
|
|
with store._connection() as db:
|
|
db.execute('DROP INDEX lifecycle_review_snapshot')
|
|
db.execute('DROP INDEX lifecycle_review_revision')
|
|
db.execute('PRAGMA user_version=4')
|
|
migrated = Store(store.path)
|
|
with migrated._connection() as db:
|
|
assert db.execute('PRAGMA user_version').fetchone()[0] == 5
|
|
assert db.execute("SELECT count(*) FROM sqlite_master WHERE name IN "
|
|
"('lifecycle_review_snapshot','lifecycle_review_revision')").fetchone()[0] == 2
|
|
assert app.events(actor, memo.id) == before
|
|
receipt = prepare(handoff)
|
|
assert prepare(handoff) == receipt
|
|
|
|
|
|
def test_attribute_review_checks_live_requester_before_policy(handoff):
|
|
from informed_decision.review import ReviewController, ReviewError
|
|
from test_policy_client import human
|
|
app, actor, memo, options, intake, calls = handoff
|
|
receipt = prepare(handoff)
|
|
assert request(intake, receipt['envelope'])[0] == 201
|
|
stored = app.repository.store.memo(receipt['review_memo_id'])
|
|
session = replace(human(), subject=stored.binding.principal.id)
|
|
class Engine:
|
|
def get_approval(self, approval_id):
|
|
return {'binding': {'digest': stored.approval_binding_digest, 'actor': 'wrong-requester', 'human_control': True}}
|
|
controller = ReviewController(app.repository.store, policy(), lambda session: Engine(), intake_attributes=True)
|
|
with pytest.raises(ReviewError, match='binding_changed'):
|
|
controller.open(session, stored.id)
|
|
assert controller.policy.transport.calls == []
|
|
|
|
|
|
def test_intake_policy_guard_rechecks_retained_metadata(handoff):
|
|
from informed_decision.policy import build_request
|
|
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
|
|
review = store.memo(receipt['review_memo_id'])
|
|
session = replace(human(), subject=review.binding.principal.id)
|
|
request_body = build_request(session, review, 'read', 'v1', intake_context=store.intake_context(review))
|
|
observation = policy().check(request_body)
|
|
policy_id = store.observe_policy(observation)
|
|
with store._transaction() as db:
|
|
row = db.execute('SELECT metadata FROM intake_ledger WHERE memo_id=?', (review.id,)).fetchone()
|
|
metadata = json.loads(row['metadata']); metadata['review_group'] = 'changed'
|
|
db.execute('UPDATE intake_ledger SET metadata=? WHERE memo_id=?', (json.dumps(metadata), review.id))
|
|
with pytest.raises(Conflict, match='policy observation'):
|
|
store.present(review.id, principal_sub=session.subject, tenant=session.tenant,
|
|
principal_type=session.principal_type, policy_id=policy_id)
|
|
|
|
|
|
def test_retained_handoff_api_is_revision_scoped_and_reauthorizes(handoff):
|
|
from informed_decision.lifecycle_http import LifecycleAPI
|
|
from test_lifecycle_http import call
|
|
app, actor, memo, options, intake, calls = handoff
|
|
api = LifecycleAPI(app, lambda _: actor)
|
|
path = '/v1/decisions/' + memo.id + '/review-requests'
|
|
assert call(api, path, QUERY_STRING='version=1')[0] == 404
|
|
receipt = prepare(handoff)
|
|
assert call(api, path, QUERY_STRING='version=1')[:2] == (200, receipt)
|
|
assert not calls
|
|
for query in ('', 'version=0', 'version=1&extra=x', 'version=1&version=1'):
|
|
assert call(api, path, QUERY_STRING=query)[0] == 400
|
|
app.revise(actor, memo.next_version(brief='Next revision'), expected_version=1, summary='Refined', key='revise')
|
|
assert call(api, path, QUERY_STRING='version=2')[0] == 404
|
|
# Historical export is evidence, never permission to resubmit a stale request.
|
|
assert call(api, path, QUERY_STRING='version=1')[:2] == (200, receipt)
|
|
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() == []
|