"""报告导出的接口:GET /api/runs/{run_id}/report.html。 照可验证报告的夹具真跑一次:两个虚构的 SQLite 数据源(门店流水没设遮罩,会员库遮了 phone 列),口径卡、 报告引用两份快照里的格子和一个指标。核对: - 导出的页面:正文是服务端渲染好的 HTML(数字是带 data-seg 的按钮、表格列表引用代码块都排出来), 不执行脚本也读得到;数据块能解析回来; - 出处链:数字的链里指标步骤带着口径(口径卡名、版本)、计算方式(原式、代入式)、输入和复算, 查询步骤带着 SQL 和被引用的行;同一处引用只算一条(同样的 [[m:gmv]] 共用); - 遮罩:会员库上选出了号码的那次查询,行里 phone 列写「已遮罩」; - 附件名是「<运行名>-报告.html」,中文名按 RFC 5987 编码; - ?report= 指定报告节点;没有报告文档的运行回 404; - 报告文档被改过(哈希对不上)回 409; - 只读:导出前后运行记录、事件一条不多一条不少。 """ from __future__ import annotations import json import re from urllib.parse import quote import pytest from httpx import ASGITransport, AsyncClient from langchain_core.messages import AIMessage from sqlalchemy import func, select from app.core.artifact_store import canonical_json from app.db.base import SessionLocal from app.db.models import Run, RunEvent from app.engine.runner import run_manager from app.main import app from tests.test_bundle_api import ( # noqa: F401 - 夹具和搭法照可验证报告的 CRM_SQL, SHOP_SQL, TEXT, client, engine_up, file_of, sealed_run, set_source, sources, wait, weekly, ) TWO = ("## 门店周报\n\n青石店流水 [[v:Q1.r0.amount]],会员消费 [[v:Q2.r1.spend]],口径卡销售额 [[m:gmv]]。" "另有 42 笔退款待入账。") RICH = (TWO + "\n\n- 青石店流水最高\n- 白沙店流水最低\n\n[[table:Q1 cols=store,amount rows=0-1]]") def data_of(html: str) -> dict: m = re.search(r'', html, re.S) assert m, "页面里没有数据块" return json.loads(m.group(1)) def seg_button(html: str, seg_id: str) -> str: """正文里这个片段的按钮(服务端渲染的)。""" m = re.search(rf'