muse-agent-example/tests/architecture/test_import_boundaries.py

177 lines
7.5 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.

#!/usr/bin/env python3
"""门禁:Skill 不得靠 sys.path 去别人的 scripts/ 或 humanization/src 拿实现;看板不得 import Skill。
被多个 Skill 或看板消费的实现装成顶层可安装包(muse-db / muse-llm / muse-embed /
muse-deai),调用方 import 已安装的包。角色执行模块 muse_role 随 muse-llm 安装。
拥有该实现的 Skill 自己的 scripts/ 不受限(CLI 与库同属一个能力域)。
扫描范围包含 `.agent/skills`、`framework`、`muse` 和当前运行时入口;测试、humanization/tests/tools/eval 不在范围内。
"""
from __future__ import annotations
import pathlib
import re
import unittest
def _find_project_root(start: pathlib.Path) -> pathlib.Path:
resolved = start.resolve()
for directory in (resolved, *resolved.parents):
if (directory / "AGENTS.md").is_file() and (directory / ".git").exists():
return directory
raise RuntimeError(f"无法从 {start} 向上找到项目根")
ROOT = _find_project_root(pathlib.Path(__file__))
SKILL_SCAN_ROOTS = (pathlib.Path(".agent") / "skills", pathlib.Path("muse"))
DASHBOARD_ROOTS = (pathlib.Path("muse") / "authority" / "studio" / "read",)
ACTIVE_RUNTIME_RELATIVE_ROOTS = (
pathlib.Path(".agent") / "skills",
pathlib.Path("framework"),
pathlib.Path("runtime"),
pathlib.Path("muse"),
pathlib.Path("muse") / "lifecycle" / "quality" / "harness",
pathlib.Path("muse") / "platform" / "llm",
pathlib.Path("muse") / "platform" / "embed",
pathlib.Path("docs") / "write-chapter",
)
# 框架派发接缝:允许直接调用 Agent 框架二进制的适配器文件。
# 新增框架适配器(codex/opencode…)必须在此登记,并保持独立于 Muse 业务。
FRAMEWORK_ADAPTER_ALLOWLIST = frozenset(
{"framework/adapters/pi/runner.py", "framework/adapters/dsh/runner.py"}
)
FORBIDDEN_CLAUDE_RUNTIME_TOKENS = (
"claude_runtime",
"muse-claude-runtime",
"execute-claude-task",
"claudeExecutablePath",
"claudeExecutableSha256",
"claudeCliVersion",
"CLAUDE_CODE_TMPDIR",
"CLAUDE_CONFIG_DIR",
"CLAUDE_CODE_OAUTH_TOKEN",
)
# (禁止注入的路径, 提供替代实现的包, 豁免的 Skill 目录名)
FORBIDDEN_PATHS = (
("humanization/src", "muse-deai(import deai)", None),
("访问数据库/scripts", "muse-db(from muse_db import connect)", "访问数据库"),
("调用内容模型/scripts", "muse-llm(import muse_llm)", "调用内容模型"),
("生成知识向量/scripts", "muse-embed(import muse_embed)", "生成知识向量"),
("建立作品声音档案/scripts", "muse-deai(deai.baseline / deai.load_db)",
"建立作品声音档案"),
)
def _path_pattern(path: str) -> re.Pattern[str]:
"""同时匹配裸路径与 pathlib 拼接形态:``a/b`` 与 ``"a" / "b"``。"""
head, tail = path.split("/")
return re.compile(rf"""{re.escape(head)}(?:/|["']\s*/\s*["']){re.escape(tail)}""")
class ImportBoundaryTest(unittest.TestCase):
def test_skills_do_not_syspath_into_shared_implementations(self):
for path, replacement, owner in FORBIDDEN_PATHS:
with self.subTest(path=path):
pattern = _path_pattern(path)
offenders = [
rel for rel in self._skill_files()
if not (owner and owner in pathlib.Path(rel).parts)
and pattern.search((ROOT / rel).read_text(encoding="utf-8"))
]
self.assertEqual(
offenders,
[],
f"Skill 必须消费 {replacement},不得 sys.path 指向 {path}:\n"
+ "\n".join(offenders),
)
def test_dashboard_does_not_import_skills(self):
offenders: list[str] = []
for relative_root in DASHBOARD_ROOTS:
for path in (ROOT / relative_root).rglob("*.py"):
if path.name.startswith("test_"):
continue
text = path.read_text(encoding="utf-8")
rel = str(path.relative_to(ROOT))
if re.search(r"sys\.path", text) and ".agent/skills" in text:
offenders.append(rel)
if re.search(r"^from db import|^import db\b", text, re.M):
offenders.append(rel)
if re.search(r"\b(muse_llm|muse_role|deai)\b", text):
offenders.append(rel)
self.assertEqual(
offenders,
[],
"看板只读共享运行时包,不得 sys.path 注入 Skill 或 import db.py:\n" + "\n".join(offenders),
)
def test_active_runtime_has_no_claude_cli_dependency(self):
offenders: list[str] = []
suffixes = {".py", ".json", ".yaml", ".yml", ".toml", ".md"}
for relative_root in ACTIVE_RUNTIME_RELATIVE_ROOTS:
root = ROOT / relative_root
for path in root.rglob("*"):
if (
not path.is_file()
or path.suffix not in suffixes
or "__pycache__" in path.parts
or "artifacts" in path.parts
):
continue
text = path.read_text(encoding="utf-8")
rel = path.relative_to(ROOT).as_posix()
if any(token in text for token in FORBIDDEN_CLAUDE_RUNTIME_TOKENS):
offenders.append(rel)
continue
# 框架适配器是被批准的唯一直接调用 Agent 框架二进制的位置(07 领域框架派发接缝);
# 其余任何位置 shell 调模型/框架 CLI 仍被阻断。
if rel in FRAMEWORK_ADAPTER_ALLOWLIST:
continue
if path.suffix == ".py" and re.search(
r"(?:/bin/claude|[\"']claude[\"']\s*,\s*[\"']--version|"
r"[\"'](?:pi|dsh)[\"']\s*,\s*[\"']--(?:model|profile|version)|"
r"\b(?:pi|dsh)\s+--(?:model|profile|version))",
text,
):
offenders.append(rel)
self.assertEqual(
sorted(set(offenders)),
[],
"活跃运行链不得 shell 调模型 CLI 或依赖旧 profile 字段:\n"
+ "\n".join(sorted(set(offenders))),
)
def test_active_runtime_has_no_embedded_api_token(self):
offenders: list[str] = []
token_pattern = re.compile(r"sk-[A-Za-z0-9]{20,}")
for relative_root in ACTIVE_RUNTIME_RELATIVE_ROOTS:
root = ROOT / relative_root
for path in root.rglob("*"):
if not path.is_file() or "__pycache__" in path.parts or "artifacts" in path.parts:
continue
try:
text = path.read_text(encoding="utf-8")
except UnicodeDecodeError:
continue
if token_pattern.search(text):
offenders.append(path.relative_to(ROOT).as_posix())
self.assertEqual(
offenders,
[],
"活跃运行链不得内嵌 API token:\n" + "\n".join(offenders),
)
def _skill_files(self) -> list[str]:
files: set[str] = set()
for relative_root in SKILL_SCAN_ROOTS:
scan_root = ROOT / relative_root
for skill_md in scan_root.rglob("SKILL.md"):
for path in skill_md.parent.rglob("*.py"):
files.add(path.relative_to(ROOT).as_posix())
return sorted(files)
if __name__ == "__main__":
unittest.main()