From bb38a47445afd041c92d8690669310699fd9de47 Mon Sep 17 00:00:00 2001 From: lhl Date: Sun, 9 Aug 2026 16:48:12 +0800 Subject: [PATCH] =?UTF-8?q?test:=20api=20=C2=A77=20=E4=B8=8E=E5=BC=82?= =?UTF-8?q?=E5=B8=B8=E6=A0=91=E9=94=99=E8=AF=AF=E7=A0=81=E4=B8=80=E8=87=B4?= =?UTF-8?q?=E6=80=A7=E9=97=A8=E7=A6=81=E6=B5=8B=E8=AF=95=EF=BC=88=E9=98=B2?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E6=BC=82=E7=A7=BB=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- _AI_USAGE_LOG.md | 1 + tests/test_api_design_consistency.py | 83 ++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 tests/test_api_design_consistency.py diff --git a/_AI_USAGE_LOG.md b/_AI_USAGE_LOG.md index b4a6654..e5dd31f 100644 --- a/_AI_USAGE_LOG.md +++ b/_AI_USAGE_LOG.md @@ -57,3 +57,4 @@ | 2026-08-09 14:02 | Agent 实现 | LLM 错误码对齐 Task3:engine 失败/parse_error 路径透传 error_code(与 error 同源取最后一次异常)。chat() 循环记 last_error_code=exc.error_code,失败返回 error_code=last_error_code;chat_structured() 三路径:JSONDecodeError 分支置 last_error_code="LLM_PARSE_ERROR"(解析耗尽→parse_error)、LLMError 分支 early return 带 error_code=exc.error_code、循环后 parse_error 用 last_error_code。TDD RED(5 failed,error_code=None)→ GREEN(聚焦 22 passed);pytest 全量 128 passed 覆盖 100.00%(803 stmts/188 br),fail_under=99 达标;提交见 git log | src/genesis/inference/engine.py, tests/test_inference_engine.py, _AI_USAGE_LOG.md | deepseek-v4-flash-free | | 2026-08-09 13:51 | Agent 实现 | LLM 错误码对齐 Task4:api-design §7 补 LLM_NETWORK_ERROR 行(对齐异常树 error_code)。在 docs/api-design.md §7 错误码表 LLM_TIMEOUT 行后插入新行 `| LLM_NETWORK_ERROR | 502 | 网络失败/5xx 重试耗尽 | retry / skip / abort | exceptions.LLMNetworkError |`,保留 LLM_NOT_CONFIGURED/LLM_PARSE_ERROR 与 INTERNAL_ERROR 兜底行不动;纯文档改动,全量回归 128 passed 覆盖 100.00%(803 stmts/188 br),fail_under=99 达标 | docs/api-design.md, _AI_USAGE_LOG.md | deepseek-v4-flash-free | | 2026-08-09 | 测试验证 | 里程碑3.1 LLM错误码对齐收尾改动(最终评审 Minor):① 删除 tests/test_inference_engine.py 中 test_chat_failed_error_code_not_configured 首行未使用变量 client = FakeLLMClient(...)(死代码,未引用,函数自洽性核查通过);② 补齐 src/genesis/inference/engine.py 文件末尾换行符(最后一字节 ) → \n)。覆盖测试 22 passed;pytest 全量 128 passed,覆盖 100.00%(803 stmts/188 br),fail_under=99 达标;提交见 git log | tests/test_inference_engine.py, src/genesis/inference/engine.py, _AI_USAGE_LOG.md | deepseek-v4-flash-free | +| 2026-08-09 | 测试验证 | 新增 api §7 ↔ 异常树 error_code 一致性门禁测试(防文档漂移)。新建 tests/test_api_design_consistency.py(4 用例:树→文档 / 文档→树双向断言 + 行数护栏 / 空解析护栏);首次运行 RED(行数护栏 ==5 与事实 4 冲突,文档与异常各为 4 条/4 个子类,双向一致)→ 修正护栏为 4 → GREEN(聚焦 4 passed);pytest 全量 132 passed 覆盖 100.00%(803 stmts/188 br),fail_under=99 达标;提交见 git log | tests/test_api_design_consistency.py, _AI_USAGE_LOG.md | deepseek-v4-flash-free | diff --git a/tests/test_api_design_consistency.py b/tests/test_api_design_consistency.py new file mode 100644 index 0000000..0945079 --- /dev/null +++ b/tests/test_api_design_consistency.py @@ -0,0 +1,83 @@ +"""错误码一致性门禁: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 | +_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 是否一致。" \ No newline at end of file