Files
mini-openclaw-v2/backend/test_risk_agent_completion.py
qiaoqichaoandClaude Code d56c699ce6 chore(v2): 从兴邦预审v2白名单抽取独立可运行基座
按 docs/v2/05-implementation-plan.md §1 候选迁移表与 claude_use/T00 白名单,
从 55e2cdb 抽取 224 个文件(2.48 MB)到独立目录 F:/mini-OpenClaw-v2。

- 排除:凭据(.env/server-deploy.conf/旧DELIVERY.md)、运行数据(xingbang_data 等
  4 个 SQLite)、依赖(venv/node_modules 符号链接)、归档(_backup*/test-evidence/
  ori_file)、Office 临时文件
- 目标目录独立 git 仓库,初始恢复点;Gitea 远程与分支待 OQ-06 Q2 定案
- 未部署、未启动服务、未调用模型

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-29 12:58:26 +08:00

151 lines
9.8 KiB
Python

import json
import os
import sqlite3
import tempfile
import unittest
from copy import deepcopy
from pathlib import Path
from unittest.mock import patch
from fastapi import FastAPI
from fastapi.testclient import TestClient
from api import risk_agent_config as engine
from api.risk_agent_alignment import align
from api.risk_agent_connectors import execute, validate_query
from services.business_demo_database import build_business_demo_database, database_summary
class AlignmentTests(unittest.TestCase):
def setUp(self):
self.card={"mapping":{"financial.total_assets":"assets"},"unit":"元","alignment":{"project_field":"pid","period_field":"year","scope_field":"scope","selection":"latest","target_scope":"合并"}}
self.rows=[{"pid":"P1","year":"2024","scope":"合并","assets":10000},{"pid":"P1","year":"2025","scope":"合并","assets":20000}]
def test_latest_keeps_history_and_converts(self):
records,selected=align(self.card,self.rows,"P1")
self.assertEqual(len(records),2)
self.assertEqual(selected["values"]["financial.total_assets"],2)
self.assertEqual(selected["period"],"2025")
def test_wrong_project_rejected(self):
with self.assertRaises(ValueError): align(self.card,self.rows,"P2")
def test_duplicates_rejected(self):
with self.assertRaises(ValueError): align(self.card,self.rows+self.rows,"P1")
def test_period_selection_and_all(self):
_,selected=align(self.card,self.rows,"P1","2024")
self.assertEqual(selected["values"]["financial.total_assets"],1)
self.card["alignment"]["selection"]="all"
records,selected=align(self.card,self.rows,"P1")
self.assertIsNone(selected)
self.assertEqual(len(records),2)
def test_mixed_scopes_rejected(self):
self.card["alignment"].pop("target_scope")
self.rows[1]["scope"]="母公司"
with self.assertRaises(ValueError): align(self.card,self.rows,"P1")
class DatabaseTests(unittest.TestCase):
def test_multitable_business_database_and_configured_cards(self):
with tempfile.TemporaryDirectory() as root:
database = build_business_demo_database(Path(root) / "business.sqlite3")
summary = database_summary(database)
self.assertEqual(len(summary["tables"]), 11)
self.assertEqual(summary["tables"]["lease_project"], 3)
connection = deepcopy(next(item for item in engine.SEED["connections"] if item["id"] == "conn-business-demo"))
connection["database"] = str(database)
cards = {item["id"]: item for item in engine.SEED["cards"] if item["id"].startswith("card-business-")}
with patch.dict(os.environ, {"XINGBANG_SQLITE_ROOT": root}):
project = engine.query_card(cards["card-business-project"], connection, "DEMO-B")
finance = engine.query_card(cards["card-business-finance"], connection, "DEMO-B")
credit = engine.query_card(cards["card-business-credit"], connection, "DEMO-B")
lease = engine.query_card(cards["card-business-lease"], connection, "DEMO-B")
repayment = engine.query_card(cards["card-business-repayment"], connection, "DEMO-B")
self.assertEqual(project[0]["facility_amount"], 5000)
self.assertEqual(len(finance), 2)
self.assertIsNone(finance[-1]["operating_cashflow"])
self.assertEqual(credit[0]["legal_case_count"], 1)
self.assertEqual(lease[0]["lease_total_value"], 5400)
self.assertEqual(lease[0]["duplicate_asset_count"], 1)
self.assertEqual(repayment[0]["evidence_status"], "缺少最新流水")
def test_real_sqlite_read_only_and_allowlist(self):
with tempfile.TemporaryDirectory() as root:
file=Path(root)/"erp.sqlite"
db=sqlite3.connect(file)
db.execute("CREATE TABLE finance(project_id TEXT, assets REAL)")
db.execute("INSERT INTO finance VALUES ('P1',100)");db.commit();db.close()
conn={"type":"SQLite","database":str(file),"allowed_tables":["finance"]}
with patch.dict(os.environ,{"XINGBANG_SQLITE_ROOT":root}):
self.assertEqual(execute(conn,"SELECT * FROM finance WHERE project_id=:project_id","P1")[0]["assets"],100)
self.assertEqual(execute(conn,"SELECT * FROM finance WHERE project_id=:project_id","' OR 1=1 --"),[])
with self.assertRaises(ValueError):execute(conn,"DELETE FROM finance","P1")
with self.assertRaises(ValueError):execute(conn,"SELECT * FROM sqlite_master","P1")
def test_sql_parser_rejects_multi_statement_and_writes(self):
for sql in ("SELECT * FROM finance; DELETE FROM finance", "SELECT * INTO backup FROM finance", "SELECT pg_sleep(5) FROM finance"):
with self.assertRaises(ValueError):validate_query(sql,["finance"],"postgres")
ACCOUNTS=[{"id":"editor","token":"editor-token","role":"admin"},{"id":"approver","token":"review-token","role":"reviewer"},{"id":"manager","token":"manager-token","role":"manager","projects":["XB-2026-001"]}]
class WorkflowTests(unittest.TestCase):
def setUp(self):
self.temp=tempfile.TemporaryDirectory()
self.file_patch=patch.object(engine,"DB_PATH",Path(self.temp.name)/"catalog.sqlite");self.file_patch.start()
self.auth_patch=patch.dict(os.environ,{"XINGBANG_AUTH_USERS":json.dumps(ACCOUNTS)});self.auth_patch.start()
app=FastAPI();app.include_router(engine.router)
self.client=TestClient(app);self.base="/api/xingbang/risk-agent"
def tearDown(self):
self.client.close();self.auth_patch.stop();self.file_patch.stop();self.temp.cleanup()
def call(self,path,method="GET",data=None,token="editor-token"):
return self.client.request(method,self.base+path,json=data,headers={"Authorization":"Bearer "+token})
def test_auth_and_project_scope(self):
self.assertEqual(self.call("/catalog",token="bad").status_code,401)
self.assertEqual(self.call("/cards","POST",{"name":"unauthorized"},"manager-token").status_code,403)
catalog=self.call("/catalog",token="manager-token").json()
self.assertEqual(catalog["cards"],[])
self.assertEqual(self.call("/plans/plan-default/run","POST",{"project_id":"P2"},"manager-token").status_code,403)
result=self.call("/plans/plan-default/run","POST",{"project_id":"XB-2026-001"},"manager-token").json()
self.assertNotIn("configuration",result)
def test_demo_role_dropdown_is_gated_and_preserves_role_permissions(self):
header=lambda role:{"X-Xingbang-Demo-Role":role}
self.assertEqual(self.client.get(self.base+"/access/context",headers=header("manager")).status_code,401)
with patch.dict(os.environ,{"XINGBANG_DEMO_ROLE_LOGIN":"1"}):
for role in ("admin","reviewer","manager"):
response=self.client.get(self.base+"/access/context",headers=header(role))
self.assertEqual(response.status_code,200,response.text)
identity=response.json()
self.assertEqual(identity["role"],role)
self.assertTrue(identity["demo"])
self.assertNotIn("*",identity["projects"])
self.assertEqual(self.client.get(self.base+"/access/context",headers=header("owner")).status_code,401)
self.assertEqual(self.client.post(self.base+"/cards",headers=header("manager"),json={"name":"x"}).status_code,403)
self.assertEqual(self.client.post(self.base+"/cards",headers=header("reviewer"),json={"name":"x"}).status_code,403)
def test_review_publishes_exact_version(self):
item=self.call("/skills","POST",{"name":"新增检查","data":{"engine":"quality","inputs":["*"]}}).json()
item["status"]="published"
self.assertEqual(self.call("/skills/"+item["id"],"PUT",{"name":item["name"],"data":item}).status_code,403)
pending=self.call("/skills/"+item["id"]+"/submit","POST").json()
path="/reviews/"+pending["id"]+"/review"
self.assertEqual(self.call(path,"POST",{"approve":True,"note":"同意"}).status_code,403)
self.assertEqual(self.call(path,"POST",{"approve":True,"note":"确认输入契约"},"review-token").status_code,200)
saved=next(x for x in self.call("/catalog").json()["skills"] if x["id"]==item["id"])
self.assertEqual(saved["status"],"published")
self.assertEqual(saved["approved_by"],"approver")
def test_changed_submission_cannot_be_approved(self):
item=self.call("/skills","POST",{"name":"检查","data":{"engine":"quality","inputs":["*"]}}).json()
pending=self.call("/skills/"+item["id"]+"/submit","POST").json()
self.call("/skills/"+item["id"],"PUT",{"name":"修改后","data":item})
response=self.call("/reviews/"+pending["id"]+"/review","POST",{"approve":True,"note":"同意"},"review-token")
self.assertEqual(response.status_code,409)
def test_natural_rules_reach_model_without_explicit_model_skill(self):
item=self.call("/catalog").json()["rules"][0]
item.update(status="draft",natural_language="重点关注现金流,不能下审批结论")
item=self.call("/rules/"+item["id"],"PUT",{"name":item["name"],"data":item}).json()
pending=self.call("/rules/"+item["id"]+"/submit","POST").json()
self.call("/reviews/"+pending["id"]+"/review","POST",{"approve":True,"note":"内容已核对"},"review-token")
with patch.object(engine,"local_model",return_value="请补充现金流材料") as model:
response=self.call("/plans/plan-default/run","POST",{"project_id":"XB-2026-001"},"manager-token")
self.assertEqual(response.status_code,200,response.text)
context=model.call_args.args[1]
self.assertEqual(context["human_rules"][0]["instructions"],item["natural_language"])
self.assertTrue(context["peer_datasets"])
self.assertIn("advisories",response.json())
if __name__=="__main__":unittest.main()