Add revisioned decisions and scoped consultation lifecycle

This commit is contained in:
aiagent
2026-10-19 22:28:53 -07:00
parent 0fcfe1c024
commit 6e4178dc79
27 changed files with 1431 additions and 192 deletions
+57
View File
@@ -157,6 +157,63 @@ class NotifierTests(unittest.TestCase):
self.assertIn(cfg['execution_notification_targets']['worker'],run.call_args.args[0])
self.assertNotIn(cfg['internal_event_thread_id'],run.call_args.args[0])
def test_decision_workflow_uses_bound_consultation_and_never_replays(self):
with tempfile.TemporaryDirectory() as root:
cfg,_,path=self.fixture(root);cfg['decision_notifications']=True
thread='44444444-4444-4444-8444-444444444444'
n={'notification_id':'fact-answer','target':'decision','decision':{'id':'one','title':'Plan','session_thread_id':thread},
'event':{'kind':'fact_answered','request_id':'fact-one','answer':'The prerequisite is verified.'}}
class Client:
def call(self,p,data=None):return {'notifications':[n]} if p=='/api/decision-notifications' else {'notifications':[]}
result=SimpleNamespace(returncode=0,stdout='Queued message 22222222-2222-4222-8222-222222222222 for thread '+thread+'.')
state={}
with patch.object(notifier.subprocess,'run',return_value=result) as run:
notifier.deliver(Client(),cfg,state,path);notifier.deliver(Client(),cfg,state,path)
self.assertEqual(run.call_count,1)
self.assertIn(thread,run.call_args.args[0])
self.assertNotIn(cfg['notification_targets']['control'],run.call_args.args[0])
def test_review_notice_is_concise_and_submission_is_not_acceptance(self):
n={'notification_id':'notice','decision':{'id':'one','title':'Plan','options':[{'id':'a','label':'Private option detail'}]},
'event':{'kind':'reviewed','reason':'Suitable in scope','impact':'No migration needed','execution_plan':'Verify before rollout'}}
text=notifier.notification_text(n)
self.assertNotIn('Private option detail',text)
self.assertIn('#/decisions?decision=one',text)
n['event']={'kind':'submitted','revision':2}
self.assertIn('不要把提交直接当接受',notifier.notification_text(n))
n['event']={'kind':'archive_required','revision':3}
self.assertIn('不能只写数据库冒充宿主归档',notifier.notification_text(n))
def test_failed_decision_submission_marker_does_not_queue_or_leave_unknown_model_input(self):
with tempfile.TemporaryDirectory() as root:
cfg,_,path=self.fixture(root);cfg['decision_notifications']=True
n={'notification_id':'submit','target':'control','decision':{'id':'one','title':'Plan'},'event':{'kind':'submitted','revision':1}}
class Client:
failed=False
def call(self,p,data=None):
if p=='/api/decision-notifications':return {'notifications':[n]}
if p.endswith('/submitting') and not self.failed:
self.failed=True;raise OSError('temporary marker interruption')
return {'notifications':[]}
c=Client();state={}
result=SimpleNamespace(returncode=0,stdout='Queued message 22222222-2222-4222-8222-222222222222 for thread '+cfg['notification_targets']['control']+'.')
with patch.object(notifier.subprocess,'run',return_value=result) as run:
with self.assertRaises(RuntimeError):notifier.deliver(c,cfg,state,path)
self.assertEqual(state['deliveries'],{})
run.assert_not_called()
notifier.deliver(c,cfg,state,path)
self.assertEqual(run.call_count,1)
def test_consultation_target_cannot_point_to_primary_or_unbound_thread(self):
with tempfile.TemporaryDirectory() as root:
cfg,_,path=self.fixture(root);cfg['decision_notifications']=True
n={'notification_id':'bad','target':'decision','decision':{'id':'one','session_thread_id':cfg['notification_targets']['control']},'event':{'kind':'fact_answered'}}
class Client:
def call(self,p,data=None):return {'notifications':[n]} if p=='/api/decision-notifications' else {'notifications':[]}
with patch.object(notifier.subprocess,'run') as run:
with self.assertRaises(RuntimeError):notifier.deliver(Client(),cfg,{},path)
run.assert_not_called()
def test_no_events_means_no_conversation_input(self):
with tempfile.TemporaryDirectory() as root:
cfg,n,path=self.fixture(root)