"""Decision outcome read for State Hub's own memos (INFD-WP-0010-T04). Founder decision 2026-11-01: the hub's STATE-WP-0096 dispatch worker pulls outcomes; informed-decision holds no hub credential. Synthetic fixtures only. """ import io import json from types import SimpleNamespace import time import pytest from informed_decision.disposition import Actor, ActorKind, Verb from informed_decision.intake_service import AUDIENCE, IntakeService, TokenReview from informed_decision.policy import build_request from informed_decision.provenance import Claim, Route from informed_decision.store import Store from informed_decision.workload import PrivateWorkloadApp from test_intake import envelope from test_intake_service import request HUB = 'system:serviceaccount:state-hub:state-hub' OTHER = 'system:serviceaccount:other:poser' ISSUER = 'https://kc.coulomb.social' APPROVAL = '12345678-1234-1234-1234-123456789abc' DECISION = '62aa95e4-3ccc-4867-8806-5e3db005b483' HUMAN = Actor('keycape-human', ActorKind.PERSON) AT = 1_793_000_000 @pytest.fixture def setup(tmp_path): private = tmp_path / 'private' private.mkdir(mode=0o700) store = Store(private / 'review.sqlite') submitters, groups = tmp_path / 'submitters.json', tmp_path / 'groups.json' rows = {HUB: {'submitter_id': 'state-hub', 'memo_classes': ['credential-lane'], 'review_groups': ['net-kingdom-admins'], 'daily_limit': 5, 'outcome_read': True}, OTHER: {'submitter_id': 'other', 'memo_classes': ['credential-lane'], 'review_groups': ['net-kingdom-admins'], 'daily_limit': 5}} submitters.write_text(json.dumps(rows)) groups.write_text(json.dumps({'net-kingdom-admins': {'subject': HUMAN.sub, 'display_name': 'Reviewer'}})) caller = {'username': HUB} def token_review(body): assert body['spec']['audiences'] == [AUDIENCE] return {'status': {'authenticated': True, 'audiences': [AUDIENCE], 'user': {'username': caller['username'], 'uid': 'workload-uid'}}} def create(body): return {'id': APPROVAL, 'status': 'requested', 'binding': {**body['binding'], 'human_control': True, 'digest': 'sha256:' + 'a' * 64}} service = IntakeService(store, TokenReview(token_review), create, submitters_path=submitters, groups_path=groups, issuer=ISSUER) return SimpleNamespace(service=service, store=store, caller=caller, submitters=submitters) def get(app, memo_id, method='GET', token='projected-token'): environ = {'PATH_INFO': f'/v1/memos/{memo_id}/outcome', 'REQUEST_METHOD': method, 'HTTP_AUTHORIZATION': 'Bearer ' + token, 'wsgi.input': io.BytesIO(b'')} response = [] body = b''.join(app(environ, lambda status, headers: response.append((status, headers)))) return int(response[0][0].split()[0]), json.loads(body) def pose(s, resolves='on-answer', memo_id='hub-memo-1'): env = envelope() env['serves'] = {'hub_decision_id': DECISION, 'resolves': resolves} env['memo']['id'] = env['idempotency_key'] = memo_id status, receipt = request(s.service, env) assert status == 201 return receipt['memo_id'] def answer(s, memo_id, verb, *, with_policy=True, note=None): store = s.store p = store.present(memo_id, principal_sub=HUMAN.sub, tenant=Claim('tenant:platform', Route.REGISTRATION), principal_type=Claim('human', Route.AUTHENTICATION)) store.acknowledge(p.id, HUMAN, store.memo(memo_id).required_ack_ids) policy_id = None if with_policy: memo = store.memo(memo_id) session = SimpleNamespace(subject=HUMAN.sub, tenant=Claim('tenant:platform', Route.REGISTRATION), principal_type=Claim('human', Route.AUTHENTICATION), roles=(), groups=(), assurance={'level': 'aal2', 'mfa': True, 'source': 'key-cape', 'methods': ['pwd', 'otp'], 'at': AT}) req = build_request(session, memo, verb.value, 'v13', intake_context=store.intake_context(memo)) policy_id = store.observe_policy(SimpleNamespace(request_json=json.dumps(req), decision_json='{}', outcome='allow', usable_until=time.time() + 60, response_sha256='sha256:' + '0' * 64)) reasons = ('wrong_scope',) if verb is Verb.DECLINE else () d = store.record_disposition(p.id, HUMAN, verb, operation_id=f'{verb.value}-once', reasons=reasons, note=note, policy_id=policy_id) return p, d def test_pending_before_any_answer(setup): memo_id = pose(setup) assert get(setup.service, memo_id) == (202, {'kind': 'informed-decision.decision-outcome', 'version': 1, 'memo_id': memo_id, 'status': 'pending'}) def test_decline_is_an_answer_and_value_free(setup): memo_id = pose(setup) p, d = answer(setup, memo_id, Verb.DECLINE, note='packet secret-looking text stays here') status, record = get(setup.service, memo_id) assert status == 200 assert record == { 'kind': 'informed-decision.decision-outcome', 'version': 1, 'status': 'decided', 'hub_decision_id': DECISION, 'memo_id': memo_id, 'memo_revision': 1, 'verb': 'decline', 'subject': {'issuer': ISSUER, 'sub': HUMAN.sub}, 'assurance': {'aal': 'aal2', 'auth_time': '2026-10-26T07:33:20Z'}, 'decided_at': d.at, 'view_hash': p.view_hash, 'presentation_id': p.id, 'approval_id': APPROVAL} raw = json.dumps(record) for forbidden in ('note', 'reasons', 'wrong_scope', 'secret-looking', 'Reviewer', 'brief', 'packet', 'token'): assert forbidden not in raw assert len(raw.encode()) <= 8 * 1024 def test_accept_waits_for_confirmed_entry(setup): memo_id = pose(setup) _, d = answer(setup, memo_id, Verb.ACCEPT) assert get(setup.service, memo_id)[1]['status'] == 'pending' # prepared attempt = setup.store.begin_submission(d.id) assert get(setup.service, memo_id)[1]['status'] == 'pending' # in flight from informed_decision.approval_client import EntryResult setup.store.finish_submission(d.id, attempt, EntryResult(APPROVAL, HUMAN.sub, '2026-11-01T10:00:00Z', 'approved')) status, record = get(setup.service, memo_id) assert status == 200 and record['verb'] == 'accept' assert record['approval_id'] == APPROVAL assert record['approval_entry'] == {'approved_at': '2026-11-01T10:00:00Z'} def test_unresolved_entry_is_not_an_answer(setup): memo_id = pose(setup) _, d = answer(setup, memo_id, Verb.ACCEPT) setup.store.finish_submission(d.id, setup.store.begin_submission(d.id)) # lost response assert get(setup.service, memo_id) == (409, {'error': 'approval_entry_unresolved'}) def test_audit_reference_when_delivered(setup): memo_id = pose(setup) _, d = answer(setup, memo_id, Verb.DECLINE) with setup.store._connection() as db: event = db.execute("SELECT id FROM evidence WHERE json_extract(envelope,'$.data.disposition_id')=?", (d.id,)).fetchone()[0] db.execute("UPDATE outbox SET state='delivered',receiver_reference='audit-rec-1' WHERE id=?", (event,)) assert get(setup.service, memo_id)[1]['audit_ref'] == 'audit-rec-1' def test_non_terminal_verbs_do_not_answer(setup): memo_id = pose(setup) p = setup.store.present(memo_id, principal_sub=HUMAN.sub, tenant=Claim('tenant:platform', Route.REGISTRATION), principal_type=Claim('human', Route.AUTHENTICATION)) setup.store.record_disposition(p.id, HUMAN, Verb.DISCUSS, operation_id='talk', note='question') assert get(setup.service, memo_id)[1]['status'] == 'pending' def test_missing_assurance_refuses(setup): memo_id = pose(setup) answer(setup, memo_id, Verb.DECLINE, with_policy=False) assert get(setup.service, memo_id) == (409, {'error': 'assurance_unrecorded'}) def test_only_own_memos_and_no_existence_oracle(setup): memo_id = pose(setup) assert get(setup.service, 'never-posed') == (404, {'error': 'memo_not_found'}) # Another submitter's memo looks exactly like an unknown one to the hub. setup.caller['username'] = OTHER other_id = pose(setup, memo_id='other-memo-1') setup.caller['username'] = HUB assert get(setup.service, other_id) == (404, {'error': 'memo_not_found'}) assert get(setup.service, memo_id)[0] == 202 def test_only_admitted_reader(setup): setup.caller['username'] = OTHER memo_id = pose(setup, memo_id='other-memo-1') assert get(setup.service, memo_id) == (403, {'error': 'outcome_read_not_admitted'}) setup.caller['username'] = 'system:serviceaccount:x:unknown' assert get(setup.service, memo_id) == (403, {'error': 'submitter_unknown'}) def test_memo_that_does_not_resolve(setup): memo_id = pose(setup, resolves='never') assert get(setup.service, memo_id) == (409, {'error': 'memo_does_not_resolve'}) def test_method_token_and_path_guards(setup): memo_id = pose(setup) assert get(setup.service, memo_id, method='POST') == (405, {'error': 'method_not_allowed'}) assert get(setup.service, memo_id, token='')[0] == 401 assert get(setup.service, 'Bad_ID')[0] == 404 app = PrivateWorkloadApp(intake=setup.service) assert get(app, memo_id)[0] == 202 def test_no_issuer_means_unavailable(setup): memo_id = pose(setup) setup.service.issuer = None assert get(setup.service, memo_id) == (503, {'error': 'outcome_unavailable'}) @pytest.mark.parametrize('row', [ {'outcome_read': False}, {'outcome_read': 'yes'}, {'outcome_read': True, 'present_existing': {'approval_actor': 'service:nk-bind-executor', 'actions': ['bounded-execution']}}, ]) def test_registry_flag_is_exact(setup, row): data = json.loads(setup.submitters.read_text()) data[HUB].update(row) setup.submitters.write_text(json.dumps(data)) assert get(setup.service, 'hub-memo-1') == (503, {'error': 'intake_configuration_unavailable'}) def test_read_is_side_effect_free(setup): memo_id = pose(setup) answer(setup, memo_id, Verb.DECLINE) with setup.store._connection() as db: before = [db.execute(f'SELECT COUNT(*) FROM {t}').fetchone()[0] for t in ('intake_ledger', 'intake_deliveries', 'evidence', 'outbox', 'policy_observations')] for _ in range(3): assert get(setup.service, memo_id)[0] == 200 with setup.store._connection() as db: after = [db.execute(f'SELECT COUNT(*) FROM {t}').fetchone()[0] for t in ('intake_ledger', 'intake_deliveries', 'evidence', 'outbox', 'policy_observations')] assert before == after