# -*- coding: utf-8 -*- """杜邦区间窗口「完全越出可用月域」回归用例(2026-10-05 口径裁定 R1–R4 + M2)。 裁定件(财物侧口径,执行前必读): /srv/hermes/mailbox/finance/pending-tasks/_reports/project-20261005-dupont-oob-ruling-finance.md 施工单: project-20261005-dupont-oob-window-fix-fullstack.md 缺陷(改前实测,部署树 HEAD 3ed9fa0): · `?period=2026-Q4` → 200 / period_start=2026-10 > period_end=2026-09 / months=0 / label「2026年10-9月(季度)」/ 四因子全 None(**窗口倒挂 + 名号不符 + 静默空数据**); · `?period=2026-H2` → 裁至 2026-07..09(实 3 个月)却仍标「(半年)」。 裁定后口径(本文件逐条断言): · R1 整区间与可用月域 D 无交集 ⇒ `error`(端点转 400),**不返回任何窗口/数值字段**; · R2 有交集 ⇒ 窗口 = 交集(双向裁剪),`months` = 实际月数 ≥ 1(不倒挂); · R3 后缀((季度)/(半年)…)仅在「返回窗口 == 名义窗口」时保留 ⇒ 由 `clipped` 判; · R4 裁剪留痕:上界沿用既有文案;下界为对称文案; · M2 TTM 名义区间 = [anchor−11, anchor] = 12 个月。 可用月域取裁定 §2 场景:D = 2026-01 ~ 2026-09(latest=2026-09)。 本文件只测纯函数(`resolve_window` / `apply_period_suffix`),不连库、不写盘、不碰服务。 """ import pytest from app.utils import dupont_basis as _du from app.utils.dupont_basis import months_between, resolve_window def apply_period_suffix(label, period_key, clipped=False): """R3 判据入口(经模块属性取,便于负对照)。 改前件(origin/main 3ed9fa0)**没有** `apply_period_suffix` ⇒ 走显式断言失败 (AssertionError,而不是 import 期 ImportError 导致整个用例文件收集失败), 这样「把实现换回改前版本」时这些断言会以**可读的失败**呈现。 """ fn = getattr(_du, "apply_period_suffix", None) assert fn is not None, ("resolve_window/dupont_basis 未落地 R3 后缀纪律(clipped 判据)" "—— 改前件预期失败") return fn(label, period_key, clipped) #: 裁定 §2/§4 场景可用月域(latest = 2026-09,博海账套实测形态) D_AVAIL = [f"2026-{m:02d}" for m in range(1, 10)] #: 整区间无交集的统一 400 文案(端点直接把它作为 `detail`,前端零改动可正确呈现) Q4_DETAIL = "请求区间 2026-10~2026-12 与可用月域 2026-01~2026-09 无交集,无法计算区间指标" def rwd(period, start=None, end=None, **kw): kw.setdefault("today", "2026-10") return resolve_window(period, start, end, D_AVAIL, **kw) # ── R1:整区间与可用月域无交集 ⇒ 显式拒绝(不返回窗口/数值字段) ────────────── def test_q4_out_of_domain_rejected_with_exact_detail(): """`?period=2026-Q4`(A=[2026-10,2026-12] 与 D 无交集)⇒ error,文案逐字符合裁定 §2.1。""" w = rwd("2026-Q4") assert w.get("error") == Q4_DETAIL, w # 不得以 null/空串占位:窗口/月数/标签/裁剪留痕一律**不存在** for k in ("period_start", "period_end", "months", "months_label", "label", "period", "clip_note", "nominal_months", "clipped"): assert k not in w, (k, w) @pytest.mark.parametrize("period,start,end", [ ("2026-Q4", None, None), # 未来侧整季度越界 ("2026-10", None, None), # 未来侧单月越界 ("2027", None, None), # 未来侧整年越界 ("2025-Q1", None, None), # 历史侧整季度(域前) ("2025-Q4", None, None), # 历史侧整季度(域前) ("custom", "2026-10", "2026-12"), # custom 未来侧整段 ("custom", "2026-11", "2026-11"), # custom 未来侧单月(改前 months=-1) ("custom", "2025-01", "2025-12"), # custom 历史侧整段 ]) def test_all_no_intersection_forms_rejected(period, start, end): w = rwd(period, start, end) assert w.get("error"), (period, start, end, w) assert "无交集" in w["error"] assert "period_start" not in w and "months" not in w def test_nominal_inverted_custom_never_returns_inverted_window(): """名义区间自身颠倒(custom start > end)⇒ 拒绝,绝不产出倒挂窗口。""" w = rwd("custom", "2026-05", "2026-03") assert w.get("error"), w assert "period_start" not in w # ── R2 + R3 + R4:有交集 ⇒ 双向裁剪 + 月数/标签如实 ────────────────────────── def test_h2_clips_upper_bound_label_and_months(): """`?period=2026-H2` ⇒ 窗口 [2026-07,2026-09]、months=3、标签无「(半年)」、上界留痕。""" w = rwd("2026-H2") assert "error" not in w assert w["period_key"] == "half" # 请求形态不变(向后兼容) assert (w["period_start"], w["period_end"]) == ("2026-07", "2026-09") assert w["months"] == 3 assert w["label"] == "2026年7-9月" assert w["nominal_start"] == "2026-07" and w["nominal_end"] == "2026-12" assert w["nominal_months"] == 6 assert w["clipped"] is True assert w["clip_note"] == "上界 2026-12 超出可用月域(最新 2026-09),已裁至可用域" # R3:发生裁剪 ⇒ 去后缀(裁定 §2.2 期望标签「2026年7-9月」,不得带「(半年)」) assert apply_period_suffix(w["label"], w["period_key"], w["clipped"]) == "2026年7-9月" assert "半年" not in apply_period_suffix(w["label"], w["period_key"], w["clipped"]) def test_h2_window_equals_q3_window(): """强判据(裁定 §2.2):同一返回窗口 [2026-07,2026-09] ⇒ H2 与 Q3 窗口逐项相同。""" h2, q3 = rwd("2026-H2"), rwd("2026-Q3") assert "error" not in h2 and "error" not in q3 for k in ("period_start", "period_end", "months", "label"): assert h2[k] == q3[k], (k, h2[k], q3[k]) def test_full_year_clips_lower_upper_and_label(): """`?period=2026`(全年)⇒ [2026-01,2026-09]、months=9、标签无「(全年)」。""" w = rwd("2026") assert (w["period_start"], w["period_end"], w["months"]) == ("2026-01", "2026-09", 9) assert w["label"] == "2026年1-9月" assert w["clipped"] is True assert apply_period_suffix(w["label"], w["period_key"], w["clipped"]) == "2026年1-9月" def test_custom_lower_bound_clip_note_symmetric(): """R4:下界越界(域起点晚于名义起点)⇒ 裁到 avail_start + 对称留痕 + 去后缀。""" w = rwd("custom", "2025-01", "2026-06") assert (w["period_start"], w["period_end"], w["months"]) == ("2026-01", "2026-06", 6) assert w["clipped"] is True assert w["clip_note"] == "下界 2025-01 早于可用月域起点(2026-01),已裁至可用域" assert apply_period_suffix(w["label"], w["period_key"], w["clipped"]) == "2026年1-6月" def test_ttm_nominal_12_months_and_lower_clip(): """M2:TTM 名义 12 个月;域内 ⇒ 下界裁剪 + 去「(TTM 滚动 12 月)」后缀。""" nom = resolve_window("ttm", None, None, [f"2025-{m:02d}" for m in range(1, 13)] + [f"2026-{m:02d}" for m in range(1, 13)], anchor="2026-08") assert (nom["period_start"], nom["period_end"], nom["nominal_months"]) == ( "2025-09", "2026-08", 12) w = rwd("ttm", anchor="2026-08") assert (w["period_start"], w["period_end"], w["months"]) == ("2026-01", "2026-08", 8) assert w["clipped"] is True assert w["clip_note"] == "下界 2025-09 早于可用月域起点(2026-01),已裁至可用域" assert apply_period_suffix(w["label"], w["period_key"], w["clipped"]) == "2026年1-8月" def test_ytd_unclipped_keeps_suffix(): """未裁剪(返回窗口 == 名义窗口)⇒ 后缀保留(R3 的另一半)。""" w = rwd("ytd", anchor="2026-08") assert w["clipped"] is False assert w["clip_note"] is None assert apply_period_suffix(w["label"], w["period_key"], w["clipped"]) == "2026年1-8月(YTD 年初至今)" # ── R3 后缀纪律(纯函数) ───────────────────────────────────────────────────── @pytest.mark.parametrize("label,key,unclipped,clipped", [ ("2026年1-6月", "half", "2026年1-6月(半年)", "2026年1-6月"), ("2026年1-3月", "quarter", "2026年1-3月(季度)", "2026年1-3月"), ("2026年1-12月", "year", "2026年1-12月(全年)", "2026年1-12月"), ("2026年8月", "month", "2026年8月(单月)", "2026年8月"), ("2026年1-8月", "ytd", "2026年1-8月(YTD 年初至今)", "2026年1-8月"), ("2025年9月-2026年8月", "ttm", "2025年9月-2026年8月(TTM 滚动 12 月)", "2025年9月-2026年8月"), ("2026年6-9月", "custom", "2026年6-9月(自定义区间)", "2026年6-9月"), ]) def test_apply_period_suffix_discipline(label, key, unclipped, clipped): """R3:未裁剪 ⇒ 保留后缀;发生任何裁剪 ⇒ 去后缀。""" assert apply_period_suffix(label, key, False) == unclipped assert apply_period_suffix(label, key, True) == clipped # ── 全形态矩阵:任意入参下不得倒挂、months 与标签自洽 ──────────────────────── @pytest.mark.parametrize("period,start,end", [ ("2026-Q1", None, None), ("2026-Q3", None, None), ("2026-Q4", None, None), ("2026-H1", None, None), ("2026-H2", None, None), ("2026-08", None, None), ("2026-10", None, None), ("2026", None, None), ("2026-01", None, None), ("2027", None, None), ("2025-Q1", None, None), ("2025-Q4", None, None), ("ytd", None, None), ("ttm", None, None), ("custom", "2026-06", "2026-12"), ("custom", "2026-10", "2026-12"), ("custom", "2026-11", "2026-11"), ("custom", "2025-01", "2026-06"), ]) def test_matrix_no_inversion_and_self_consistency(period, start, end): w = rwd(period, start, end, anchor="2026-08") if w.get("error"): assert "无交集" in w["error"] or "无可用" in w["error"], w return assert w["period_start"] <= w["period_end"], w assert w["months"] == months_between(w["period_start"], w["period_end"]), w assert w["months"] >= 1, w assert D_AVAIL[0] <= w["period_start"] <= w["period_end"] <= D_AVAIL[-1], w assert w["clipped"] is (w["nominal_months"] != w["months"]), w def test_clip_note_present_iff_clipped(): for period, start, end in [("2026-Q3", None, None), ("2026-H2", None, None), ("2026", None, None), ("2025-Q1", None, None), ("custom", "2025-01", "2026-06")]: w = rwd(period, start, end) if w.get("error"): assert "clip_note" not in w continue assert (w["clip_note"] is not None) is w["clipped"], (period, w)