Files
patbond-ai/tests/test_layering.py
T
lixi 4762358f54
CI / ai-gates (push) Failing after 29s
fix(security): 凭证扫描放行依赖注入传参 + 四仓口径更正
`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>
2026-09-20 12:18:18 +08:00

128 lines
5.0 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 就会悄悄失效——没有测试守着,它迟早会失效。
"""
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}。请通过构造参数注入。"
)