Files
mini-openclaw/backend/test_operations_center.py
T

160 lines
8.6 KiB
Python

import json
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 operations_center as api
from api import risk_agent_config as catalog
from services.project_assistant import ProjectAssistantService
from services.operations_demo import DemoService, PROJECT
from services.assistant_worker import DurableWorker
from services.assistant_store import get, listing
class OperationsCenterTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory(ignore_cleanup_errors=True)
self.path = Path(self.temp.name)
self.current = ProjectAssistantService(self.path / "current.sqlite3")
self.lab = DemoService(self.path / "lab.sqlite3")
self.patches = [patch.object(api, "current_service", lambda: self.current), patch.object(api, "demo_service", lambda: self.lab),
patch.object(catalog, "DB_PATH", self.path / "catalog.sqlite3"),
patch.dict(os.environ, {"XINGBANG_AUTH_USERS":"", "XINGBANG_REQUIRE_AUTH":"0", "XINGBANG_LOCAL_MODEL_URL":"", "XINGBANG_ASSISTANT_ENABLED":"1", "XINGBANG_ASSISTANT_DEMO_ENABLED":"1"})]
for p in self.patches:
p.start()
app = FastAPI()
app.include_router(api.router)
self.client = TestClient(app)
self.worker = DurableWorker(self.lab)
self.root = "/api/xingbang/risk-agent/project-assistant/operations"
def tearDown(self):
self.worker.stop()
self.client.close()
for p in reversed(self.patches):
p.stop()
self.temp.cleanup()
def get(self, path):
return self.client.get(self.root + path)
def post(self, path, body=None, key="test-run"):
return self.client.post(self.root + path, json=body or {}, headers={"Idempotency-Key":key})
def submit(self, key="test-run", model=False):
result = self.post("/lab/runs", {"source_revision":self.lab.source()["project"]["revision"], "model":model}, key)
self.assertEqual(result.status_code, 202, result.text)
return result.json()
def edit(self, **overrides):
s = self.lab.source()
body = {"revision":s["project"]["revision"], "amount":s["project"]["amount"], "liabilities":s["finance"]["liabilities"], "dd_liabilities":s["finance"]["dd_liabilities"], "cashflow":s["finance"]["cashflow"], "duplicate":bool(s["assets"][2]["active"]), "enabled":True, **overrides}
result = self.post("/lab/source", body)
self.assertEqual(result.status_code, 200, result.text)
return body
def test_real_sql_worker_and_scoped_monitor(self):
self.assertEqual(len(self.lab.list_projects()), 1)
self.assertEqual(len(self.lab.source()["assets"]), 3)
initial = self.submit()
self.assertEqual(self.get("/overview?domain=lab").json()["counts"]["queued"], 1)
self.worker.process_next()
detail = self.get("/runs/" + initial["id"] + "?domain=lab").json()
self.assertEqual(detail["run"]["status"], "partial")
self.assertEqual(detail["run"]["summary"], {"coverage":"1.0800", "open":4, "resolved":0})
self.assertTrue(all("SQLite" in e["source"] for e in detail["run"]["evidence"] if e["field"] == "lease.total_value"))
self.assertEqual(detail["source_revision"], 1)
self.assertEqual(self.get("/overview?domain=current").json()["total_runs"], 0)
self.assertNotIn("model_config", detail["run"])
self.assertNotIn("identity", detail["run"])
def test_source_changes_require_sync_and_keep_snapshots(self):
first = self.submit()
self.worker.process_next()
original = self.lab.resource("input_snapshot", first["input_snapshot_id"])
self.edit(amount="4000", dd_liabilities="13200", cashflow="900", duplicate=False)
self.assertEqual(self.lab.resource("project", PROJECT)["values"]["facility.amount"], "5000")
second = self.submit("second")
self.worker.process_next()
result = self.lab.resource("run", second["id"])
self.assertEqual(result["summary"]["coverage"], "1.3500")
self.assertEqual(result["summary"]["open"], 0)
self.assertEqual(result["status"], "completed")
self.assertEqual(self.lab.resource("input_snapshot", first["input_snapshot_id"]), original)
self.assertEqual(self.current.get_project("DEMO-B")["facts"]["facility_amount"], "5000")
def test_frozen_configuration_and_idempotency(self):
first = self.submit()
self.assertEqual(self.submit()["id"], first["id"])
source = self.get("/lab/source").json()
change = self.post("/lab/rule", {"version":"2.0.0", "revision":source["release"]["revision"]})
self.assertEqual(change.status_code, 200)
self.worker.process_next()
detail = self.get("/runs/" + first["id"] + "?domain=lab").json()
rule = next(c for c in detail["components"] if c["id"] == "lab.coverage")
self.assertEqual(rule["version"], "1.0.0")
self.assertTrue(rule["changed"])
second = self.submit("new-version")
self.assertEqual(next(c for c in second["component_versions"] if c["id"] == "lab.coverage")["version"], "2.0.0")
def test_pause_conflict_cancel_and_invalid_amount(self):
body = self.edit(enabled=False)
self.assertEqual(self.post("/lab/source", body).status_code, 409)
self.assertEqual(self.post("/lab/runs", {"source_revision":2}).status_code, 422)
with self.lab.connect() as db:
self.assertEqual(len(listing(db, "run")), 0)
body.update(revision=2,amount="NaN")
self.assertEqual(self.post("/lab/source", body).status_code, 422)
self.edit(enabled=True)
run = self.submit()
self.assertEqual(self.post("/lab/runs/" + run["id"] + "/cancel").status_code, 200)
self.assertFalse(self.worker.process_next())
def test_no_fake_natural_language_or_model_success(self):
self.assertEqual(self.post("/lab/runs", {"source_revision":1,"requirement":"重点现金流"}).status_code, 422)
run = self.submit(model=True)
self.worker.process_next()
run = self.lab.resource("run", run["id"])
model = next(s for s in run["steps"] if s["step_id"] == "corporate.analysis")
self.assertEqual(model["status"], "failed")
self.assertIn("未配置", model["errors"][0]["message"])
self.assertEqual(run["status"], "partial")
def test_evaluation_is_real_and_isolated(self):
before = self.lab.source()
result = self.post("/lab/evaluate")
self.assertEqual(result.status_code, 200, result.text)
self.assertTrue(result.json()["passed"], result.text)
self.assertEqual([len(r["actual_codes"]) for r in result.json()["cases"]], [4,2,0,1])
self.assertEqual(self.lab.source(), before)
self.assertEqual(self.get("/overview?domain=lab").json()["total_runs"], 0)
self.assertEqual(len(self.get("/overview?domain=lab").json()["evaluations"]), 1)
def test_authorization_and_demo_switch(self):
run = self.submit()
accounts = [{"id":"reviewer","token":"r","role":"reviewer","projects":["DEMO-A"]}, {"id":"manager","token":"m","role":"manager","projects":[PROJECT]}]
with patch.dict(os.environ, {"XINGBANG_AUTH_USERS":json.dumps(accounts)}):
for token, code in (("r",403),("m",200)):
self.assertEqual(self.client.get(self.root+"/overview?domain=lab", headers={"Authorization":"Bearer "+token}).status_code, code)
self.assertEqual(self.client.get(self.root+"/runs/"+run["id"]+"?domain=lab", headers={"Authorization":"Bearer r"}).status_code,403)
self.assertEqual(self.client.post(self.root+"/lab/evaluate", headers={"Authorization":"Bearer m"},json={}).status_code,403)
self.assertEqual(self.client.get(self.root+"/health", headers={"Authorization":"Bearer m"}).status_code,403)
self.assertEqual(self.client.get(self.root+"/overview?domain=current", headers={"Authorization":"Bearer r"}).json()["projects"][0]["id"],"DEMO-A")
with patch.dict(os.environ, {"XINGBANG_ASSISTANT_DEMO_ENABLED":"0"}):
self.assertEqual(self.get("/lab/source").status_code,403)
def test_navigation_keeps_legacy_without_four_agent_primary(self):
root = Path(__file__).resolve().parents[1]
nav = (root / "src/components/xingbang/AgentNav.tsx").read_text(encoding="utf-8")
self.assertNotIn("四 Agent 工作区",nav)
for label in ("运行中心","检查依据","演示与验收","兼容入口"):
self.assertIn(label,nav)
self.assertIn('href.includes("?")',nav)
if __name__ == "__main__":
unittest.main()