328 lines
9.4 KiB
Python
328 lines
9.4 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 "湖南 / 2025年数据 / 长沙理工大学 / 会计学" 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"])
|