test: 补充 PEP 562 re-export 契约守护测试 (ce-code-review)

ce-code-review 9 个 reviewer 共识 finding:core/exceptions.py 的 PEP 562
__getattr__ re-export 契约无自动化守护。27 个调用方依赖
`from agentkit.core.exceptions import LLMProviderError`,但现有测试仅
顺带触发 happy path,未验证 is identity / isinstance 跨路径 / AttributeError /
__dir__ / 缓存行为。误删 __getattr__ 仅靠现有测试可能延迟发现。

新增 tests/unit/test_core_exceptions_reexport.py (6 tests) 锁定 5 个不变量:
1. 三条导入路径返回同一类对象 (is identity)
2. 跨路径 isinstance/except 捕获正常
3. 未知属性抛 AttributeError (PEP 562 契约)
4. __dir__() 包含 re-export 名字 (通配导入可发现)
5. __getattr__ 调用后缓存到模块 __dict__

finding 1 of 2 (P2, gated_auto, requires_verification=true)
This commit is contained in:
Chiguyong 2026-07-06 23:42:09 +08:00
parent a4d3f227a6
commit 7449458710
1 changed files with 81 additions and 0 deletions

View File

@ -0,0 +1,81 @@
"""PEP 562 re-export 契约守护测试。
core/exceptions.py 用模块级 __getattr__ 延迟 re-export LLM 异常类
打破 core.exceptions llm.exceptions 循环依赖27 个调用方依赖
`from agentkit.core.exceptions import LLMProviderError` 契约
本测试锁定以下不变量防止 __getattr__ 被误删或 re-export 断裂
1. 三条导入路径返回同一类对象is identity
2. 跨路径 isinstance/except 捕获正常
3. 未知属性抛 AttributeError
4. __dir__() 使通配导入能发现 re-export
5. __getattr__ 调用后缓存到模块 __dict__后续访问不再触发 lazy import
"""
import agentkit.core.exceptions as core_exc
import agentkit.llm.exceptions as llm_exc
from agentkit.core.exceptions import (
LLMError,
LLMProviderError,
ModelNotFoundError,
)
_REEXPORT_NAMES = ("LLMError", "LLMProviderError", "ModelNotFoundError")
def test_reexport_class_identity() -> None:
"""core.exceptions 的 LLM 异常类与 llm.exceptions 中的是同一对象。"""
assert core_exc.LLMError is llm_exc.LLMError
assert core_exc.LLMProviderError is llm_exc.LLMProviderError
assert core_exc.ModelNotFoundError is llm_exc.ModelNotFoundError
def test_reexport_isinstance_cross_path() -> None:
"""从 llm.exceptions raise 的异常能被 core.exceptions 的 except 捕获。"""
for exc_cls in (llm_exc.LLMError, llm_exc.LLMProviderError, llm_exc.ModelNotFoundError):
try:
raise exc_cls("test")
except LLMError:
pass # 所有三个都是 LLMError 子类或自身
else:
raise AssertionError(f"{exc_cls.__name__} 未被 core.exceptions.LLMError 捕获")
def test_getattr_raises_attributeerror_for_unknown() -> None:
"""访问不存在的属性抛 AttributeErrorPEP 562 契约)。"""
try:
core_exc.NonExistentException # noqa: B018
except AttributeError as e:
assert "NonExistentException" in str(e)
else:
raise AssertionError("预期 AttributeError 未抛出")
def test_dir_includes_reexport_names() -> None:
"""__dir__() 包含三个 re-export 名字,使 from ... import * 可发现。"""
dir_names = set(dir(core_exc))
for name in _REEXPORT_NAMES:
assert name in dir_names, f"{name} 不在 dir(core.exceptions) 中"
def test_getattr_caches_to_module_dict() -> None:
"""__getattr__ 调用后,类被缓存到模块 __dict__后续访问不再触发 lazy import。
白盒验证直接调用 __getattr__ 后检查 __dict__即使其他测试已触发缓存
本测试仍有效幂等再次调用 __getattr__ 会重新写入相同类对象
"""
result = core_exc.__getattr__("LLMError") # 显式触发 lazy import
assert result is llm_exc.LLMError
# 缓存后,三个名字都应存在于模块 __dict____getattr__ 一次性缓存全部)
for name in _REEXPORT_NAMES:
assert name in core_exc.__dict__, f"{name} 未被缓存到 core.exceptions.__dict__"
assert core_exc.__dict__[name] is getattr(llm_exc, name)
def test_llmprovider_error_inheritance_preserved() -> None:
"""re-export 不影响继承链LLMProviderError 仍是 LLMError 子类。"""
assert issubclass(LLMProviderError, LLMError)
assert issubclass(ModelNotFoundError, LLMError)
err = LLMProviderError(provider="openai", reason="timeout")
assert isinstance(err, LLMError)
assert err.provider == "openai"