Files
PolyLoop/tests/unit/test_package.py
T
iomgaa e017160c45 build(repo): 落成工具链、依赖契约与十个模块的空骨架
第 ③ 阶段剩下的那半:架构文档之外,import-linter 契约也落地了。

pyproject.toml 把九条依赖规则里的七条写成五条 import-linter 契约。
分层用一条 layers 契约表达规则 1、2、3、8,`|` 表示同层互不 import;
另外四条 forbidden 分别管下游反向 import、polygateway 的唯一入口、
以及三个纯逻辑模块不碰 asyncio 与 pathlib。
契约不是平凡的绿:故意注入两处违规验证过,都被点名到行号。

先建十个模块的空包,是为了避开 PolyGateway bootstrap 期那段 Makefile 门控——
它当时没有包,lint-imports 报 module not found 而红,只好加一段跳过逻辑。
空包让契约从第一天就真的在跑。

剩下两条规则落不进契约,写成了 tests/unit 下的测试:
规则 6「types 与 ports 不许 import 任何第三方」判据要反过来写(只许标准库和自己),
规则 9「import polyloop 之后 sys.modules 里没有 polygateway」是运行时事实。
另加硬约束 §1.1 零业务假设的黑名单扫描——它第一次跑就抓到 _assembly 的 docstring
里写了 dissect 的业务词,已改。三个扫描类测试都带 fail-closed 守卫,
防止目录搬走之后扫到空列表安静地绿。

工程约定取自实验室已有项目:setuptools + src layout、ruff 十一项 select、
line-length 100 来自 PolyGateway;dev 工具链版本钉死、--strict-markers 与
--import-mode=importlib 来自 CHSAnalyzer 与 dissect 踩过的坑,各自的理由写在配置注释里。
e2e 默认不跑,它打真实网关要花钱。

CLAUDE.md §0 那句「契约还没写,要等 src/ 落地」已过期,改掉;
README 勾掉第 ②③ 阶段,补上本地检查命令与 GovDoc-Editor 那一行。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 21:28:59 -04:00

59 lines
2.3 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.
"""包基线:版本号双写一致,以及 import 本库不会把网关拉起来。
**这个文件读源码文本和 `pyproject.toml`,不是只跑被测代码。** 它仍然属于 unit,因为
分层判据是「依赖什么」(`CLAUDE.md` §1.9),而它不连任何外部服务。
"""
import subprocess
import sys
import tomllib
from pathlib import Path
import pytest
import polyloop
REPO_ROOT = Path(__file__).resolve().parents[2]
pytestmark = pytest.mark.unit
def test_version_matches_pyproject() -> None:
"""`__version__` 与 `pyproject.toml` 的 version 必须一致。
两处双写是刻意的(见 `polyloop/__init__.py`),代价就是会漂移,所以用这条断言钉住。
漂移的表现是下游报 bug 时说的版本号和实际装的不是一个,而那种错查起来要绕很远。
"""
declared = tomllib.loads((REPO_ROOT / "pyproject.toml").read_text(encoding="utf-8"))
assert polyloop.__version__ == declared["project"]["version"]
def test_importing_polyloop_does_not_import_polygateway() -> None:
"""依赖规则 9`import polyloop` 之后 `sys.modules` 里不许出现 `polygateway`。
这条写不成 import-linter 契约——它是运行时事实,不是静态图上的边。适配器模块允许
import 网关(规则 5),所以静态图上那条边合法;这里要禁的是**顶层 import 时就把它
拉起来**。一个顺手提供的默认模型客户端会让每个进程在 import 本库时,把网关连同它的
provider 目录一起加载。
在子进程里跑,因为本进程早就 import 过别的东西了,`sys.modules` 不干净。
"""
code = "import polyloop, sys; print('polygateway' in sys.modules)"
result = subprocess.run( # noqa: S603
[sys.executable, "-c", code],
capture_output=True,
text=True,
check=True,
cwd=REPO_ROOT,
)
assert result.stdout.strip() == "False", result.stdout
def test_py_typed_marker_ships_with_the_package() -> None:
"""`py.typed` 必须在包根里。
少了它,下游的类型检查器把整个包当成无注解的黑盒——而这个库对下游的承诺大半写在
签名里。这个文件是空的,很容易在某次移动目录时丢掉且不报错。
"""
assert (REPO_ROOT / "src" / "polyloop" / "py.typed").is_file()