Ask Flex Auth list, not read, for each overview row (INFD-WP-0004-T03)

The overview now requests the new list action and its rows carry only the
memo id, version, question, live approval status and the person's own
responses without notes. Opening a memo still asks read; every act asks
its own action. The fixture Flex Auth package admits list under a
re-derived pin. Not deployable until flex-auth answers INFD-IN-0008.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 359683@bnt-lap001
Assistant-Session: eebdc939-7a9b-4e50-9d39-c8437e8a14ec
This commit is contained in:
tegwick 2026-09-21 22:53:31 +02:00
parent 180370ed44
commit 98c8de078e
10 changed files with 85 additions and 33 deletions

View file

@ -1,6 +1,6 @@
{
"package": "informed-decision.fixture",
"version": "v1",
"package_digest": "sha256:bf39cd7fb33c3db6ba5adb3c036911216e35ef03c1168034e38fe741df67653a",
"package_digest": "sha256:f45e366d3a07430a5171716dc9b970dd97b2b1cfa68cfda5a042416f7a277236",
"fixture_only": true
}

View file

@ -6,7 +6,7 @@ version: v1
status: ready
package: flexauth.informed_decision.fixture
allow_ttl: 60s
actions: [read, acknowledge, accept, return, discuss, decline]
actions: [read, list, acknowledge, accept, return, discuss, decline]
owner: fixture-only
fixtures: [fixtures.json]
caring:
@ -32,7 +32,7 @@ decision := {"effect": "allow", "reason": "synthetic_review_fixture"} if {
input.resource.system == "informed-decision"
input.resource.type == "decision-memo"
input.resource.id == "memo:memo-1"
input.action in {"read", "acknowledge", "accept", "return", "discuss", "decline"}
input.action in {"read", "list", "acknowledge", "accept", "return", "discuss", "decline"}
input.context.approval_id == "fixture"
} else := {"effect": "deny", "reason": "fixture_scope_refused"} if {
true

View file

@ -308,7 +308,7 @@ def test_overview_classifies_without_presenting_or_binding(review):
assert {r.memo_id:(r.group,r.engine_status) for r in rows}=={
'memo-1':('open','requested'),'memo-declined':('open','requested'),
'memo-returned':('open','requested'),'memo-closed':('open','requested')}
assert rows[0].memo.question and rows[0].history==()
assert rows[0].question and rows[0].history==()
p=opened(review).presentation;controller.acknowledge(session,p.id,['h-1'])
controller.act(session,p.id,Verb.ACCEPT,operation_id=str(uuid.uuid4()))
@ -320,9 +320,9 @@ def test_overview_classifies_without_presenting_or_binding(review):
rows={r.memo_id:r for r in controller.overview(session)}
assert {k:r.group for k,r in rows.items()}=={'memo-1':'accepted','memo-declined':'declined',
'memo-returned':'returned','memo-closed':'closed'}
assert rows['memo-1'].intent['state']=='confirmed' and rows['memo-1'].intent['approved_at']
assert rows['memo-1'].approved_at
assert rows['memo-1'].engine_status=='approved' and rows['memo-closed'].engine_status=='revoked'
assert [d.verb for d,_ in rows['memo-declined'].history]==[Verb.DECLINE]
assert [h['verb'] for h in rows['memo-declined'].history]==['decline']
# A revision after a return reopens the question on the new version.
controller.store.save_memo(replace(controller.store.memo('memo-returned'),version=2))
assert groups(controller,session)['memo-returned']=='open'
@ -333,7 +333,7 @@ def test_overview_redacts_rows_the_pdp_does_not_allow(review):
controller.policy.transport.change=lambda d:d.update(effect='deny')
before=len(transport.calls)
[row]=controller.overview(session)
assert (row.memo_id,row.group,row.reason,row.memo,row.history)==('memo-1','unavailable','policy_denied',None,())
assert (row.memo_id,row.group,row.reason,row.question,row.history)==('memo-1','unavailable','policy_denied',None,())
assert len(transport.calls)==before
@ -345,7 +345,7 @@ def test_overview_degrades_one_row_when_engine_is_unreachable(review):
transport.request=down
[row]=controller.overview(session)
assert (row.group,row.engine_status,row.reason)==('open',None,'engine_unavailable')
assert row.memo.question==memo.question
assert row.question==memo.question
transport.request=original
@ -415,3 +415,38 @@ def test_home_keeps_the_memo_form_when_the_overview_fails(review,monkeypatch):
monkeypatch.setattr(controller.store,'memos_for',broken)
r=home(review)
assert r['status']==200 and 'could not be listed right now' in r['body'] and 'Open a decision review' in r['body']
def only(action,effect='deny'):
return lambda d:d.update(effect=effect) if d['binding']['action']==action else None
def test_overview_asks_list_and_a_list_allow_never_opens_the_memo(review):
controller,session,app,memo,engine,transport=review
controller.policy.transport.change=only('read')
r=home(review)
assert 'Open for you (1)' in r['body'] and 'Approve the synthetic factory delivery?' in r['body']
assert [json.loads(o['request'])['action'] for o in controller.store.policy_observations()]==['list']
opened_=call(app,'/review',query='memo_id=memo-1',cookie=SESSION_COOKIE+'=fixture-session')
assert opened_['status']==403 and 'private-brief-sentinel' not in opened_['body']
assert controller.store.evidence()==[]
def test_list_row_discloses_no_brief_packet_highlight_or_note(review):
controller,session,app,memo,engine,transport=review;p=opened(review).presentation
controller.acknowledge(session,p.id,['h-1'])
controller.act(session,p.id,Verb.DISCUSS,operation_id=str(uuid.uuid4()),note='private-note-sentinel')
controller.policy.transport.change=only('read')
[row]=controller.overview(session)
assert row.group=='returned' and row.history[0]['verb']=='discuss' and 'note' not in row.history[0]
body=home(review)['body']
for sentinel in ('private-brief-sentinel','private-packet-sentinel','private-note-sentinel',
'Disposable test target','Full fixture terms'):
assert sentinel not in body
def test_a_list_deny_redacts_even_when_read_would_allow(review):
controller,session,*_=review
controller.policy.transport.change=only('list')
[row]=controller.overview(session)
assert (row.group,row.question,row.reason)==('unavailable',None,'policy_denied')