muse-agent-example/tests/architecture/test_import_boundaries.py
zizi 061010ba1b 框架: 共享运行时装成可安装包,切断 Skill 之间的 sys.path 互指
被多个 Skill 或看板消费的连接、模型、嵌入、Claude 运行时与声音账入口
各只保留一份实现;Skill 只留 CLI/落库,看板只读 muse-db,门禁锁死跨域注入。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-20 00:45:20 +08:00

82 lines
3.4 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-claude-runtime / muse-deai),调用方 import 已安装的包。拥有该实现的 Skill 自己的
scripts/ 不受限(CLI 与库同属一个能力域)。
扫描范围只含 .claude/skills 与 dashboard;测试、humanization/tests/tools/eval 不在范围内。
"""
from __future__ import annotations
import pathlib
import re
import unittest
ROOT = pathlib.Path(__file__).resolve().parents[2]
SKILLS = ROOT / ".claude" / "skills"
DASHBOARD = ROOT / "dashboard"
# (禁止注入的路径, 提供替代实现的包, 豁免的 Skill 目录名)
FORBIDDEN_PATHS = (
("humanization/src", "muse-deai(import deai)", None),
("access-database/scripts", "muse-db(from muse_db import connect)", "access-database"),
("call-content-model/scripts", "muse-llm(import muse_llm)", "call-content-model"),
("embed-knowledge/scripts", "muse-embed(import muse_embed)", "embed-knowledge"),
("execute-claude-task/scripts", "muse-claude-runtime(import claude_runtime)",
"execute-claude-task"),
("establish-voice-baseline/scripts", "muse-deai(deai.baseline / deai.load_db)",
"establish-voice-baseline"),
)
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)
prefix = f".claude/skills/{owner}/" if owner else None
offenders = [
rel for rel in self._skill_files()
if not (prefix and rel.startswith(prefix))
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 path in DASHBOARD.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 ".claude/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|claude_runtime|deai)\b", text):
offenders.append(rel)
self.assertEqual(
offenders,
[],
"看板只读共享运行时包,不得 sys.path 注入 Skill 或 import db.py:\n" + "\n".join(offenders),
)
def _skill_files(self) -> list[str]:
return sorted(p.relative_to(ROOT).as_posix() for p in SKILLS.rglob("*.py"))
if __name__ == "__main__":
unittest.main()