"""Isolated failure proofs: generated test keys and an in-memory deployment adapter.""" import base64 import copy import json import sqlite3 from datetime import datetime, timedelta, timezone from pathlib import Path import pytest import yaml from cryptography.exceptions import InvalidSignature from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat from activity_core.release_broker import AUTHORITY, REQUIRED_CHECKS, Broker, Receipts, canonical, fingerprint ROOT = Path(__file__).resolve().parents[1] NOW = datetime(2026, 9, 28, 16, tzinfo=timezone.utc) @pytest.fixture def bundle(tmp_path): before = list(yaml.safe_load_all((ROOT / 'k8s/gitops/runtime.yaml').read_text())) after = copy.deepcopy(before) next(d for d in after if d['metadata']['name'] == 'actcore-worker')['spec']['template']['spec']['containers'][0]['image'] = 'forgejo.coulomb.social/coulomb/activity-core@sha256:'+'f'*64 keys = {r: Ed25519PrivateKey.generate() for r in ['build','review','health','retention']} trusted = {r: {'public_key': k.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw), 'principal':r, 'roles':{r}} for r,k in keys.items()} broker = Broker(tmp_path/'release.sqlite', Receipts(trusted), admitted=True) return broker, before, after, keys def images(docs): return {c['image'] for d in docs if d['kind']=='Deployment' for c in d['spec']['template']['spec']['containers']} def envelopes(before, after, keys, candidate='a'*40, rollback='b'*40): binding = dict(repository='coulomb/activity-core',application='activity-core', candidate=candidate,rollback=rollback,before_sha256=fingerprint(before),after_sha256=fingerprint(after)) out={} for role,key in keys.items(): payload=dict(schema=1,role=role,authority=AUTHORITY,binding=binding,result='pass', issued_at=NOW.isoformat(),expires_at=(NOW+timedelta(minutes=5)).isoformat()) if role=='health':payload.update(revision=rollback,continuous=True,healthy=True,synced=True,healthy_since=(NOW-timedelta(hours=25)).isoformat()) if role=='build': payload['images']=sorted(images(after)) payload['checks']={name:'success' for name in REQUIRED_CHECKS} if role=='retention':payload['images']=sorted(images(before)|images(after)) out[role]={'key_id':role,'payload':payload,'signature':base64.b64encode(key.sign(canonical(payload))).decode()} return out def resign(receipt,key): receipt['signature']=base64.b64encode(key.sign(canonical(receipt['payload']))).decode() def admit(bundle, **kwargs): broker,before,after,keys=bundle return broker.admit(before,after,'a'*40,'b'*40,envelopes(before,after,keys),now=NOW,**kwargs) class FakeBackend: def __init__(self): self.revision='b'*40;self.calls=[];self.fail_health=False;self.fail_rollback=False;self.lose_response=False def publish_revision(self,expected,target): if self.revision not in {expected,target}:raise ValueError('CAS conflict') self.revision=target;self.calls.append(('publish',target)) if self.lose_response: self.lose_response=False raise ConnectionError('lost response after durable publication') def sync_revision(self,target): assert self.revision==target self.calls.append(('sync',target)) def healthy(self,target): assert self.revision==target return not (self.fail_health if target=='a'*40 else self.fail_rollback) def drive(broker, rid, backend, now=NOW): phases=[] for _ in range(9): phase=broker.advance(rid,backend,now);phases.append(phase) if phase in {'complete','rolled_back','cancelled'}:break return phases def test_normal_release_restart_and_idempotent_delivery(bundle): broker,*_=bundle;rid=admit(bundle);backend=FakeBackend() assert broker.advance(rid,backend,NOW)=='publish_pending' assert not backend.calls restarted=Broker(broker.database,broker.receipts,admitted=True) assert drive(restarted,rid,backend)==['published','synced','complete'] assert admit(bundle)==rid previous=list(backend.calls) assert restarted.advance(rid,backend,NOW)=='complete' assert backend.calls==previous with broker.connect() as db: assert [r[0] for r in db.execute('SELECT phase FROM transitions ORDER BY sequence')]==['planned','publish_pending','published','synced','complete'] def test_health_failure_rolls_back_through_same_adapter(bundle): broker,*_=bundle;rid=admit(bundle);backend=FakeBackend();backend.fail_health=True assert drive(broker,rid,backend)[-1]=='rolled_back' assert backend.calls==[('publish','a'*40),('sync','a'*40),('publish','b'*40),('sync','b'*40)] def test_failed_rollback_holds_exclusive_slot(bundle): broker,before,after,keys=bundle;rid=admit(bundle);backend=FakeBackend();backend.fail_health=True;backend.fail_rollback=True assert drive(broker,rid,backend)[-1]=='rollback_synced' with pytest.raises(sqlite3.IntegrityError): broker.admit(before,after,'c'*40,'b'*40,envelopes(before,after,keys,candidate='c'*40),now=NOW) backend.fail_rollback=False assert broker.advance(rid,backend,NOW)=='rolled_back' def test_lost_publish_response_and_expiry_recovers_prior_revision(bundle): broker,*_=bundle;rid=admit(bundle);backend=FakeBackend();backend.lose_response=True assert broker.advance(rid,backend,NOW)=='publish_pending' with pytest.raises(ConnectionError):broker.advance(rid,backend,NOW) assert backend.revision=='a'*40 restarted=Broker(broker.database,broker.receipts,admitted=True) assert drive(restarted,rid,backend,NOW+timedelta(minutes=6))[-1]=='rolled_back' assert backend.revision=='b'*40 def test_expired_plan_never_publishes(bundle): broker,*_=bundle;rid=admit(bundle);backend=FakeBackend() assert broker.advance(rid,backend,NOW+timedelta(minutes=6))=='cancelled' assert backend.calls==[] @pytest.mark.parametrize('kind',['signature','commit','stale','future','key','role','scope','retention','build','independence','health','checks']) def test_rejects_untrusted_or_incomplete_evidence(bundle,kind): broker,before,after,keys=bundle;e=envelopes(before,after,keys);r=e['review'] if kind=='signature':r['payload']['result']='fail' elif kind=='commit':r['payload']['binding']['candidate']='c'*40;resign(r,keys['review']) elif kind=='stale':r['payload']['issued_at']=(NOW-timedelta(minutes=6)).isoformat();resign(r,keys['review']) elif kind=='future':r['payload']['issued_at']=(NOW+timedelta(seconds=1)).isoformat();resign(r,keys['review']) elif kind=='key':r['key_id']='caller-provided' elif kind=='role':r['key_id']='build' elif kind=='scope':r['payload']['authority']='admin';resign(r,keys['review']) elif kind=='retention':e['retention']['payload']['images']=[];resign(e['retention'],keys['retention']) elif kind=='build':e['build']['payload']['images']=[];resign(e['build'],keys['build']) elif kind=='independence':broker.receipts.trusted['review']['principal']='build' elif kind=='checks':e['build']['payload']['checks']={};resign(e['build'],keys['build']) elif kind=='health':e['health']['payload']['continuous']=False;resign(e['health'],keys['health']) with pytest.raises((ValueError,InvalidSignature)): broker.admit(before,after,'a'*40,'b'*40,e,now=NOW) with broker.connect() as db:assert db.execute('SELECT count(*) FROM releases').fetchone()[0]==0 def test_signed_out_of_scope_manifest_still_refused(bundle): broker,before,after,keys=bundle next(d for d in after if d['kind']=='Deployment')['spec']['replicas']=99 with pytest.raises(ValueError,match='non-image'): broker.admit(before,after,'a'*40,'b'*40,envelopes(before,after,keys),now=NOW) def test_unadmitted_identity_cannot_start_or_resume(bundle): broker,*_=bundle;rid=admit(bundle);broker.admitted=False with pytest.raises(ValueError,match='not admitted'):admit(bundle) with pytest.raises(ValueError,match='not admitted'):broker.advance(rid,FakeBackend(),NOW) def test_fixed_git_and_argo_mutation_shapes(): from activity_core.release_operations import update_child, sync_operations declaration = '''apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: activity-core namespace: argocd spec: project: activity-core source: repoURL: https://forgejo.coulomb.social/coulomb/activity-core.git targetRevision: BBBB path: k8s/gitops destination: namespace: activity-core server: https://kubernetes.default.svc syncPolicy: syncOptions: [CreateNamespace=false, ApplyOutOfSyncOnly=true, PruneLast=true, FailOnSharedResource=true] '''.replace('BBBB','b'*40) updated=update_child(declaration,'b'*40,'a'*40) assert updated==declaration.replace('b'*40,'a'*40) assert update_child(updated,'b'*40,'a'*40)==updated with pytest.raises(ValueError,match='CAS'):update_child(declaration,'c'*40,'a'*40) with pytest.raises(ValueError):update_child(declaration.replace('namespace: activity-core','namespace: foreign'),'b'*40,'a'*40) with pytest.raises(ValueError):update_child(declaration,'b'*40,'main') root,child=sync_operations('c'*40,'a'*40) assert root[0]=='railiance-apps-root' and child[0]=='activity-core' assert root[1]['operation']['sync']['resources']==[{'group':'argoproj.io','kind':'Application','name':'activity-core','namespace':'argocd'}] assert all(op['operation']['sync']['prune'] is False for _,op in [root,child]) def test_different_principals_cannot_share_review_signing_key(bundle): broker,before,after,keys=bundle e=envelopes(before,after,keys) broker.receipts.trusted['review']['public_key']=broker.receipts.trusted['build']['public_key'] resign(e['review'],keys['build']) with pytest.raises(ValueError,match='independent'): broker.admit(before,after,'a'*40,'b'*40,e,now=NOW)