83 lines
5.8 KiB
Python
83 lines
5.8 KiB
Python
"""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()
|