Files
mini-openclaw-v2/backend/test_lease_library.py
T

142 lines
8.6 KiB
Python
Raw Normal View History

"""AC-70..73/75: real SQLite selection and API/model boundaries."""
import os
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
from fastapi import FastAPI
from fastapi.testclient import TestClient
from api import lease_library as api
from services import lease_case_library as lib, assistant_store as store
from services.project_assistant import ProjectAssistantService
BASE = '/api/xingbang/risk-agent/project-assistant/library'
ADMIN = {'X-Xingbang-Demo-Role': 'admin'}
class LeaseLibraryTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory(ignore_cleanup_errors=True)
self.root = Path(self.temp.name)
self.svc = ProjectAssistantService(self.root / 'assistant.sqlite3')
lib.initialize(self.svc.db_path)
self.patches = [patch.object(api, 'service', lambda: self.svc),
patch('services.skill_packages.ROOT', self.root / 'packages'),
patch.dict(os.environ, {'XINGBANG_AUTH_USERS':'', 'XINGBANG_REQUIRE_AUTH':'0',
'XINGBANG_LOCAL_MODEL_URL':'', 'XINGBANG_DEMO_ROLE_LOGIN':'1'})]
for p in self.patches: p.start()
app = FastAPI(); app.include_router(api.router)
self.client = TestClient(app)
def tearDown(self):
self.client.close()
for p in reversed(self.patches): p.stop()
self.temp.cleanup()
def test_seed_is_durable_idempotent_and_corporate(self):
lib.initialize(self.svc.db_path)
with store.transaction(self.svc.db_path) as db:
rows = lib.read_query(db, 'SELECT * FROM lease_cases ORDER BY id')
self.assertEqual(len(rows), 240)
self.assertEqual({r['data_domain'] for r in rows}, {'demo'})
self.assertTrue(all(r['lessee'].endswith('有限公司') and r['asset'] and r['meeting_reason'] for r in rows))
self.assertEqual({r['outcome'] for r in rows}, set(lib.OUTCOMES))
import json
for row in rows:
self.assertGreater(row['total_assets'], row['total_liabilities'])
snapshot=json.loads(row['initial_snapshot'])
self.assertNotIn('outcome',snapshot)
self.assertEqual(snapshot['values']['facility.amount'],row['amount'])
def test_filters_sql_and_natural_agree(self):
with self.svc.connect() as db:
a = lib.preview(db, 'a', 'filters', filters={'industry':'制造业','outcome':'通过'})
b = lib.preview(db, 'a', 'natural', '筛选制造业已经通过的历史案例')
c = lib.preview(db, 'a', 'sql', "SELECT * FROM lease_cases WHERE industry='制造业' AND outcome='通过' ORDER BY id")
self.assertEqual(a['case_ids'], b['case_ids'])
self.assertEqual(a['case_ids'], c['case_ids'])
self.assertEqual(a['count'], 12)
def test_unknown_natural_is_rejected(self):
for text in ('制造业或者医疗健康', '没有通过的制造业', '风险小的项目', '金额大于2000万元'):
with self.subTest(text=text), self.assertRaises(lib.LibraryError): lib.natural_filters(text)
self.assertEqual(lib.natural_filters('制造业,金额不少于2000万元'), {'industry':'制造业','min_amount':2000})
def test_sql_rejects_writes_system_tables_and_fabrication(self):
statements = ['DELETE FROM lease_cases', 'PRAGMA table_info(lease_cases)',
'SELECT * FROM assistant_objects', 'SELECT * FROM sqlite_master',
"SELECT * FROM lease_cases; DELETE FROM lease_cases", "SELECT * FROM lease_cases UNION SELECT * FROM lease_cases",
"SELECT * FROM lease_cases WHERE load_extension('bad')=1", 'SELECT lessee FROM lease_cases']
with self.svc.connect() as db:
for sql in statements:
with self.subTest(sql=sql), self.assertRaises(lib.LibraryError): lib.read_query(db, sql)
self.assertEqual(len(lib.read_query(db,'SELECT * FROM lease_cases')),240)
self.assertTrue(store.listing(db,'project'), 'Authorizer must be reset after failure')
def test_confirmation_freezes_exact_scope_and_actor(self):
with self.svc.connect() as db:
preview = lib.preview(db,'a','filters',filters={'mode':'直接租赁'})
confirmed = lib.confirm(db,'a',preview['id'],preview['case_ids'][:2])
selection, rows = lib.selected_cases(db,'a',confirmed['id'])
self.assertEqual([r['id'] for r in rows],preview['case_ids'][:2])
for actor, ids in [('b',None), ('a',[]), ('a',['bad'])]:
with self.assertRaises(lib.LibraryError): lib.confirm(db,actor,preview['id'],ids)
with self.assertRaises(lib.LibraryError): lib.selected_cases(db,'b',confirmed['id'])
self.assertEqual(selection['source_hash'],store.digest(rows))
def test_preview_expiry_and_changed_cases_fail(self):
with self.svc.connect() as db:
p = lib.preview(db,'a','filters')
selection = lib.confirm(db,'a',p['id'],p['case_ids'][:1])
db.execute('UPDATE lease_cases SET amount=1 WHERE id=?',(selection['case_ids'][0],))
with self.assertRaises(lib.LibraryError): lib.selected_cases(db,'a',selection['id'])
with self.assertRaises(lib.LibraryError): lib.validate_selection({'created_by':'a','expires_at':'2000-01-01T00:00:00+00:00'},'a')
def test_api_manager_reads_but_cannot_process_and_reviewer_retired(self):
manager={'X-Xingbang-Demo-Role':'manager'}
self.assertEqual(self.client.get(BASE+'/cases',headers=manager).status_code,200)
self.assertEqual(self.client.post(BASE+'/queries',headers=manager,json={'mode':'filters'}).status_code,403)
self.assertEqual(self.client.get(BASE+'/cases',headers={'X-Xingbang-Demo-Role':'reviewer'}).status_code,403)
def confirmed(self):
preview=self.client.post(BASE+'/queries',headers=ADMIN,json={'mode':'filters','filters':{'industry':'制造业'}}).json()
return self.client.post(BASE+'/selections',headers=ADMIN,json={'preview_id':preview['preview_id'],'selected_ids':preview['case_ids'][:3]}).json()
def test_unconfigured_model_never_creates_candidate(self):
confirmed=self.confirmed()
result=self.client.post(BASE+'/extractions',headers=ADMIN,json={'project_id':'DEMO-A','selection_id':confirmed['id'],'kind':'skill'}).json()
self.assertEqual(result['status'],'failed')
self.assertIsNone(result['candidate_id'])
self.assertEqual(result['degradations'][0]['code'],'LIBRARY_MODEL_UNAVAILABLE')
with self.svc.connect() as db:
self.assertEqual(store.listing(db,'component_draft','DEMO-A'),[])
self.assertEqual(len(store.listing(db,'lease_extraction','DEMO-A')),1)
def test_model_skill_and_rule_are_distinct_governed_drafts(self):
confirmed=self.confirmed()
proposal={'title':'经营现金流核查','description':'制造业早期材料核查','action':'核对经营现金流材料及来源',
'exceptions':'无来源不作通过结论','field':'financial.operating_cashflow','operator':'lt','threshold':'100'}
with patch('services.lease_extraction.invoke',return_value=(proposal,{'model':'test-stub','engine':'scoped-agent'})) as call:
for kind in ('skill','rule_set'):
result=self.client.post(BASE+'/extractions',headers=ADMIN,json={'project_id':'DEMO-A','selection_id':confirmed['id'],'kind':kind}).json()
self.assertEqual(result['status'],'completed')
with self.svc.connect() as db:
draft=store.get(db,'component_draft',result['candidate_id'])
self.assertEqual(draft['origin'],'history_derived')
self.assertEqual(draft['source_evidence_ids'],confirmed['case_ids'])
self.assertEqual(draft['kind'],kind)
self.assertEqual(draft['status'],'draft')
if kind=='rule_set': self.assertEqual(draft['effect'],'advisory')
self.assertEqual(call.call_count,2)
self.assertEqual(len(call.call_args.args[2]),3)
def test_project_and_domain_are_server_guarded(self):
confirmed=self.confirmed()
result=self.client.post(BASE+'/extractions',headers=ADMIN,json={'project_id':'not-allowed','selection_id':confirmed['id'],'kind':'skill'})
self.assertEqual(result.status_code,403)
with self.svc.connect() as db:
project=store.get(db,'project','DEMO-A'); project['data_domain']='production'; store.put(db,'project',project)
result=self.client.post(BASE+'/extractions',headers=ADMIN,json={'project_id':'DEMO-A','selection_id':confirmed['id'],'kind':'skill'})
self.assertEqual(result.status_code,403)
if __name__=='__main__': unittest.main()