Add revisioned decisions and scoped consultation lifecycle
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user