"""Reproducible offline acceptance and latency evidence in an isolated database.""" import argparse import base64 import json import os import statistics import sys import tempfile import time from datetime import datetime from pathlib import Path ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / "backend")) def main(): parser = argparse.ArgumentParser() parser.add_argument("--output", type=Path, default=ROOT / "test-evidence" / ("assistant-acceptance-" + datetime.now().strftime("%Y%m%d-%H%M%S"))) args = parser.parse_args() output = args.output.resolve() if not output.is_relative_to(ROOT / "test-evidence"): raise ValueError("验收输出必须位于当前仓库 test-evidence") output.mkdir(parents=True, exist_ok=False) os.environ.update(XINGBANG_LOCAL_MODEL_URL="", XINGBANG_AUTH_USERS="", XINGBANG_REQUIRE_AUTH="0") from api import risk_agent_config as catalog from services.project_assistant import ProjectAssistantService from services.assistant_worker import DurableWorker from services.assistant_store import listing identity = {"id": "offline-acceptance", "role": "admin", "projects": ["*"], "preview": True} records = [] with tempfile.TemporaryDirectory(dir=output, ignore_cleanup_errors=True) as tmp: catalog.DB_PATH = Path(tmp) / "catalog.sqlite3" service = ProjectAssistantService(Path(tmp) / "assistant.sqlite3") worker = DurableWorker(service) def run(project_id): start = time.perf_counter() queued = service.enqueue(project_id, identity, "核对现金流与来源") queued_ms = (time.perf_counter() - start) * 1000 worker.process_next() result = service.resource("run", queued["id"]) records.append({"project": project_id, "run_id": result["id"], "enqueue_ms": round(queued_ms, 2), "total_ms": round((time.perf_counter() - start) * 1000, 2), "status": result["status"], "summary": result["summary"]}) return result first = run("DEMO-B") assert first["summary"]["coverage"] == "1.0800" and first["summary"]["open"] == 4 project = service.resource("project", "DEMO-B") service.supplement("DEMO-B", identity["id"], {"due_diligence_liabilities": "13200", "duplicate_asset_count": 0}, [], project["revision"], "离线验收补件") second = run("DEMO-B") assert len(second["change_summary"]["resolved"]) == 2 scenario = service.compare_scenario("DEMO-B", "4000", identity["id"]) assert scenario["scenario"]["coverage"] == "1.3500" and len(scenario["unresolved"]) == 1 feedback = service.feedback("DEMO-B", identity["id"], {"category": "rule_omission", "target_kind": "run", "target_id": second["id"], "note": "追加回款凭证检查"}) experience = service.save_experience("DEMO-B", identity["id"], {"title": "回款凭证检查", "trigger": "存在回款依赖", "action": "补充到账依据", "exceptions": "已有到账凭证", "scope": "team", "team": "demo", "feedback_id": feedback["id"], "conditions": {"collection.dependency": True}, "required_field": "collection.evidence", "exception_field": "collection.evidence", "expires_at": "2099-01-01T00:00:00+00:00"}) experience = service.experience_action("DEMO-B", experience["id"], identity, "evaluate", experience["revision"], "隔离正反例评测") service.experience_action("DEMO-B", experience["id"], identity, "publish", experience["revision"], "本机模拟独立审核") c = run("DEMO-C") a = run("DEMO-A") assert any(f["code"].startswith("experience.") for f in c["findings"]) assert not any(f["code"].startswith("experience.") for f in a["findings"]) reports = [] for template in ("template.due-diligence", "template.review", "template.committee"): report = service.create_report("DEMO-B", second["id"], identity["id"], template) target = output / (template + ".docx") target.write_bytes(service.export_report("DEMO-B", report["id"])) reports.append({"path": target.name, "template": template, "snapshot_id": report["snapshot_id"], "visual_qa": "pending-renderer"}) times = [] for _ in range(30): start = time.perf_counter() service.resource("run", second["id"]) times.append((time.perf_counter() - start) * 1000) with service.connect() as db: for document in listing(db, "document"): (output / document["name"]).write_bytes(base64.b64decode(document["raw_base64"])) summary = {"mode": "offline-deterministic-no-live-model", "records": records, "scenario": scenario, "poll_ms_p95": round(sorted(times)[28], 2), "poll_ms_median": round(statistics.median(times), 2), "reports": reports, "experience_positive_and_negative": "passed", "preserved_live_database": True} (output / "summary.json").write_text(json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8") print(json.dumps({"output": str(output), "mode": summary["mode"], "poll_ms_p95": summary["poll_ms_p95"], "records": records}, ensure_ascii=False, indent=2)) return 0 if __name__ == "__main__": raise SystemExit(main())