"""报告导出的接口: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'