feat: 完成三角色工作台及金租对公Skills全流程演示

This commit is contained in:
NathanAlex7
2026-09-15 10:32:24 +08:00
parent 5f0b1f9409
commit 47ec090345
58 changed files with 2534 additions and 609 deletions
+132
View File
@@ -244,6 +244,25 @@ class ProjectAssistantTests(unittest.TestCase):
self.assertEqual(errors, [])
self.assertEqual(values, mock_values)
def test_multitable_business_sql_plan_reaches_project_pre_review(self):
project = self.project("DEMO-A")
queued = self.post("/projects/DEMO-A/runs", {
"plan_id": "corporate.business-sql", "revision": project["revision"],
"requirement": "核对真实多表数据卡",
})
self.assertEqual(queued.status_code, 202, queued.text)
self.assertTrue(self.worker.process_next())
run = self.service.resource("run", queued.json()["id"])
self.assertIn(run["status"], ("completed", "partial"), run)
snapshot = self.service.resource("snapshot", run["snapshot_id"])
self.assertEqual(
{item["card_id"] for item in snapshot["datasets"]},
{"card-business-project", "card-business-finance", "card-business-credit", "card-business-lease", "card-business-repayment"},
)
project_source = next(item for item in run["evidence"] if item["field"] == "facility.amount" and item.get("card_id") == "card-business-project")
self.assertEqual(project_source["value"], "3000.0")
self.assertEqual(project_source["locator"], "数据卡字段 facility_amount")
def test_concurrent_queued_projects_stay_isolated(self):
from concurrent.futures import ThreadPoolExecutor
queued = [self.post(f"/projects/{p}/runs").json() for p in ("DEMO-A", "DEMO-B", "DEMO-C")]
@@ -273,6 +292,119 @@ class ProjectAssistantTests(unittest.TestCase):
with migrated.connect() as db:
self.assertEqual(len(listing(db, "legacy_archive")), 1)
def test_role_specific_plans_time_dashboard_and_demo_worker(self):
env = {"XINGBANG_DEMO_ROLE_LOGIN": "1", "XINGBANG_DEMO_PROJECTS": "DEMO-A,DEMO-B,DEMO-C"}
headers = {"X-Xingbang-Demo-Role": "manager"}
with patch.dict(os.environ, env):
project = self.client.get(self.base + "/projects/DEMO-B", headers=headers).json()
self.assertIn("corporate.pre-review", [p["id"] for p in project["plans"]])
self.assertNotIn("corporate.independent-review", [p["id"] for p in project["plans"]])
denied = self.post("/projects/DEMO-B/runs", {"plan_id": "corporate.independent-review"}, headers=headers)
self.assertEqual(denied.status_code, 403)
queued = self.post("/projects/DEMO-B/runs", {"plan_id": "corporate.pre-review"}, headers=headers).json()
self.assertTrue(self.worker.process_next())
self.assertIn(self.service.resource("run", queued["id"])["status"], ("completed", "partial"))
dashboard = self.client.get(self.base + "/time-dashboard", headers=headers)
self.assertEqual(dashboard.status_code, 200, dashboard.text)
self.assertGreaterEqual(dashboard.json()["summary"]["project_count"], 3)
timeline = self.service.get_project("DEMO-B")["timeline"]
live = next(e for e in timeline if e["source_id"] == queued["id"])
self.assertEqual(live["status"], "completed")
def test_conversation_command_uses_business_service_and_submits_report(self):
env = {"XINGBANG_DEMO_ROLE_LOGIN": "1", "XINGBANG_DEMO_PROJECTS": "DEMO-A,DEMO-B,DEMO-C"}
headers = {"X-Xingbang-Demo-Role": "manager"}
with patch.dict(os.environ, env):
command = self.post("/commands", {"project_id": "DEMO-A", "message": "请对这个项目进行AI预审"}, headers={**headers, "Idempotency-Key": "chat-run"})
self.assertEqual(command.status_code, 200, command.text)
self.assertEqual(command.json()["intent"], "start_run")
self.worker.process_next()
report = self.post("/commands", {"project_id": "DEMO-A", "message": "生成AI预审报告"}, headers={**headers, "Idempotency-Key": "chat-report"})
self.assertEqual(report.status_code, 200, report.text)
self.assertEqual(report.json()["intent"], "create_report")
submitted = self.post("/commands", {"project_id": "DEMO-A", "message": "提交正式评审"}, headers=headers)
self.assertEqual(submitted.status_code, 200, submitted.text)
self.assertEqual(submitted.json()["intent"], "submit_review")
self.assertEqual(submitted.json()["url"], "/review-pool?submitted=DEMO-A")
self.assertEqual(self.service.resource("project", "DEMO-A")["workflow_state"], "submitted")
reviewer_list = self.client.get(self.base + "/projects", headers={"X-Xingbang-Demo-Role": "reviewer"})
reviewer_a = next(item for item in reviewer_list.json()["projects"] if item["id"] == "DEMO-A")
self.assertEqual(reviewer_a["submitted_at"], self.service.resource("project", "DEMO-A")["submitted_at"])
self.assertEqual(reviewer_a["review_state"], "待领取")
reviewer = {"X-Xingbang-Demo-Role": "reviewer", "Idempotency-Key": "chat-review"}
claimed = self.post("/projects/DEMO-A/review-claim", {}, headers={"X-Xingbang-Demo-Role": "reviewer"})
self.assertEqual(claimed.status_code, 201, claimed.text)
self.assertEqual(claimed.json()["submitted_at"], reviewer_a["submitted_at"])
self.assertGreaterEqual(claimed.json()["wait_hours"], 0)
independent = self.post("/commands", {"project_id": "DEMO-A", "message": "发起独立评审"}, headers=reviewer)
self.assertEqual(independent.status_code, 200, independent.text)
self.assertEqual(independent.json()["intent"], "start_run")
self.assertEqual(self.service.resource("run", independent.json()["object_id"])["run_kind"], "independent_review")
decision = self.post("/projects/DEMO-A/review-decisions", {"decision": "退回", "opinion": "补充最新银行流水"}, headers={"X-Xingbang-Demo-Role": "reviewer"})
self.assertEqual(decision.status_code, 200, decision.text)
self.assertEqual(decision.json()["decision"], "退回")
def test_full_review_topics_gate_approval_and_freeze_trace(self):
pre_run = self.run_project("DEMO-A")
report = self.post("/projects/DEMO-A/reports", {"run_id": pre_run["id"]}).json()
submitted = self.post("/projects/DEMO-A/reports/" + report["id"] + "/actions",
{"action": "submit", "revision": report["revision"], "payload": {}})
self.assertEqual(submitted.status_code, 200, submitted.text)
self.assertEqual(self.post("/projects/DEMO-A/review-claim").status_code, 201)
queued = self.post("/projects/DEMO-A/runs", {"plan_id": "corporate.independent-review", "revision": self.project("DEMO-A")["revision"]})
self.assertEqual(queued.status_code, 202, queued.text)
self.worker.process_next()
project = self.project("DEMO-A")
self.assertEqual(len(project["review_topics"]), 6)
topic_codes = [check["code"] for topic in project["review_topics"] for check in topic["checks"]]
self.assertEqual(len(topic_codes), len(set(topic_codes)))
lease_topic = next(item for item in project["review_topics"] if item["id"] == "lease_asset")
self.assertTrue(any("coverage" in check["code"] for check in lease_topic["checks"]))
denied = self.post("/projects/DEMO-A/review-decisions", {"decision": "批准"})
self.assertEqual(denied.status_code, 409, denied.text)
run_id = queued.json()["id"]
for topic in project["review_topics"]:
decision = "确认无异常" if topic["ai_status"] == "pass" else "不适用"
response = self.post("/projects/DEMO-A/review-decisions", {
"node_id": topic["id"], "decision": decision,
"opinion": "该领域不适用于本项目" if decision == "不适用" else "",
})
self.assertEqual(response.status_code, 200, response.text)
trace = response.json()
self.assertEqual(trace["run_id"], run_id)
self.assertEqual(trace["source_snapshot_id"], queued.json().get("snapshot_id") or self.service.resource("run", run_id)["snapshot_id"])
self.assertIn("evidence_ids", trace)
self.assertIn("basis_ids", trace)
approved = self.post("/projects/DEMO-A/review-decisions", {"decision": "批准"})
self.assertEqual(approved.status_code, 200, approved.text)
self.assertEqual(len(approved.json()["decision_refs"]), 6)
def test_manager_review_pool_withdraw_and_resubmit_preserves_versions(self):
env = {"XINGBANG_DEMO_ROLE_LOGIN": "1", "XINGBANG_DEMO_PROJECTS": "DEMO-A,DEMO-B,DEMO-C"}
manager = {"X-Xingbang-Demo-Role": "manager"}
with patch.dict(os.environ, env):
queued = self.post("/projects/DEMO-A/runs", {"plan_id": "corporate.pre-review"}, headers=manager)
self.assertEqual(queued.status_code, 202, queued.text)
self.worker.process_next()
report = self.post("/projects/DEMO-A/reports", {"run_id": queued.json()["id"], "template_id": "template.ai-pre-review"}, headers=manager).json()
submitted = self.post("/projects/DEMO-A/reports/" + report["id"] + "/actions",
{"action": "submit", "revision": report["revision"], "payload": {}}, headers=manager)
self.assertEqual(submitted.status_code, 200, submitted.text)
pool = self.client.get(self.base + "/review-pool", headers=manager).json()["projects"]
self.assertEqual(next(item for item in pool if item["project_id"] == "DEMO-A")["status"], "submitted")
withdrawn = self.post("/projects/DEMO-A/review-withdraw", headers=manager)
self.assertEqual(withdrawn.status_code, 200, withdrawn.text)
self.assertEqual(withdrawn.json()["status"], "withdrawn")
repeated = self.post("/projects/DEMO-A/review-resubmit", headers=manager)
self.assertEqual(repeated.status_code, 201, repeated.text)
self.assertEqual(repeated.json()["sequence"], 2)
reviewer_pool = self.client.get(self.base + "/review-pool", headers={"X-Xingbang-Demo-Role": "reviewer"}).json()["projects"]
self.assertTrue(any(item["project_id"] == "DEMO-A" and item["status"] == "submitted" for item in reviewer_pool))
with self.service.connect() as db:
versions = listing(db, "submission", "DEMO-A")
self.assertEqual(len(versions), 2)
self.assertEqual(versions[1]["status"], "withdrawn")
if __name__ == "__main__":
unittest.main()