Lane key-cape-oidc-login -> keycape-attended-case-change in the register-client, ensure-groups and bind-env descriptors; deliver-consumer-secret actor agent -> human; verify_consumer reports a value-free, exactly-once probe record list bound into the observation digest. Composition artifacts, type.yaml and admit/compose tests are left for the orchestrator to regenerate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 903097@bnt-lap001 Assistant-Session: aa320c8f-c3fa-46e7-8541-0b58e1e45d23
250 lines
13 KiB
Python
250 lines
13 KiB
Python
"""Synthetic positive and refusal fixtures for the T29 hand-over candidate."""
|
|
from dataclasses import asdict, replace
|
|
import importlib.util
|
|
import json
|
|
from pathlib import Path
|
|
import unittest
|
|
|
|
from warden.case_run import Descriptor, Invocation, OwnerResult, digest
|
|
from warden.case_receipt_state import Outcome, Pins, Refused
|
|
import yaml
|
|
|
|
ROOT = Path(__file__).parent
|
|
spec = importlib.util.spec_from_file_location('handover_adapter', ROOT / 'handover-adapter.py')
|
|
a = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(a)
|
|
|
|
PARAMS = {'client_id': 'forgejo', 'access_groups': 'forge-users,forge-admins'}
|
|
|
|
|
|
def case_bytes(type_=a.CANONICAL_TYPE, params=PARAMS):
|
|
return yaml.safe_dump({'spec': {'type': type_, 'parameters': params}}).encode()
|
|
|
|
|
|
class HandoverTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.case = case_bytes()
|
|
desc_bytes = json.dumps(asdict(a.descriptor('apps')), sort_keys=True, separators=(',', ':')).encode()
|
|
pins = Pins(digest(self.case), digest(b'type'), digest(desc_bytes), digest(b'catalog'),
|
|
digest(b'case-acceptance:test'), digest(b'run sheet'))
|
|
self.admitted = Invocation('hand-over', 'apps', 'forgejo', 'human', 'founder',
|
|
'apps-consumer-handover', pins)
|
|
self.inv = replace(self.admitted, attempt_id='attempt-one', operation='run')
|
|
self.rb = replace(self.admitted, attempt_id='attempt-two', operation='rollback')
|
|
self.evidence, self.receipts, self.revoked = {}, set(), []
|
|
self.observed = {'client_id': 'forgejo', 'enforces_groups': True,
|
|
'groups': ['forge-admins', 'forge-users'], 'login_verified': True,
|
|
'observations': [{'probe': p, 'evidence': 'apps-probe:' + p}
|
|
for p in ('member-login', 'non-member-refused',
|
|
'membership-add-grants', 'membership-remove-revokes')]}
|
|
self.recorded = []
|
|
self.kw = dict(admitted=self.admitted, case_source=self.case, case_acceptance='case-acceptance:test',
|
|
verify_consumer=lambda c, g: dict(self.observed), record_handover=self.record,
|
|
remove_handover=self.remove, publish=self.publish,
|
|
authenticate=lambda ref, payload: self.evidence.get(ref) == payload,
|
|
revoke_session=self.revoke)
|
|
|
|
def revoke(self, inv):
|
|
self.revoked.append(inv)
|
|
return 'session-revocation:' + inv.attempt_id
|
|
|
|
def publish(self, payload):
|
|
ref = 'owner-evidence:' + str(len(self.evidence))
|
|
self.evidence[ref] = payload
|
|
return ref
|
|
|
|
def record(self, client_id, observation):
|
|
self.recorded.append(observation)
|
|
ref = 'handover:' + client_id
|
|
created = ref not in self.receipts
|
|
self.receipts.add(ref)
|
|
return ref, created
|
|
|
|
def remove(self, ref):
|
|
self.receipts.discard(ref)
|
|
return True
|
|
|
|
def test_positive_handover_verify_and_created_only_rollback(self):
|
|
adapter = a.build(**self.kw)
|
|
result = adapter.invoke(self.inv)
|
|
self.assertEqual((result.outcome, result.created), (Outcome.SUCCESS, (a.RECEIPT,)))
|
|
self.assertTrue(adapter.verify(self.inv, result))
|
|
self.assertNotIn(b'synthetic', self.evidence[result.evidence])
|
|
back = adapter.rollback(self.rb, (a.RECEIPT,))
|
|
self.assertEqual(back.outcome, Outcome.ROLLED_BACK)
|
|
self.assertEqual(self.receipts, set())
|
|
self.assertEqual(self.revoked, [self.inv, self.rb])
|
|
for res, inv in ((result, self.inv), (back, self.rb)):
|
|
self.assertIs(res.revoked, True)
|
|
self.assertEqual(res.session_revocation_evidence, 'session-revocation:' + inv.attempt_id)
|
|
self.assertIn(res.session_revocation_evidence.encode(), self.evidence[res.evidence])
|
|
|
|
def test_descriptor_decodes_in_warden_kernel_with_parameter_owner(self):
|
|
for owner in ('apps', 'railiance-apps'):
|
|
desc = a.descriptor(owner)
|
|
self.assertEqual((desc.owner, desc.revocation_scopes), (owner, ('parent-session',)))
|
|
raw = json.dumps(asdict(desc), sort_keys=True, separators=(',', ':')).encode()
|
|
self.assertEqual(Descriptor.decode(raw), desc)
|
|
for owner in (None, '', 'bad owner', 3):
|
|
with self.subTest(owner=owner), self.assertRaises(Refused):
|
|
a.descriptor(owner)
|
|
|
|
def test_parent_session_revoked_even_when_attempt_fails(self):
|
|
self.observed['login_verified'] = False
|
|
adapter = a.build(**self.kw)
|
|
with self.assertRaises(Refused):
|
|
adapter.invoke(self.inv)
|
|
self.assertEqual(self.revoked, [self.inv])
|
|
with self.assertRaises(Refused): # nothing created: rollback refused, session still revoked
|
|
adapter.rollback(self.rb, (a.RECEIPT,))
|
|
self.assertEqual(self.revoked, [self.inv, self.rb])
|
|
self.assertEqual(self.evidence, {})
|
|
|
|
def test_missing_revocation_evidence_refused_before_publication(self):
|
|
for bad in (None, '', 'bad id', 7):
|
|
with self.subTest(bad=bad), self.assertRaises(Refused):
|
|
a.build(**{**self.kw, 'revoke_session': lambda inv, b=bad: b}).invoke(self.inv)
|
|
self.assertEqual(self.evidence, {})
|
|
with self.assertRaises(Refused):
|
|
a.build(**{**self.kw, 'revoke_session': None})
|
|
|
|
def test_existing_receipt_is_no_change_and_not_rollbackable(self):
|
|
self.receipts.add('handover:forgejo')
|
|
adapter = a.build(**self.kw)
|
|
result = adapter.invoke(self.inv)
|
|
self.assertEqual((result.outcome, result.created), (Outcome.NO_CHANGE, ()))
|
|
with self.assertRaises(Refused):
|
|
adapter.rollback(self.rb, (a.RECEIPT,))
|
|
self.assertEqual(self.receipts, {'handover:forgejo'})
|
|
|
|
def test_rollback_refuses_unauthenticated_creation(self):
|
|
adapter = a.build(**{**self.kw, 'authenticate': lambda ref, payload: False})
|
|
adapter.invoke(self.inv)
|
|
with self.assertRaises(Refused):
|
|
adapter.rollback(self.rb, (a.RECEIPT,))
|
|
self.assertEqual(self.receipts, {'handover:forgejo'})
|
|
|
|
def test_consumer_verification_refusals(self):
|
|
for change in ({'enforces_groups': False}, {'login_verified': False}, {'groups': ['forge-users']},
|
|
{'client_id': 'other'}, {'extra': 1}):
|
|
self.observed = {**dict(self.observed), **change}
|
|
with self.subTest(change=change), self.assertRaises(Refused):
|
|
a.build(**self.kw).invoke(self.inv)
|
|
self.observed = {k: v for k, v in self.observed.items() if k != 'extra'}
|
|
self.setUp()
|
|
self.assertEqual(self.receipts, set())
|
|
|
|
def test_observation_record_refusals(self):
|
|
obs = self.observed['observations']
|
|
cases = {
|
|
'missing-field': None,
|
|
'not-a-list': {'member-login': 'apps-probe:x'},
|
|
'missing-probe': obs[:3],
|
|
'extra-probe': obs + [{'probe': 'admin-login', 'evidence': 'apps-probe:y'}],
|
|
'duplicate-probe': obs[:3] + [dict(obs[0])],
|
|
'unknown-probe': obs[:3] + [{'probe': 'admin-login', 'evidence': 'apps-probe:y'}],
|
|
'value-bearing-key': [{**obs[0], 'value': 'secret'}] + obs[1:],
|
|
'missing-evidence': [{'probe': 'member-login'}] + obs[1:],
|
|
'evidence-not-identifier': [{'probe': 'member-login', 'evidence': 'has space/x'}] + obs[1:],
|
|
'evidence-not-string': [{'probe': 'member-login', 'evidence': 7}] + obs[1:],
|
|
'record-not-dict': ['member-login'] + obs[1:],
|
|
}
|
|
for name, value in cases.items():
|
|
self.setUp()
|
|
if value is None:
|
|
del self.observed['observations']
|
|
else:
|
|
self.observed['observations'] = value
|
|
with self.subTest(name=name), self.assertRaises(Refused):
|
|
a.build(**self.kw).invoke(self.inv)
|
|
self.assertEqual(self.receipts, set())
|
|
|
|
def test_observations_bound_into_digest_order_independent(self):
|
|
def run(mutate):
|
|
self.setUp()
|
|
mutate(self.observed['observations'])
|
|
a.build(**self.kw).invoke(self.inv)
|
|
return self.recorded[-1]
|
|
base = run(lambda o: None)
|
|
self.assertEqual(base, run(lambda o: o.reverse()))
|
|
self.assertNotEqual(base, run(lambda o: o[0].update(evidence='apps-probe:changed')))
|
|
|
|
def test_canonical_default_and_explicit_selection_only(self):
|
|
with self.assertRaises(Refused):
|
|
a.build(**{**self.kw, 'version_root': '1.0'})
|
|
case12 = case_bytes(a.SELECTABLE_TYPES['1.2'])
|
|
pins = replace(self.admitted.pins, case=digest(case12))
|
|
admitted12 = replace(self.admitted, pins=pins)
|
|
with self.assertRaises(Refused): # 1.2 never reached by default
|
|
a.build(**{**self.kw, 'admitted': admitted12, 'case_source': case12})
|
|
with self.assertRaises(Refused): # 1.2 case must name consumer_owner_repo
|
|
a.build(**{**self.kw, 'admitted': admitted12, 'case_source': case12, 'version_root': '1.2'})
|
|
case12 = case_bytes(a.SELECTABLE_TYPES['1.2'], {**PARAMS, 'consumer_owner_repo': 'railiance-apps'})
|
|
desc = json.dumps(asdict(a.descriptor('railiance-apps')), sort_keys=True, separators=(',', ':')).encode()
|
|
admitted12 = replace(self.admitted, owner='railiance-apps',
|
|
pins=replace(self.admitted.pins, case=digest(case12), adapter=digest(desc)))
|
|
kw12 = {**self.kw, 'admitted': admitted12, 'case_source': case12, 'version_root': '1.2'}
|
|
inv = replace(admitted12, attempt_id='attempt-one', operation='run')
|
|
self.assertEqual(a.build(**kw12).invoke(inv).outcome, Outcome.SUCCESS)
|
|
with self.assertRaises(Refused): # admitted owner differs from the case's consumer_owner_repo
|
|
a.build(**{**kw12, 'admitted': replace(admitted12, owner='apps')})
|
|
|
|
def test_1_3_explicit_selection_with_1_2_owner_handling(self):
|
|
case13 = case_bytes(a.SELECTABLE_TYPES['1.3'])
|
|
admitted13 = replace(self.admitted, pins=replace(self.admitted.pins, case=digest(case13)))
|
|
with self.assertRaises(Refused): # 1.3 never reached by default
|
|
a.build(**{**self.kw, 'admitted': admitted13, 'case_source': case13})
|
|
with self.assertRaises(Refused): # 1.3 case must name consumer_owner_repo
|
|
a.build(**{**self.kw, 'admitted': admitted13, 'case_source': case13, 'version_root': '1.3'})
|
|
case13 = case_bytes(a.SELECTABLE_TYPES['1.3'], {**PARAMS, 'consumer_owner_repo': 'railiance-apps'})
|
|
desc = json.dumps(asdict(a.descriptor('railiance-apps')), sort_keys=True, separators=(',', ':')).encode()
|
|
admitted13 = replace(self.admitted, owner='railiance-apps',
|
|
pins=replace(self.admitted.pins, case=digest(case13), adapter=digest(desc)))
|
|
kw13 = {**self.kw, 'admitted': admitted13, 'case_source': case13, 'version_root': '1.3'}
|
|
with self.assertRaises(Refused): # a 1.3 case under the 1.2 root
|
|
a.build(**{**kw13, 'version_root': '1.2'})
|
|
inv = replace(admitted13, attempt_id='attempt-one', operation='run')
|
|
self.assertEqual(a.build(**kw13).invoke(inv).outcome, Outcome.SUCCESS)
|
|
with self.assertRaises(Refused): # admitted owner differs from the case's consumer_owner_repo
|
|
a.build(**{**kw13, 'admitted': replace(admitted13, owner='apps')})
|
|
|
|
def test_binding_refusals(self):
|
|
bad = [{'case_acceptance': 'other'}, {'case_source': self.case + b'\n#'},
|
|
{'admitted': replace(self.admitted, owner='net-kingdom')},
|
|
{'admitted': replace(self.admitted, target='other')},
|
|
{'admitted': replace(self.admitted, pins=replace(self.admitted.pins, adapter=digest(b'x')))},
|
|
{'publish': None}]
|
|
for change in bad:
|
|
with self.subTest(change=list(change)), self.assertRaises(Refused):
|
|
a.build(**{**self.kw, **change})
|
|
no_groups = case_bytes(params={'client_id': 'forgejo'})
|
|
with self.assertRaises(Refused):
|
|
a.build(**{**self.kw, 'case_source': no_groups,
|
|
'admitted': replace(self.admitted, pins=replace(self.admitted.pins, case=digest(no_groups)))})
|
|
|
|
def test_attempt_and_contract_refusals(self):
|
|
adapter = a.build(**self.kw)
|
|
for inv in (replace(self.inv, principal='other'), replace(self.inv, operation='rollback'),
|
|
replace(self.inv, attempt_id='')):
|
|
with self.subTest(inv=inv), self.assertRaises(Refused):
|
|
adapter.invoke(inv)
|
|
for receipt in (('handover:forgejo', 'yes'), 'handover:forgejo', ('', True)):
|
|
with self.subTest(receipt=receipt), self.assertRaises(Refused):
|
|
a.build(**{**self.kw, 'record_handover': lambda c, o, r=receipt: r}).invoke(self.inv)
|
|
reuse = a.build(**{**self.kw, 'publish': lambda p: 'owner-evidence:0'})
|
|
reuse.invoke(self.inv)
|
|
with self.assertRaises(Refused): # evidence identifier reuse
|
|
reuse.invoke(replace(self.inv, attempt_id='attempt-three'))
|
|
self.assertFalse(adapter.verify(self.inv, OwnerResult(Outcome.SUCCESS, 'forged')))
|
|
|
|
def test_removal_unconfirmed_keeps_creation(self):
|
|
adapter = a.build(**{**self.kw, 'remove_handover': lambda ref: None})
|
|
adapter.invoke(self.inv)
|
|
with self.assertRaises(Refused):
|
|
adapter.rollback(self.rb, (a.RECEIPT,))
|
|
with self.assertRaises(Refused):
|
|
adapter.rollback(self.rb, ())
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|