import tempfile import json import unittest from io import BytesIO from pathlib import Path from unittest.mock import patch from fastapi import FastAPI from fastapi.testclient import TestClient from docx import Document from api import risk_agent_config as risk from api.report_agent import router class ReportTests(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" run=self.client.post(self.base+"/plans/plan-default/run",json={"project_id":"XB-2026-001"}).json() self.doc=self.client.post(self.base+"/reports/documents",json={"run_id":run["id"],"confirmation":"已核对来源,现金流缺失待补充"}).json() def tearDown(self): self.client.close();self.dbpatch.stop();self.env.stop();self.temp.cleanup() def action(self,kind,**extra): response=self.client.post(self.base+f"/reports/documents/{self.doc['id']}/actions",json={"kind":kind,"version":self.doc["version"],**extra}) if response.status_code==200:self.doc=response.json() return response def finalize(self): self.action("edit",section_id="2",text="负债较高,需核实新增借款用途。") self.action("edit",section_id="3",text="补充现金流材料后复核。") for i in range(4):self.action("lock",section_id=str(i),locked=True) return self.action("finalize",note="逐项确认,保留未解决信息缺口") def test_source_facts_cannot_be_overwritten(self): self.assertEqual(self.action("edit",section_id="0",text="伪造金额").status_code,409) self.assertIn("facility.amount: 5000",self.doc["sections"][0]["text"]) def test_locked_and_final_sections_immutable(self): self.action("lock",section_id="2",locked=True) self.assertEqual(self.action("edit",section_id="2",text="覆盖").status_code,409) self.action("lock",section_id="2",locked=False) self.assertEqual(self.finalize().status_code,200) self.assertEqual(self.doc["experience"]["status"],"草稿") self.assertEqual(self.action("lock",section_id="2",locked=False).status_code,409) def test_finalize_requires_all_locks(self): self.assertEqual(self.action("finalize",note="确认").status_code,409) def test_experience_needs_outcome(self): self.finalize() for _ in range(2):self.assertEqual(self.action("experience",note="审核意见").status_code,200) self.assertEqual(self.action("experience",note="批准共享").status_code,422) self.assertEqual(self.action("experience",note="审核完成",approval_result="审批通过").status_code,200) self.assertEqual(self.action("experience",note="验证").status_code,422) self.assertEqual(self.action("experience",note="租后已核对",post_lease_result="连续12个月正常还款").status_code,200) self.assertEqual(self.doc["experience"]["status"],"已验证") def test_docx_is_valid_and_has_source(self): response=self.client.get(self.base+f"/reports/documents/{self.doc['id']}/export") self.assertEqual(response.status_code,200) document=Document(BytesIO(response.content)) self.assertIn(self.doc["source_id"],"\n".join(p.text for p in document.paragraphs)) def test_model_candidate_not_overwrite(self): before=self.doc["sections"][2]["text"] with patch.object(risk,"local_model",return_value={"text":"建议核查现金流","engine":"mini-openclaw/AgentManager","messages":[]}): response=self.client.post(self.base+f"/reports/documents/{self.doc['id']}/generate",json={"kind":"generate","version":self.doc["version"],"section_id":"2"}) self.assertEqual(response.status_code,200,response.text) self.assertEqual(response.json()["sections"][2]["text"],before) self.assertEqual(response.json()["proposals"][0]["text"],"建议核查现金流") def test_manager_project_scope_and_read_only(self): users=[{"id":"manager","token":"test-manager","role":"manager","projects":[]}] with patch.dict("os.environ",{"XINGBANG_AUTH_USERS":json.dumps(users)}): self.client.headers["authorization"]="Bearer test-manager" self.assertEqual(self.client.get(self.base+"/reports/documents").json(),[]) self.assertEqual(self.client.get(self.base+f"/reports/documents/{self.doc['id']}/export").status_code,403) self.assertEqual(self.action("edit",section_id="2",text="changed").status_code,403) users[0]["projects"]=[self.doc["project_id"]] with patch.dict("os.environ",{"XINGBANG_AUTH_USERS":json.dumps(users)}): self.assertEqual(len(self.client.get(self.base+"/reports/documents").json()),1) self.assertEqual(self.client.get(self.base+f"/reports/documents/{self.doc['id']}/export").status_code,200) def test_shared_experience_requires_independent_reviewer(self): users=[{"id":"reviewer-one","token":"one","role":"reviewer","projects":[self.doc["project_id"]]},{"id":"reviewer-two","token":"two","role":"reviewer","projects":[self.doc["project_id"]]}] with patch.dict("os.environ",{"XINGBANG_AUTH_USERS":json.dumps(users)}): self.client.headers["authorization"]="Bearer one" self.assertEqual(self.finalize().status_code,200) for _ in range(2):self.assertEqual(self.action("experience",note="审核意见").status_code,200) self.assertEqual(self.action("experience",note="共享",approval_result="有条件通过").status_code,403) self.client.headers["authorization"]="Bearer two" self.assertEqual(self.action("experience",note="独立审核通过",approval_result="有条件通过").status_code,200) if __name__=="__main__":unittest.main()