4762358f54
CI / ai-gates (push) Failing after 29s
`api_key=volcengine.api_key` 这类**依赖注入传参**被 KEY-ASSIGN 判为疑似凭证。 这是规则的真缺口而非代码问题:行里没有任何秘密,而做 DI 的代码到处都是这种 写法——绕过一次就得在每个调用点绕过,那等于把规则废掉。故改规则。 新增 ALLOW_REFERENCE,刻意收得很紧以免开后门: - 值必须是**未加引号**的小写起首标识符引用(真密钥要么带引号,要么不是标识符形状) - 只允许**恰好一个点且随即行尾** - 连字符不在字符集内 对抗性夹具验证(三真三假): - 拦下 `api_key = "sk-T3Blbk…"`(OpenAI 形态) - 拦下 `secret_key: "AKID…"`(腾讯云形态) - 拦下 `auth_token=eyJ….eyJ….Sfl…`(**三段式 JWT——两个点,故不落进放行**) - 放行 `api_key=lowercase.reference` / `api_key = settings.api_key` / `api_token: config.authToken` 原 5 形态泄漏夹具复验仍 8 条命中,一条不少。 另更正口径:脚本头与 git-workflow.md 仍写「三仓同构」,实际已是四仓—— patbond-ai 是唯一持有 AI 服务商凭证的仓,漏同步它风险最大。四仓 md5 已一致 (4945940093b4b8413b7d3eec8bf6a5ec),四仓 --all 全绿。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
128 lines
5.0 KiB
Python
128 lines
5.0 KiB
Python
"""分层边界的测试。
|
||
|
||
这些断言不测业务,测的是**架构约束本身**。分层的价值全在「换供应商不动上层」,
|
||
而这个性质只要有人无意加一行 import 就会悄悄失效——没有测试守着,它迟早会失效。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import ast
|
||
import pathlib
|
||
|
||
APP = pathlib.Path(__file__).resolve().parent.parent / "app"
|
||
|
||
|
||
def imports_of(path: pathlib.Path) -> set[str]:
|
||
tree = ast.parse(path.read_text(encoding="utf-8"))
|
||
names: set[str] = set()
|
||
for node in ast.walk(tree):
|
||
if isinstance(node, ast.Import):
|
||
names.update(alias.name for alias in node.names)
|
||
elif isinstance(node, ast.ImportFrom) and node.module:
|
||
names.add(node.module)
|
||
return names
|
||
|
||
|
||
def modules_under(*parts: str) -> list[pathlib.Path]:
|
||
return sorted((APP.joinpath(*parts)).rglob("*.py"))
|
||
|
||
|
||
class TestDomainIsIndependent:
|
||
"""领域层是「换供应商不动上层」的支点。它一旦依赖厂商或 HTTP,分层就名存实亡。"""
|
||
|
||
def test_domain_imports_no_provider_code(self):
|
||
for module in modules_under("domain"):
|
||
offenders = {name for name in imports_of(module) if "providers" in name}
|
||
assert not offenders, f"{module.name} 不应依赖服务商代码:{offenders}"
|
||
|
||
def test_domain_imports_no_http_library(self):
|
||
for module in modules_under("domain"):
|
||
offenders = {
|
||
name
|
||
for name in imports_of(module)
|
||
if name.split(".")[0] in {"httpx", "requests"}
|
||
}
|
||
assert not offenders, f"{module.name} 不应依赖 HTTP 库:{offenders}"
|
||
|
||
def test_domain_does_not_read_configuration(self):
|
||
# 领域对象应当是纯数据;让它自己读配置会使单测必须摆弄环境变量。
|
||
for module in modules_under("domain"):
|
||
offenders = {
|
||
name for name in imports_of(module) if name in {"os", "app.config"}
|
||
}
|
||
assert not offenders, f"{module.name} 不应读取配置:{offenders}"
|
||
|
||
|
||
class TestAbstractionDoesNotLeak:
|
||
def test_provider_base_knows_no_specific_vendor(self):
|
||
"""抽象接口若 import 了火山,新增第二家供应商就得改抽象层。"""
|
||
offenders = {
|
||
name
|
||
for name in imports_of(APP / "providers" / "base.py")
|
||
if "volcengine" in name
|
||
}
|
||
assert not offenders, f"抽象层不应依赖具体服务商:{offenders}"
|
||
|
||
def test_registry_knows_no_specific_vendor(self):
|
||
offenders = {
|
||
name
|
||
for name in imports_of(APP / "providers" / "registry.py")
|
||
if "volcengine" in name
|
||
}
|
||
assert not offenders, f"注册表不应依赖具体服务商:{offenders}"
|
||
|
||
|
||
class TestTransportStaysBusinessAgnostic:
|
||
def test_ark_client_does_not_import_video_business(self):
|
||
"""传输层复用的前提是它不认识「视频」。
|
||
|
||
一旦 client.py 依赖了 video.py,接火山的图片接口时就没法复用它,
|
||
只能再抄一遍鉴权与错误解包——那正是拆分要避免的。
|
||
"""
|
||
offenders = {
|
||
name
|
||
for name in imports_of(APP / "providers" / "volcengine" / "client.py")
|
||
if name.endswith((".video", ".image")) or name.endswith("models")
|
||
}
|
||
assert not offenders, f"传输层不应依赖具体业务模块:{offenders}"
|
||
|
||
|
||
class TestVendorVocabularyIsContained:
|
||
def test_only_the_volcengine_package_mentions_ark_paths(self):
|
||
"""厂商特有的路径、字段名只应出现在其适配包内。
|
||
|
||
`framespersecond` 这种厂商拼法漏到上层,就意味着上层已经知道自己在跟
|
||
火山说话——换供应商时会牵连到它。
|
||
"""
|
||
vendor_markers = ("framespersecond", "contents/generations", "ark.cn-beijing")
|
||
for module in sorted(APP.rglob("*.py")):
|
||
if "volcengine" in module.parts:
|
||
continue
|
||
text = module.read_text(encoding="utf-8")
|
||
for marker in vendor_markers:
|
||
assert marker not in text, (
|
||
f"{module.relative_to(APP)} 出现了厂商专有词汇 {marker!r},"
|
||
f"应收拢到 providers/volcengine/ 内"
|
||
)
|
||
|
||
|
||
class TestDependencyDirectionIsOneWay:
|
||
"""配置 → 适配层,绝不反向。
|
||
|
||
`config.py` 作为装配根知道有哪家供应商是合理的;反过来,适配层一旦自己去读
|
||
配置,依赖注入就失效了——单测又得摆弄环境变量,而且同一进程内没法跑两套
|
||
不同配置的适配器实例。
|
||
"""
|
||
|
||
def test_providers_do_not_read_config(self):
|
||
for module in modules_under("providers"):
|
||
offenders = {
|
||
name
|
||
for name in imports_of(module)
|
||
if name == "app.config" or name == "os"
|
||
}
|
||
assert not offenders, (
|
||
f"{module.relative_to(APP)} 不应自行读取配置或环境变量:"
|
||
f"{offenders}。请通过构造参数注入。"
|
||
)
|