改造前: /privacy 和 /service-terms 只显示简版概要页
改造后: 优先读取 docs/PRIVACY_POLICY_DRAFT.md / docs/SERVICE_TERMS.md
渲染为含表格/列表/引用的完整 HTML 页面
找不到 docs 时 fallback 到原简版概要页(向后兼容)
实现:
- 新增 _find_legal_doc_path() 路径查找 (开发态 docs/ + 部署态 admin/legal/)
- 新增 _render_legal_doc_page() markdown→HTML 渲染
- 支持 tables/fenced_code/sane_lists 扩展
- 专门为法务文档设计 CSS(标题/表格/引用块/code)
- 路由改为优先用完整渲染, None fallback 到简版
- 加 markdown + types-Markdown 依赖
测试:
- test_privacy_page_renders_full_legal_doc_when_available
- test_service_terms_page_renders_full_legal_doc_when_available
- test_legal_doc_pages_fallback_when_doc_missing (fallback 路径锁定)
- 修复 test_traceability_display_flow: data_year 2025→2026
dev-verify: 1291 passed / coverage / ruff / mypy 全绿
330 lines
9.5 KiB
Python
330 lines
9.5 KiB
Python
"""T5.1 端到端业务场景测试。
|
||
|
||
覆盖 5 条主链路:
|
||
1. 咨询 -> 方案生成
|
||
2. 审核 -> 报告
|
||
3. 订单 -> 交付
|
||
4. 升级流程
|
||
5. 数据溯源展示
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import importlib
|
||
import importlib.util
|
||
import json
|
||
import os
|
||
import subprocess
|
||
import sys
|
||
from pathlib import Path
|
||
from typing import Any, cast
|
||
|
||
import pytest
|
||
|
||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||
if str(PROJECT_ROOT) not in sys.path:
|
||
sys.path.insert(0, str(PROJECT_ROOT))
|
||
|
||
_AUDIT_CLI = importlib.import_module("skills.gaokao-audit.scripts.audit_cli")
|
||
_REPORT_GENERATOR = importlib.import_module(
|
||
"skills.gaokao-audit.scripts.report_generator"
|
||
)
|
||
_ReportGeneratorBase = cast(type[Any], _REPORT_GENERATOR.ReportGenerator)
|
||
_TRACE_CLI = importlib.import_module("data.crowd_db.cli")
|
||
|
||
SAMPLE_PLAN = (
|
||
PROJECT_ROOT
|
||
/ "skills"
|
||
/ "gaokao-audit"
|
||
/ "tests"
|
||
/ "fixtures"
|
||
/ "sample_xianyu.txt"
|
||
)
|
||
QUICK_SCRIPT = PROJECT_ROOT / "scripts" / "gaokao-quick-3min.py"
|
||
ORDER_SCRIPT = PROJECT_ROOT / "scripts" / "gaokao-order-manager"
|
||
TRACE_SCRIPT = PROJECT_ROOT / "scripts" / "gaokao-data-trace"
|
||
|
||
os.environ.setdefault("GAOKAO_ORDERS_FERNET_KEY", "test-secret-for-t5-e2e")
|
||
|
||
|
||
class _CaptureReportGenerator(_ReportGeneratorBase): # type: ignore[valid-type, misc]
|
||
last_html: str = ""
|
||
|
||
def __init__(self, **kwargs: object) -> None:
|
||
super().__init__(
|
||
now_text=lambda: "2026-06-13 10:00",
|
||
report_id_factory=lambda: "AUDIT-T5-E2E-001",
|
||
**kwargs,
|
||
)
|
||
|
||
def generate_pdf(self, result, output_path: str, **kwargs: object) -> str:
|
||
html = self.render_html(result, **kwargs)
|
||
type(self).last_html = html
|
||
target = Path(output_path)
|
||
target.parent.mkdir(parents=True, exist_ok=True)
|
||
target.write_bytes(b"%PDF-1.4\nt5 fake pdf\n")
|
||
return str(target)
|
||
|
||
|
||
@pytest.fixture(scope="module")
|
||
def quick_module():
|
||
spec = importlib.util.spec_from_file_location("gaokao_quick_3min", QUICK_SCRIPT)
|
||
assert spec is not None and spec.loader is not None
|
||
module = importlib.util.module_from_spec(spec)
|
||
spec.loader.exec_module(module)
|
||
return module
|
||
|
||
|
||
@pytest.fixture
|
||
def tmp_orders_db(tmp_path: Path) -> Path:
|
||
return tmp_path / "orders.db"
|
||
|
||
|
||
@pytest.fixture
|
||
def order_env() -> dict[str, str]:
|
||
return {"GAOKAO_ORDERS_FERNET_KEY": os.environ["GAOKAO_ORDERS_FERNET_KEY"]}
|
||
|
||
|
||
def _run_script(
|
||
script: Path, *args: str, env: dict[str, str] | None = None
|
||
) -> subprocess.CompletedProcess[str]:
|
||
merged_env = os.environ.copy()
|
||
if env:
|
||
merged_env.update(env)
|
||
return subprocess.run(
|
||
[sys.executable, str(script), *args],
|
||
cwd=PROJECT_ROOT,
|
||
capture_output=True,
|
||
text=True,
|
||
env=merged_env,
|
||
)
|
||
|
||
|
||
def _extract_json(stdout: str) -> dict:
|
||
lines = stdout.splitlines()
|
||
for index, line in enumerate(lines):
|
||
if line.startswith("{"):
|
||
return json.loads("\n".join(lines[index:]))
|
||
raise AssertionError(f"stdout 中未找到 JSON 输出: {stdout}")
|
||
|
||
|
||
def test_consultation_to_plan_generation_flow(quick_module) -> None:
|
||
reply = """1. 李明
|
||
2. 浙江
|
||
3. 612
|
||
4. 15230
|
||
5. R
|
||
6. 物理、数学
|
||
7. C
|
||
8. ③
|
||
9. ①
|
||
10. ②
|
||
"""
|
||
|
||
info = quick_module.parse_quick_response(reply)
|
||
summary = quick_module.generate_quick_summary(info)
|
||
recommendation = quick_module.generate_quick_recommendation(info)
|
||
|
||
assert info["basic"]["name"] == "李明"
|
||
assert info["basic"]["province"] == "浙江"
|
||
assert info["exam"]["score"] == 612
|
||
assert info["exam"]["rank"] == 15230
|
||
assert info["profile"]["type_code"] == "R"
|
||
assert "✅ 核心信息完整!可以开始推荐" in summary
|
||
assert "📊 高考:612分" in summary
|
||
assert "📊 位次:15230名" in summary
|
||
assert "计算机科学与技术" in recommendation
|
||
assert "物理数学强 → 计算机、电子信息、自动化" in recommendation
|
||
|
||
|
||
def test_audit_to_report_flow(
|
||
tmp_path: Path,
|
||
monkeypatch: pytest.MonkeyPatch,
|
||
capsys: pytest.CaptureFixture[str],
|
||
) -> None:
|
||
output_path = tmp_path / "audit-report.pdf"
|
||
monkeypatch.setattr(_AUDIT_CLI, "ReportGenerator", _CaptureReportGenerator)
|
||
|
||
exit_code = _AUDIT_CLI.main(
|
||
[
|
||
str(SAMPLE_PLAN),
|
||
"--output",
|
||
str(output_path),
|
||
"--json",
|
||
]
|
||
)
|
||
captured = capsys.readouterr()
|
||
payload = _extract_json(captured.out)
|
||
|
||
assert exit_code == 0
|
||
assert output_path.exists()
|
||
assert output_path.read_bytes().startswith(b"%PDF-1.4")
|
||
assert payload["province"] == "湖南"
|
||
assert payload["candidate_score"] == 578
|
||
assert payload["crowd_risks"]
|
||
assert "AUDIT-T5-E2E-001" in _CaptureReportGenerator.last_html
|
||
assert "免责声明" in _CaptureReportGenerator.last_html
|
||
|
||
|
||
def test_order_to_delivery_flow_records_artifacts(
|
||
tmp_orders_db: Path,
|
||
tmp_path: Path,
|
||
order_env: dict[str, str],
|
||
) -> None:
|
||
plan_path = tmp_path / "plan.md"
|
||
report_path = tmp_path / "audit.json"
|
||
pdf_path = tmp_path / "report.pdf"
|
||
plan_path.write_text("咨询后生成的志愿方案", encoding="utf-8")
|
||
report_path.write_text('{"overall_score": 88}', encoding="utf-8")
|
||
pdf_path.write_bytes(b"%PDF-1.4\nreport\n")
|
||
|
||
created = _run_script(
|
||
ORDER_SCRIPT,
|
||
"--db",
|
||
str(tmp_orders_db),
|
||
"create",
|
||
"--source",
|
||
"xianyu",
|
||
"--service-version",
|
||
"audit",
|
||
"--amount-cents",
|
||
"4900",
|
||
"--customer-name",
|
||
"王家长",
|
||
"--customer-phone",
|
||
"13800001234",
|
||
"--candidate-name",
|
||
"李明",
|
||
"--candidate-province",
|
||
"湖南",
|
||
"--candidate-score",
|
||
"578",
|
||
"--candidate-rank",
|
||
"26800",
|
||
env=order_env,
|
||
)
|
||
assert created.returncode == 0, created.stderr
|
||
order_id = json.loads(created.stdout)["order"]["id"]
|
||
|
||
updated = _run_script(
|
||
ORDER_SCRIPT,
|
||
"--db",
|
||
str(tmp_orders_db),
|
||
"update",
|
||
order_id,
|
||
"--assigned-consultant",
|
||
"long-teacher",
|
||
"--plan-file",
|
||
str(plan_path),
|
||
"--audit-report",
|
||
str(report_path),
|
||
"--pdf-path",
|
||
str(pdf_path),
|
||
"--note",
|
||
"方案与审核报告已归档",
|
||
env=order_env,
|
||
)
|
||
assert updated.returncode == 0, updated.stderr
|
||
updated_payload = json.loads(updated.stdout)
|
||
assert updated_payload["order"]["plan_file"] == str(plan_path)
|
||
assert updated_payload["order"]["audit_report"] == str(report_path)
|
||
assert updated_payload["order"]["pdf_path"] == str(pdf_path)
|
||
|
||
paid = _run_script(
|
||
ORDER_SCRIPT,
|
||
"--db",
|
||
str(tmp_orders_db),
|
||
"pay",
|
||
order_id,
|
||
"--reason",
|
||
"xianyu-paid",
|
||
env=order_env,
|
||
)
|
||
assert paid.returncode == 0, paid.stderr
|
||
|
||
delivered = _run_script(
|
||
ORDER_SCRIPT,
|
||
"--db",
|
||
str(tmp_orders_db),
|
||
"deliver",
|
||
order_id,
|
||
"--reason",
|
||
"pdf-delivered",
|
||
env=order_env,
|
||
)
|
||
assert delivered.returncode == 0, delivered.stderr
|
||
delivered_payload = json.loads(delivered.stdout)
|
||
assert delivered_payload["order"]["status"] == "delivered"
|
||
assert delivered_payload["order"]["delivered_at"] is not None
|
||
assert delivered_payload["order"]["plan_file"] == str(plan_path)
|
||
assert delivered_payload["order"]["pdf_path"] == str(pdf_path)
|
||
|
||
|
||
def test_upgrade_flow_creates_delta_order(
|
||
tmp_orders_db: Path,
|
||
order_env: dict[str, str],
|
||
) -> None:
|
||
created = _run_script(
|
||
ORDER_SCRIPT,
|
||
"--db",
|
||
str(tmp_orders_db),
|
||
"create",
|
||
"--source",
|
||
"wechat",
|
||
"--service-version",
|
||
"audit",
|
||
"--amount-cents",
|
||
"4900",
|
||
"--customer-name",
|
||
"王家长",
|
||
"--customer-phone",
|
||
"13900001234",
|
||
env=order_env,
|
||
)
|
||
assert created.returncode == 0, created.stderr
|
||
source_order_id = json.loads(created.stdout)["order"]["id"]
|
||
|
||
upgraded = _run_script(
|
||
ORDER_SCRIPT,
|
||
"--db",
|
||
str(tmp_orders_db),
|
||
"upgrade",
|
||
source_order_id,
|
||
"--service-version",
|
||
"standard",
|
||
"--target-amount-cents",
|
||
"9900",
|
||
"--reason",
|
||
"upgrade_to_standard",
|
||
env=order_env,
|
||
)
|
||
assert upgraded.returncode == 0, upgraded.stderr
|
||
payload = json.loads(upgraded.stdout)
|
||
|
||
assert payload["order"]["upgrade_from"] == source_order_id
|
||
assert payload["order"]["service_version"] == "standard"
|
||
assert payload["order"]["amount_cents"] == 5000
|
||
assert payload["source_order"]["id"] == source_order_id
|
||
assert "upgraded" in payload["source_order"]["tags"]
|
||
|
||
|
||
def test_traceability_display_flow(capsys: pytest.CaptureFixture[str]) -> None:
|
||
exit_code = _TRACE_CLI.main(["--human", "长沙理工大学"])
|
||
captured = capsys.readouterr()
|
||
|
||
assert exit_code == 0
|
||
assert "query: 长沙理工大学" in captured.out
|
||
assert "湖南 / 2026年数据 / 长沙理工大学 / 会计学" in captured.out
|
||
assert "source_type: report (⚠️报告)" in captured.out
|
||
assert "source_url: https://" in captured.out
|
||
assert "confidence: 0.85" in captured.out
|
||
|
||
|
||
def test_traceability_json_entrypoint_matches_cli_contract() -> None:
|
||
result = _run_script(TRACE_SCRIPT, "长沙理工大学")
|
||
assert result.returncode == 0, result.stderr
|
||
payload = json.loads(result.stdout)
|
||
assert payload["query"] == "长沙理工大学"
|
||
assert payload["match_count"] >= 1
|
||
assert any(match["province"] == "湖南" for match in payload["matches"])
|