"""错误码一致性门禁:api-design §7 ↔ 异常树 error_code 的机器契约。 变更错误码必须两处同步修改:api-design.md §7 错误码表 与 src/genesis/inference/exceptions.py 的 error_code 类属性。 本测试防文档漂移:文档多/少/错一行都会变红,且失败信息给出差异清单。 """ from __future__ import annotations import re from pathlib import Path from genesis.inference.exceptions import LLMError # 项目根定位(与 tests/test_real_samples.py:7 同款先例;不依赖 CWD) API_DOC = Path(__file__).resolve().parents[1] / "docs/api-design.md" # §7 表格 LLM 行:| `CODE` | 语义 | ... | exceptions.Xxx | # 当前全文档仅 §7 含 exceptions. 行,此正则隐式限域;若他处新增类似行需加小节锚定。 _LLM_ROW_RE = re.compile( r"^\|\s*`([A-Z][A-Z_]+)`\s*\|.*\|\s*exceptions\.([A-Za-z_]+)\s*\|$" ) def _extract_llm_error_rows(document: str) -> dict[str, str]: """解析 §7 表格中来源列以 exceptions. 开头的行,返回 {错误码: 来源类名}。 非 LLM 行(来源列为 "—")被天然排除。 """ mapping: dict[str, str] = {} for line in document.splitlines(): m = _LLM_ROW_RE.match(line.strip()) if m: mapping[m.group(1)] = m.group(2) return mapping def _llm_exception_classes() -> list[type[LLMError]]: """error_code 非 None 的全部 LLM 异常子类(不硬编码清单,新增异常自动纳入)。""" return sorted( (cls for cls in LLMError.__subclasses__() if cls.error_code is not None), key=lambda cls: cls.__name__, ) def test_error_doc_lists_every_llm_error_code(): # 异常树 → 文档:每个子类 error_code 必须登记在 §7,且来源类名匹配 mapping = _extract_llm_error_rows(API_DOC.read_text(encoding="utf-8")) missing = { cls.__name__: cls.error_code for cls in _llm_exception_classes() if cls.error_code not in mapping or mapping[cls.error_code] != cls.__name__ } assert not missing, ( "异常类存在文档未登记或来源类名失配的 error_code(请同步 api-design.md §7):\n" + repr(missing) ) def test_error_doc_llm_rows_resolve_to_exception(): # 文档 → 异常树:每个 LLM 行都要能解析到同码同名的异常类 mapping = _extract_llm_error_rows(API_DOC.read_text(encoding="utf-8")) by_code = {cls.error_code: cls.__name__ for cls in _llm_exception_classes()} unresolved = {code: name for code, name in mapping.items() if by_code.get(code) != name} assert not unresolved, ( "§7 LLM 行解析不到对应异常类(请核对来源列或错误码名):\n" + repr(unresolved) ) def test_error_doc_stable_llm_row_count(): # 格式护栏:LLM 行数 === 4(与 _llm_exception_classes 子类数一致,现为 4) # 新增/删除 LLM 错误码需显式更新此数 mapping = _extract_llm_error_rows(API_DOC.read_text(encoding="utf-8")) assert len(mapping) == 4, ( f"api §7 LLM 错误码行数应为 4,实际 {len(mapping)};" "新增/删除错误码须同步更新此计数。" ) def test_error_doc_parse_guard_prevents_silent_noop(): # 格式护栏:解析结果为空须硬失败(表格格式变更会静默失效,这里立即暴露) mapping = _extract_llm_error_rows(API_DOC.read_text(encoding="utf-8")) assert mapping, "api §7 表格解析结果为空:若无匹配行,请检查表格格式与 _LLM_ROW_RE 是否一致。"