From 50f16aece50cdabde9a128aa6737eb5385774317 Mon Sep 17 00:00:00 2001 From: ThearchyHelios Date: Tue, 6 Oct 2026 17:12:56 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8F=AF=E9=AA=8C=E8=AF=81=E6=8A=A5=E5=91=8A?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E6=B5=8B=E8=AF=95=EF=BC=9A=E7=9C=9F=E8=B7=91?= =?UTF-8?q?=E4=B8=80=E6=AC=A1=EF=BC=8C=E9=81=AE=E7=BD=A9=E5=BA=93=E4=B8=8A?= =?UTF-8?q?=E5=8F=AA=E6=B1=87=E6=80=BB=E6=B6=88=E8=B4=B9=E9=A2=9D=E7=9A=84?= =?UTF-8?q?=E6=9F=A5=E8=AF=A2=E5=BF=AB=E7=85=A7=E9=9A=8F=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E9=99=84=E5=B8=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 照证据测试的夹具真跑:会员库遮了 phone 列,另加一次只汇总消费额的查询(没碰号码列)。导出的页面里,数据源查询工具 真落的这份快照按原字节附带、和服务端的文件一致,它的查询事件带规范字符串,报告里引用它的数字和快照里那一格对得上; 选出了号码的那份照旧只给 id,页面里找不到号码。这条也守着「认得这份快照」那一关:查询工具以后往快照里加了键,这里 会先变红,得同时想清楚可验证报告该不该认它。在改动前的 build.py 上这条是红的。 --- backend/tests/test_bundle_api.py | 33 ++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/backend/tests/test_bundle_api.py b/backend/tests/test_bundle_api.py index bf383c08..90395059 100644 --- a/backend/tests/test_bundle_api.py +++ b/backend/tests/test_bundle_api.py @@ -5,7 +5,8 @@ - 只给已封存且封存核对一致的运行:没封存、封存后被改过都回 409; - 导出的数据里,事件行按浏览器的拼法重算等于运行记下的 manifest_hash; -- 遮罩库的快照只有 id 没有内容,它的查询事件只有哈希;运行之后才设的遮罩同样生效; +- 遮罩库上选出了号码的快照只有 id 没有内容,它的查询事件只有哈希;运行之后才设的遮罩同样生效;同一个库上只汇总消费额 + (没碰号码列)的那次查询,快照照常随文件附带; - 附件名是「<运行名>-可验证报告.html」,中文名按 RFC 5987 编码; - 只读:导出前后运行记录、事件一条不多一条不少。 @@ -91,11 +92,12 @@ def node(nid, ntype, **config): return {"id": nid, "type": ntype, "position": {"x": 0, "y": 0}, "data": {"label": nid, "config": config}} -def weekly(text_nodes=()): +def weekly(text_nodes=(), queries=()): nodes = [ node("start", "input"), node("fetch", "tool", tool="db_query__bundle_shop", args={"sql": SHOP_SQL}), node("members", "tool", tool="db_query__bundle_crm", args={"sql": CRM_SQL}), + *queries, node("card", "metrics", caliber="周报口径", caliber_version="v2", metrics=[ {"id": "gmv", "name": "销售额", "unit": "元", "decimals": 1, "format": "thousands", "expression": "cell(nodes.fetch, 0, 'amount')"}]), @@ -197,6 +199,33 @@ async def test_the_masked_source_ships_only_hashes(client, monkeypatch): assert "000-0001" not in json.dumps(bundle, ensure_ascii=False) +async def test_an_aggregate_on_the_masked_source_ships_its_snapshot(client, monkeypatch): + """会员库设了号码遮罩,另一次查询只汇总了消费额(没碰号码列):数据源查询工具真落的快照随文件附带,原字节和服务端的 + 文件一致,查询事件带规范字符串,报告里引用它的数字和快照里那一格对得上;选出了号码的那份照旧只给 id。""" + script(monkeypatch, "会员消费合计 [[v:Q3.r0.spend]],会员林一 [[v:Q2.r0.spend]]。[[see:Q3]]") + total = node("total", "tool", tool="db_query__bundle_crm", + args={"sql": "SELECT COUNT(*) AS members, SUM(spend) AS spend FROM members"}) + row = await wait((await run_manager.start(graph=weekly(queries=[total]), input_payload={})).id) + assert row.status == "succeeded", row.error + crm, summed = await query_artifact(row.id, "members"), await query_artifact(row.id, "total") + assert json.loads(file_of(summed).read_bytes())["mask_columns"] == ["phone"] + + bundle = data_of((await client.get(f"/api/runs/{row.id}/bundle.html")).text) + by_id = {a["id"]: a for a in bundle["artifacts"]} + assert base64.b64decode(by_id[summed]["b64"]) == file_of(summed).read_bytes() + assert by_id[crm] == {"id": crm, "kind": "query", "node_id": "members", "masked": True} + ends = {e["node_id"]: e for e in bundle["events"] if e["type"] == "tool.end"} + assert "canonical" in ends["total"] and "canonical" not in ends["members"] + + doc = json.loads(base64.b64decode(next(a["b64"] for a in bundle["artifacts"] if a["kind"] == "report_doc"))) + assert doc["catalog"]["Q3"]["artifact"] == summed + [seg] = [s for b in doc["blocks"] for u in b["units"] for s in u["segments"] + if (s.get("cite") or {}).get("alias") == "Q3"] + snapshot = json.loads(base64.b64decode(by_id[summed]["b64"])) + assert seg["cite"]["status"] == "resolved" and seg["cite"]["value"] == snapshot["rows"][0][1] == 508.5 + assert "000-0001" not in json.dumps(bundle, ensure_ascii=False) + + async def test_the_page_is_named_after_the_run(client, monkeypatch): row = await sealed_run(monkeypatch) resp = await client.get(f"/api/runs/{row.id}/bundle.html")