From 9298278fbb78638d3153d130bec178d53f436ef2 Mon Sep 17 00:00:00 2001 From: ThearchyHelios Date: Wed, 21 Oct 2026 16:00:44 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=A4=E4=BB=BD=E9=A1=B5=E9=9D=A2=E8=84=9A?= =?UTF-8?q?=E6=9C=AC=E7=9A=84=E6=B5=8B=E8=AF=95=E8=B7=9F=E4=B8=8A=E6=96=B9?= =?UTF-8?q?=E6=B3=95=E5=8F=AB=E6=B3=95=E5=92=8C=E6=AF=94=E7=8E=87=E7=9A=84?= =?UTF-8?q?=E9=97=AD=E5=90=88=E6=A0=B8=E5=AF=B9=EF=BC=9A=E5=8F=AF=E9=AA=8C?= =?UTF-8?q?=E8=AF=81=E6=8A=A5=E5=91=8A=E6=A0=B8=E5=AF=B9=E9=A1=B5=E7=9A=84?= =?UTF-8?q?=E3=80=8C=E6=96=B9=E6=B3=95=E4=B8=8E=E7=89=88=E6=9C=AC=E3=80=8D?= =?UTF-8?q?=E5=86=99=E3=80=8C=E5=8F=98=E5=8C=96=E6=8B=86=E8=A7=A3=EF=BC=88?= =?UTF-8?q?=E5=8F=AF=E5=8A=A0=EF=BC=89=20=C2=B7=20decompose.additive@1?= =?UTF-8?q?=E3=80=8D=E3=80=8C=E5=8F=AF=E6=AF=94=E6=80=A7=E6=A3=80=E6=9F=A5?= =?UTF-8?q?=20=C2=B7=20comparable@1=E3=80=8D=EF=BC=88=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E7=AB=AF=E5=B8=A6=E6=9D=A5=E7=9A=84=E5=8F=AB=E6=B3=95=E5=8A=A0?= =?UTF-8?q?=20id=EF=BC=89=EF=BC=9B=E5=AF=BC=E5=87=BA=E9=A1=B5=E7=82=B9?= =?UTF-8?q?=E5=BC=80=E6=AF=94=E7=8E=87=E6=8B=86=E8=A7=A3=E7=9A=84=E5=88=A4?= =?UTF-8?q?=E5=AE=9A=E8=AF=AD=EF=BC=8C=E6=80=BB=E6=95=B0=E9=82=A3=E4=B8=80?= =?UTF-8?q?=E6=A0=BC=E5=86=99=E3=80=8C=E6=95=B4=E4=BD=93=E6=AF=94=E7=8E=87?= =?UTF-8?q?=E9=82=A3=E4=B8=80=E6=A0=BC=E3=80=8D=E3=80=81=E9=97=AD=E5=90=88?= =?UTF-8?q?=E6=A0=B8=E5=AF=B9=E5=86=99=E3=80=8C=E6=95=B4=E4=BD=93=E6=AF=94?= =?UTF-8?q?=E7=8E=87=20x=20=E4=B8=8E=E5=90=84=E6=AE=B5=E5=88=86=E5=AD=90?= =?UTF-8?q?=E4=B9=8B=E5=92=8C=20=C3=B7=20=E5=88=86=E6=AF=8D=E4=B9=8B?= =?UTF-8?q?=E5=92=8C=20y=20=E9=97=AD=E5=90=88=E3=80=8D=EF=BC=8C=E4=B8=8D?= =?UTF-8?q?=E5=86=99=E3=80=8C=E5=90=84=E6=AE=B5=E4=B9=8B=E5=92=8C=E3=80=8D?= =?UTF-8?q?=EF=BC=88=E6=AF=94=E7=8E=87=E6=8B=86=E8=A7=A3=E7=9A=84=E9=82=A3?= =?UTF-8?q?=E6=AC=A1=E5=B0=81=E5=AD=98=E8=BF=90=E8=A1=8C=E6=8A=BD=E6=88=90?= =?UTF-8?q?=20ratio=5Frun=EF=BC=8C=E8=AF=81=E6=8D=AE=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E5=92=8C=E9=A1=B5=E9=9D=A2=E4=B8=A4=E5=A4=84=E5=85=B1=E7=94=A8?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/tests/test_analysis_evidence_api.py | 15 ++++++++---- backend/tests/test_analysis_pages.py | 26 ++++++++++++++++++--- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/backend/tests/test_analysis_evidence_api.py b/backend/tests/test_analysis_evidence_api.py index 02bff472..c871a6ea 100644 --- a/backend/tests/test_analysis_evidence_api.py +++ b/backend/tests/test_analysis_evidence_api.py @@ -333,10 +333,8 @@ async def test_headline_inputs_show_the_closure(client): assert next(s for s in body["chain"] if s["step"] == "analysis_recompute")["status"] == "same" -async def test_a_ratio_headline_closes_against_the_overall_ratio(client): - """比率拆解给了整体比率的两格(headline):闭合核对对着结论里的整体比率,即各段分子之和 ÷ 分母之和(r_base_total、 - r_target_total),角色写「基期整体比率」「本期整体比率」,closure.mode 写明是比率(导出页照它说「各段分子之和 ÷ 分母 - 之和」,不说「各段之和」)。以前只认可加拆解的总数键,比率拆解的 headline 不显示闭合核对。""" +async def ratio_run() -> dict[str, Any]: + """一次封存的运行:按渠道拆退款率(比率拆解),给了整体比率的两格(headline,0.035 → 0.053),报告引用判定语。""" rates = {"columns": ["channel", "week", "refunds", "orders"], "column_types": {"channel": "text", "week": "text", "refunds": "number", "orders": "number"}, "rows": [["线上", "W41", 50, 1000], ["线上", "W42", 80, 1000], ["线下", "W41", 20, 1000], @@ -352,7 +350,14 @@ async def test_a_ratio_headline_closes_against_the_overall_ratio(client): query="Q2", by=["channel"], period="week", base="W41", target="W42", numerator="refunds", denominator="orders", label="退款率", headline={"base": "Q5.r0.rate", "target": "Q5.r1.rate"}) assert result["status"] == "ok" and result["verdict"]["code"] != "unclosed" - made = await sealed_run("[[a:A1]]。", result=result, queries=[rates, head]) + return await sealed_run("[[a:A1]]。", result=result, queries=[rates, head]) + + +async def test_a_ratio_headline_closes_against_the_overall_ratio(client): + """比率拆解给了整体比率的两格(headline):闭合核对对着结论里的整体比率,即各段分子之和 ÷ 分母之和(r_base_total、 + r_target_total),角色写「基期整体比率」「本期整体比率」,closure.mode 写明是比率(导出页照它说「各段分子之和 ÷ 分母 + 之和」,不说「各段之和」)。以前只认可加拆解的总数键,比率拆解的 headline 不显示闭合核对。""" + made = await ratio_run() body = (await client.get(f"/api/runs/{made['run']}/evidence/segments/{seg_id(made['doc'], 'a:A1')}")).json() inputs = {s["role"]: s for s in body["chain"] if s["step"] == "analysis_input"} assert inputs["headline_base"]["closure"] == {"mode": "ratio", "total_key": "r_base_total", "total": "0.035", diff --git a/backend/tests/test_analysis_pages.py b/backend/tests/test_analysis_pages.py index 7d058b76..6d086b2f 100644 --- a/backend/tests/test_analysis_pages.py +++ b/backend/tests/test_analysis_pages.py @@ -20,7 +20,9 @@ import app.bundle import app.report_export from app.main import app as asgi from tests.test_analysis_bundle import bundle_of, card_bundle -from tests.test_analysis_evidence_api import LAST_WEEK, SCOPE, TEXT, THIS_WEEK, comparability_card, sealed_run, seg_id +from tests.test_analysis_evidence_api import ( + LAST_WEEK, SCOPE, TEXT, THIS_WEEK, comparability_card, ratio_run, sealed_run, seg_id, +) NODE = shutil.which("node") pytestmark = pytest.mark.skipif(NODE is None, reason="没有 node,无法执行页面上的脚本") @@ -155,6 +157,23 @@ async def test_the_export_page_opens_the_analysis_steps_and_the_scope_numbers(): assert out["scope:A1"]["steps"] == opened["steps"] +async def test_the_export_page_speaks_of_the_overall_ratio_for_a_ratio_decomposition(): + """比率拆解的总数那一格是整体比率:导出页写「整体比率那一格」「整体比率 x 与各段分子之和 ÷ 分母之和 y 闭合」,不说 + 「各段之和」。可加拆解照旧(上一个用例的页面里没有「整体比率」)。""" + made = await ratio_run() + async with AsyncClient(transport=ASGITransport(app=asgi), base_url="http://test") as client: + html = (await client.get(f"/api/runs/{made['run']}/report.html")).text + data = re.search(r'', html, re.S).group(1) + phrase = seg_id(made["doc"], "a:A1") + out = run_node(RUN_RUNTIME, RUNTIME, {"data": data, "segs": [ + {"seg": phrase, "kind": "analysis", "state": "deterministic", "text": made["result"]["phrase"]}]}) + body = out[phrase]["body"] + for text in ("基期整体比率", "整体比率那一格", "整体比率 0.035 与各段分子之和 ÷ 分母之和 0.035 闭合", + "整体比率 0.053 与各段分子之和 ÷ 分母之和 0.053 闭合"): + assert text in body, text + assert "各段之和 0.0" not in body + + async def test_the_bundle_page_shows_the_analysis_detail_and_the_scope(): bundle, _, _ = bundle_of(doc_extra=SCOPE) out = run_node(RUN_VERIFIER, VERIFIER, {"data": json.dumps(bundle), "seal": bundle["meta"]["manifest_hash"]}) @@ -164,7 +183,8 @@ async def test_the_bundle_page_shows_the_analysis_detail_and_the_scope(): assert "没拆渠道" in scope and "这次做了 2 次差异检验,几次检验之间没有按次数校正" in scope phrase = next(text for text in out["opened"].values() if "出处分析结论 A1 的判定语" in text) assert "核对结果✓ 通过" in phrase and "分析结论里的值「销售额下降 194(-8.7%)" in phrase - assert "判定语销售额下降 194(-8.7%)" in phrase and "方法与版本decompose.additive@1" in phrase + # 方法写服务端带来的叫法和 id(和导出页同一个写法),不只写 id + assert "判定语销售额下降 194(-8.7%)" in phrase and "方法与版本变化拆解(可加) · decompose.additive@1" in phrase assert "输入数据 · Q1" in phrase # 输入的角色写叫法,不写 data assert "复算一致:按封存的输入重新计算,结论逐项相同(导出时服务端按封存的输入复算的结论;本页只核哈希" in phrase value = next(text for text in out["opened"].values() if "具名的数「top_share」" in text) @@ -196,7 +216,7 @@ def test_the_bundle_page_shows_the_comparability_in_the_metric_detail(): out = run_node(RUN_VERIFIER, VERIFIER, {"data": json.dumps(bundle), "seal": bundle["meta"]["manifest_hash"]}) assert out["verified"] == "" [detail] = out["opened"].values() - for text in ("两期可比性", "判定两期可比", "判定标准只差日期", "方法与版本comparable@1", "各项检查", "同口径", + for text in ("两期可比性", "判定两期可比", "判定标准只差日期", "方法与版本可比性检查 · comparable@1", "各项检查", "同口径", "复算一致:按封存的输入重新计算,结论逐项相同(导出时服务端按封存的输入复算的结论"): assert text in detail, text