Files
mini-openclaw-v2/backend/test_coordination_agent.py
T
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

166 lines
10 KiB
Python

import json
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 risk_agent_config as risk
from api.coordination_agent import router
class CoordinationTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.dbpatch = patch.object(risk, "DB_PATH", Path(self.temp.name) / "test.sqlite")
self.dbpatch.start()
self.env = patch.dict("os.environ", {"XINGBANG_AUTH_USERS": "", "XINGBANG_REQUIRE_AUTH": "0", "XINGBANG_LOCAL_MODEL_URL": ""})
self.env.start()
app = FastAPI()
app.include_router(risk.router)
app.include_router(router)
self.client = TestClient(app)
self.base = "/api/xingbang/risk-agent"
self.root = self.base + "/coordination"
self.run = self.client.post(self.base + "/plans/plan-default/run", json={"project_id": "XB-2026-001"}).json()
result = self.client.post(self.root + "/projects", json={"run_id": self.run["id"], "industry": "制造业", "complexity": 2})
self.assertEqual(result.status_code, 200, result.text)
self.project = result.json()
def tearDown(self):
self.client.close()
self.dbpatch.stop()
self.env.stop()
self.temp.cleanup()
def act(self, kind, **extra):
response = self.client.post(f"{self.root}/projects/{self.project['id']}/actions", json={"version": self.project["version"], "kind": kind, "note": "已核查,人工确认", **extra})
if response.status_code == 200:
self.project = response.json()
return response
def person(self, **extra):
return self.client.post(self.root + "/people", json={"name": "测试主审员", "industries": ["制造业"], "capacity": 5, **extra}).json()
def plan(self, **extra):
response = self.client.post(f"{self.root}/projects/{self.project['id']}/plans", json={"version": self.project["version"], "amount": "5000", "term": 36, "rate": "5.5", "deposit": "10", "guarantee": "担保待核实", "note": "记录当前方案", **extra})
if response.status_code == 200:
self.project = response.json()
return response
def test_duplicate_active_project_and_version_conflict(self):
self.assertEqual(self.client.post(self.root + "/projects", json={"run_id": self.run["id"], "industry": "制造业"}).status_code, 409)
version = self.project["version"]
self.assertEqual(self.act("assign", owner_id=self.person()["id"]).status_code, 200)
self.assertEqual(self.act("stage", stage="待补件/待现场核查", version=version).status_code, 409)
def test_recommendation_excludes_unavailable_conflicts_capacity(self):
valid = self.person()
invalid = [self.person(available=False), self.person(conflicts=[self.run["project_id"]]), self.person(capacity=1)]
response = self.client.get(f"{self.root}/projects/{self.project['id']}/candidates")
self.assertEqual(response.json()[0]["id"], valid["id"])
for person in invalid:
self.assertEqual(self.act("assign", owner_id=person["id"]).status_code, 422)
self.assertEqual(self.act("assign", owner_id=valid["id"]).status_code, 200)
assigned = next(p for p in self.client.get(f"{self.root}/projects/{self.project['id']}/candidates").json() if p["id"] == valid["id"])
self.assertEqual(assigned["load"], 2)
self.assertEqual(assigned["projected_load"], 2)
self.assertEqual(self.client.delete(self.root + "/people/" + valid["id"]).status_code, 409)
def test_task_cannot_skip_checks(self):
self.assertEqual(self.act("task", title="补充现金流", assignee="客户经理", due="2026-10-01", finding_index=0).status_code, 200)
task_id = self.project["tasks"][0]["id"]
self.assertEqual(self.act("task_status", task_id=task_id, status="已解决").status_code, 409)
self.assertEqual(self.act("submit", task_id=task_id, reference="ERP-附件-001").status_code, 200)
self.assertEqual(self.project["tasks"][0]["status"], "已提交待检查")
self.assertEqual(self.act("task_status", task_id=task_id, status="已解决").status_code, 409)
self.assertEqual(self.act("task_status", task_id=task_id, status="待主审员确认").status_code, 200)
self.assertEqual(self.act("task_status", task_id=task_id, status="已解决").status_code, 200)
self.assertEqual(len(self.project["tasks"][0]["history"]), 3)
def test_plan_change_detects_decimal_equality_and_creates_impacts(self):
self.assertEqual(self.plan().status_code, 200)
self.assertEqual(self.plan(amount="5000.00").status_code, 422)
self.assertEqual(self.plan(term=48).status_code, 200)
self.assertEqual(list(self.project["plans"][-1]["changes"]), ["term"])
self.assertTrue(any(i["title"] == "复核保险及担保期限" and i["plan_version"] == 2 for i in self.project["impacts"]))
def test_stage_gates_and_change_reopens_review(self):
self.act("assign", owner_id=self.person()["id"])
for stage in ("待补件/待现场核查", "待财务分析/测算", "待主审员意见"):
self.assertEqual(self.act("stage", stage=stage).status_code, 200)
self.assertEqual(self.act("stage", stage="待上会").status_code, 422)
self.assertEqual(self.act("stage", stage="待部门复核").status_code, 409)
self.plan()
self.assertEqual(self.act("stage", stage="待部门复核").status_code, 409)
for index in range(len(self.run["findings"])):
self.act("risk_review", finding_index=index)
for impact in list(self.project["impacts"]):
self.act("impact", impact_id=impact["id"])
self.assertEqual(self.act("stage", stage="待部门复核").status_code, 200)
self.assertEqual(self.act("stage", stage="待上会").status_code, 200)
self.plan(term=48)
self.assertEqual(self.project["stage"], "待财务分析/测算")
def test_manager_scope_and_mutation_boundaries(self):
self.act("task", title="补充现金流", assignee="客户经理", due="2026-10-01")
task_id = self.project["tasks"][0]["id"]
users = [{"id": "manager", "role": "manager", "token": "manager-token", "projects": []}]
self.client.headers["authorization"] = "Bearer manager-token"
with patch.dict("os.environ", {"XINGBANG_AUTH_USERS": json.dumps(users)}):
self.assertEqual(self.client.get(self.root + "/workspace").json()["projects"], [])
self.assertEqual(self.act("submit", task_id=task_id, reference="ERP-001").status_code, 403)
users[0]["projects"] = [self.run["project_id"]]
with patch.dict("os.environ", {"XINGBANG_AUTH_USERS": json.dumps(users)}):
self.assertEqual(self.act("submit", task_id=task_id, reference="ERP-001").status_code, 200)
self.assertNotIn("configuration", self.project["source"])
self.assertEqual(self.act("risk_review", finding_index=0).status_code, 403)
self.assertEqual(self.act("assign", owner_id="anything").status_code, 403)
def test_admin_cannot_close_risk_as_reviewer(self):
users = [{"id": "admin", "role": "admin", "token": "admin-token"}]
self.client.headers["authorization"] = "Bearer admin-token"
with patch.dict("os.environ", {"XINGBANG_AUTH_USERS": json.dumps(users)}):
self.assertEqual(self.act("risk_review", finding_index=0).status_code, 403)
def test_source_refresh_preserves_history_and_invalidates_risk_confirmation(self):
self.act("risk_review", finding_index=0)
run = self.client.post(self.base + "/plans/plan-default/run", json={"project_id": self.run["project_id"]}).json()
# Use an explicit later timestamp since the risk store timestamps to seconds.
with risk._db() as db:
run["time"] = "2099-01-01T00:00:00+00:00"
db.execute("UPDATE runs SET body=? WHERE id=?", (json.dumps(run), run["id"]))
self.assertEqual(self.act("refresh_source", run_id=run["id"]).status_code, 200)
self.assertEqual(self.project["source_history"][0]["id"], self.run["id"])
self.assertNotIn(f"{run['id']}:0", self.project["risk_reviews"])
self.assertEqual(len(self.project["impacts"]), 3)
def test_model_summary_uses_core_adapter_without_mutating_tasks(self):
with patch.object(risk, "local_model", return_value={"text": "需复核现金流材料", "engine": "mini-openclaw/AgentManager", "messages": []}) as model:
response = self.client.post(f"{self.root}/projects/{self.project['id']}/summary", json={"version": self.project["version"], "kind": "summary"})
self.assertEqual(response.status_code, 200, response.text)
self.assertEqual(response.json()["tasks"], [])
self.assertEqual(response.json()["summary"]["trace"]["engine"], "mini-openclaw/AgentManager")
self.assertEqual(model.call_args.args[0]["native_skills"], ["review_coordination/SKILL.md"])
def test_model_failure_preserves_project(self):
response = self.client.post(f"{self.root}/projects/{self.project['id']}/summary", json={"version": self.project["version"], "kind": "summary"})
self.assertEqual(response.status_code, 503)
current = self.client.get(self.root + "/workspace").json()["projects"][0]
self.assertEqual(current["version"], self.project["version"])
self.assertIsNone(current["summary"])
def test_summary_runs_actual_agent_tool_loop(self):
from test_risk_core import ToolModel
with patch.dict("os.environ", {"XINGBANG_LOCAL_MODEL_URL": "http://127.0.0.1:1234/v1"}), patch("graph.risk_task.ChatOpenAI", return_value=ToolModel(skill_id="review_coordination/SKILL.md")):
response = self.client.post(f"{self.root}/projects/{self.project['id']}/summary", json={"version": self.project["version"], "kind": "summary"})
self.assertEqual(response.status_code, 200, response.text)
trace = response.json()["summary"]["trace"]
self.assertEqual(trace["engine"], "mini-openclaw/AgentManager")
self.assertEqual(len([m for m in trace["messages"] if m["type"] == "tool"]), 4)
if __name__ == "__main__":
unittest.main()