"""`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, _ = module.collect_rows(module.read_section()) assert len(ids) >= MIN_REAL_ENDPOINTS assert len(set(ids)) == len(ids) 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)