"""AC-76/77: actual miniOpenClaw run, sandbox mapping, policy and snapshots.""" import os import tempfile import unittest from pathlib import Path from unittest.mock import patch from services.project_assistant import ProjectAssistantService from services.assistant_worker import DurableWorker from services import assistant_store as store, prescreen, prescreen_policy, prescreen_sandbox, component_registry from services.skill_executor import execute_skill from services.trajectory_details import compare, step_details class V2WorkbenchChainTests(unittest.TestCase): def setUp(self): self.temp=tempfile.TemporaryDirectory(ignore_cleanup_errors=True);self.path=Path(self.temp.name) self.service=ProjectAssistantService(self.path/'assistant.sqlite3') self.env=patch.dict(os.environ,{'XINGBANG_LOCAL_MODEL_URL':'','XINGBANG_AUTH_USERS':'','XINGBANG_DEMO_ROLE_LOGIN':'1'}) self.env.start();self.worker=DurableWorker(self.service) self.identity={'id':'demo-manager','role':'manager','projects':['DEMO-A'],'demo':True,'preview':False} def tearDown(self): self.worker.stop();self.env.stop();self.temp.cleanup() def run_once(self): run=self.service.enqueue('DEMO-A',self.identity,plan_id='corporate.prescreen') self.assertTrue(self.worker.process_next()) return self.service.resource('run',run['id'],'DEMO-A') def test_real_chain_has_cashflow_title_and_both_sandbox_actions(self): run=self.run_once();view=prescreen.screening_from_run(run,'demo') ids={d['id'] for d in view['dimension_results']} self.assertIn('corporate.cashflow',ids);self.assertIn('corporate.title',ids) actions=prescreen_sandbox.available_assumptions(view) self.assertEqual({a['key'] for a in actions},{'assume_bank_flow','assume_title'}) original=store.digest(view) for keys,value in [([],42),(['assume_bank_flow'],58),(['assume_title'],51),(['assume_bank_flow','assume_title'],64)]: result=prescreen_sandbox.simulate(view,keys) self.assertAlmostEqual(result['probability_view']['demo_estimate']['value'],value/100) self.assertEqual(original,store.digest(view)) self.assertEqual(view['recommendation'],'insufficient_information','Unavailable model cannot turn into advance') self.assertIsNone(view['probability']) def test_hard_rule_issue_stops_but_pass_or_unknown_does_not(self): run={'id':'run-fixture','project_id':'DEMO-A','input_snapshot_id':'input-fixture','created_at':store.now(),'status':'completed', 'components':{'plan':{'id':'plan-fixture','version':'1.0.0'},'skills':[], 'rules':[{'id':'hard.rule','version':'1.0.0','effect':'hard','on_match':'block','conditions':[{'code':'hard.hit'}]}]}, 'checks':[{'code':'hard.hit','result':'issue'}],'steps':[]} self.assertEqual(prescreen.screening_from_run(run,'demo')['recommendation'],'stop') for result in ('pass','unknown','not_applicable'): run['checks'][0]['result']=result self.assertNotEqual(prescreen.screening_from_run(run,'demo')['recommendation'],'stop') def test_failed_empty_run_is_never_advance(self): result=prescreen_policy.decide([],degradations=[{'code':'RUN_STEP_FAILED','message':'failed'}]) self.assertEqual(result['recommendation'],'insufficient_information') self.assertEqual(prescreen.stage_from_check({'result':'error'}),0) def test_skill_conditions_and_exception_are_executed(self): skill={'id':'test.conditional','version':'1.0.0','name':'现金流核查','engine':'required','inputs':['financial.operating_cashflow'], 'code':'test.missing','conditions':[{'field':'collection.dependency','operator':'eq','threshold':True}], 'exception_field':'collection.evidence'} for values,expected in [({'collection.dependency':False},'not_applicable'),({},'unknown'), ({'collection.dependency':True},'unknown'),({'collection.dependency':True,'financial.operating_cashflow':'20'},'pass'), ({'collection.dependency':True,'collection.evidence':'已核验'},'not_applicable')]: outputs,checks=execute_skill(skill,values,{}) self.assertEqual(checks[0]['result'],expected) self.assertEqual(outputs,{}) def test_comparison_reads_immutable_snapshots_and_keeps_failure(self): a=self.run_once();b=self.run_once() with self.service.connect() as db: before=store.digest(store.get(db,'run',a['id'])) result=compare(db,'DEMO-A',a['id'],b['id']) self.assertTrue(result['read_only']);self.assertFalse(result['configuration_changed']) self.assertEqual(before,store.digest(store.get(db,'run',a['id']))) with self.assertRaises(KeyError):compare(db,'DEMO-B',a['id'],b['id']) details=step_details(a) self.assertIn('corporate.title',details) self.assertTrue(details['corporate.title']['checks']) def test_administrator_can_publish_after_independent_evaluation(self): from services import component_evaluation with self.service.connect() as db: skill={'id':'skill.new','kind':'skill','name':'现金流完整性','engine':'required','inputs':['financial.operating_cashflow'], 'code':'new.missing','test_cases':[{'name':'missing','values':{},'expected_outputs':{},'expected_checks':{'new.missing':'unknown'}}]} draft=component_registry.draft(db,skill,'admin-a');result=component_evaluation.evaluate(db,draft['id'],'admin-a') self.assertTrue(result['passed']) with self.assertRaises(PermissionError):component_registry.release(db,draft['id'],'admin-a') published=component_registry.release(db,draft['id'],'admin-b');self.assertEqual(published['approved_by'],'admin-b') if __name__=='__main__':unittest.main()