Files
group_fqcd_jr/tests/unit/tools/test_docs_endpoint_ids.py
T
lzf_0626 c868f01e67 修复 §19 端点编号检查的误报:按表格块识别,而不是"整章所有表格行"
## 问题

`test_real_document_has_no_duplicate_endpoint_ids` 失败,报 7 处"首列无法识别":
`**场外基金**`、`**场外运营**`、`**客户入驻**`、`**推广材料**`、`**访客令牌**`、
`**账户与交易**`、`段`。

查下去发现**文档没错、工具错了**:§19 里除端点总目录外,还有一张
「段 / 前缀 / 挂载来源 / 权限口径」的**说明表**(`docs/05` 行 1207 起)。它的首列是
分组名、表头是 `段` 而不是 `编号`。工具早期实现把**整章所有表格行**都当端点表扫,
于是那张说明表的 6 个分组名 + 1 个表头被报成结构异常。

**误报比漏报更伤**:这个脚本存在的意义是抓"编号被复用"(2026-09-12 真的发生过
`A034`/`A035` 各出现两次),可一旦它平时就在报噪声,真出问题时会被当成噪声忽略。

## 改法

`collect_rows` 改为**按表格块**识别:

- 表格块的第一行是表头;**只有表头首列是 `编号`** 的表格才是端点总目录;
- 其它表格整体跳过并**计数** —— 跳过几张表会打印出来,所以"忽略了什么"是可见的,
  不是静默的;
- 端点表**内部**形状不对的首列**仍然报出来** —— 那才是真正的结构异常。

于是"§19 覆盖"这行现在长这样:

    §19 覆盖:93 个端点编号 / 9 个号段(A×49、AD×11、C×7、K×4、M×4、O×3、P×2、R×4、T×9);跳过 1 张非端点表

跳过的表被显式写出来,覆盖范围仍然可核对 —— 这是原实现"不枚举前缀白名单、
把扫到的号段一并打印"那套自检思路的延续。

## 测试

`test_real_document_actually_scans_enough_endpoints` 因 `collect_rows` 返回值从
2 元组变 3 元组而需要同步;顺便**加了一条断言守住新行为**:
`skipped >= 1`,即"§19 的说明表确实被识别为独立表格块并跳过" —— 否则那几个分组名
会重新变成误报,而这条测试会先失败。

## 实测

- `python tools/check_docs_endpoint_ids.py` -> **exit 0**
  「93 个端点编号 / 9 个号段;跳过 1 张非端点表 / 端点编号无重复」
- `pytest tests/unit tests/contract` -> **1428 passed, 2 skipped, 1 failed**
  (剩下的 1 个是投顾工作台 `index.html` 被整体替换成自包含静态页所致,
  属产品决策,见前述说明)
- `ruff check` -> All checks passed
2026-09-14 20:29:54 +08:00

116 lines
5.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""`docs/05-接口文档.md` §19 端点编号唯一性(只读,不连库)。
规则与动机见 `tools/check_docs_endpoint_ids.py` 的模块说明:`check_authoritative_docs.py`
只校验 `docs/` 的**文件名编号**,不校验 §19 的**端点编号** —— 2026-09-12 两条线各自
新增端点时都占了 `A034`/`A035`,合并后同时存在两个 `A034` 与两个 `A035` 却仍然通过守卫。
这里额外守住两件事:
1. **不能"空扫通过"**:真实文档必须真的扫到足量编号,否则脚本坏掉了也会显示"无重复";
2. **覆盖范围要可见**:扫描不得依赖前缀白名单 —— 同一件事上曾因正则写成 `[AMKCS]`
而漏掉 `O`/`R` 两段,把 62 个端点报成 55 个;漏掉的号段一旦被复用,脚本仍会报
"重复 0"。所以覆盖报告必须列出每个号段的数量。
"""
from __future__ import annotations
import importlib.util
from pathlib import Path
from types import ModuleType
import pytest
PROJECT_ROOT = Path(__file__).resolve().parents[3]
#: 真实 §19 的规模下限:远小于实际数量,只为拦住"扫到 0 条也算通过"。
MIN_REAL_ENDPOINTS = 40
def _load_tool_module() -> ModuleType:
path = PROJECT_ROOT / "tools" / "check_docs_endpoint_ids.py"
spec = importlib.util.spec_from_file_location("check_docs_endpoint_ids", path)
assert spec is not None and spec.loader is not None
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def _write_doc(tmp_path: Path, body: str) -> Path:
doc = tmp_path / "05-接口文档.md"
doc.write_text(
"# 接口文档\n\n## 19. 接口总目录\n\n"
"| 编号 | 方法与路径 | 权限 | 幂等 | 成功状态 | 审计 |\n"
"|---|---|---|---|---|---|\n"
f"{body}\n\n## 20. 变更流程\n\n占位。\n",
encoding="utf-8",
)
return doc
def test_real_document_has_no_duplicate_endpoint_ids() -> None:
module = _load_tool_module()
assert module.collect_findings() == []
def test_real_document_actually_scans_enough_endpoints() -> None:
"""防止脚本坏掉后"什么都没扫到"却报通过。"""
module = _load_tool_module()
# 返回值是三元组:编号、无法识别的首列、跳过的非端点表数量
ids, _, skipped = module.collect_rows(module.read_section())
assert len(ids) >= MIN_REAL_ENDPOINTS
assert len(set(ids)) == len(ids)
# §19 除端点总目录外,还有一张「段 / 前缀 / 挂载来源 / 权限口径」的说明表。
# 它必须被识别为**非**端点表并跳过 —— 否则那 6 个分组名
# (`**场外基金**` 等)会被误报成"首列无法识别",而误报会让这个检查
# 失去可信度(真出问题时会被当成噪声忽略)。
assert skipped >= 1, "§19 的说明表没有被识别为独立表格块"
def test_duplicate_endpoint_id_is_detected(tmp_path: Path) -> None:
module = _load_tool_module()
doc = _write_doc(
tmp_path,
"| A034 | `POST /api/v1/auth/tokens` | 公开 | 否 | `200` | 否 |\n"
"| A034 | `GET /api/v1/admin/customer-profile-candidates` | `x:y` | 否 | `200` | 否 |\n",
)
findings = module.collect_findings(doc)
assert any("A034" in item and "重复" in item for item in findings)
def test_unrecognized_first_column_is_reported(tmp_path: Path) -> None:
module = _load_tool_module()
doc = _write_doc(tmp_path, "| A034(临时) | `GET /x` | `a:b` | 否 | `200` | 否 |\n")
findings = module.collect_findings(doc)
assert any("无法识别" in item for item in findings)
def test_coverage_report_lists_every_prefix_segment(tmp_path: Path) -> None:
"""覆盖报告必须暴露各号段数量 —— 漏扫一个前缀就会在这里显形。"""
module = _load_tool_module()
doc = _write_doc(
tmp_path,
"| A001 | `GET /a1` | `a:b` | 否 | `200` | 否 |\n"
"| A002 | `GET /a2` | `a:b` | 否 | `200` | 否 |\n"
"| M003 | `GET /m3` | `a:b` | 否 | `200` | 否 |\n"
"| O001 | `GET /o1` | `a:b` | 否 | `200` | 否 |\n"
"| R001 | `GET /r1` | `a:b` | 否 | `200` | 否 |\n",
)
report = module.describe_coverage(doc)
for expected in ("5 个端点编号", "4 个号段", "A×2", "M×1", "O×1", "R×1"):
assert expected in report, report
def test_missing_section_raises(tmp_path: Path) -> None:
module = _load_tool_module()
doc = tmp_path / "05-接口文档.md"
doc.write_text("# 接口文档\n\n## 1. 概述\n\n没有 §19。\n", encoding="utf-8")
with pytest.raises(module.DocumentStructureError):
module.collect_findings(doc)
def test_empty_checklist_prepares_no_false_success(tmp_path: Path) -> None:
"""§19 存在但一张表都没有时,必须报结构异常,而不是"无重复"。"""
module = _load_tool_module()
doc = _write_doc(tmp_path, "本节没有表格。")
findings = module.collect_findings(doc)
assert any("没有扫描到任何端点编号" in item for item in findings)