"""模块依赖行为验证。 保护:安装产物边界与依赖方向——业务只经公共接口、Skill 不携带实现、 接入不直写表、共享基础不反向依赖业务、装配层不承载创作规则。 已实现子集: - 安装包在任意 cwd 可导入且导入不产生外部连接(含旧树同名包遮挡守卫); - 技能目录不注入 sys.path、不携带实现脚本; - src/muse 活跃运行代码不依赖外部专有 CLI; - 共享基础类型不导入业务模块;__main__ 只是薄命令入口。 依赖后续扩展的用例(宿主适配、协议、角色路由、旧库参数守卫、 技能目录合同)落地后逐例实现,不以形状检查代替。 """ from __future__ import annotations import ast import json import re import shlex import subprocess import sys from importlib.util import resolve_name from pathlib import Path import pytest import yaml import muse 仓库根 = Path(__file__).resolve().parents[2] 源码根 = 仓库根 / "src" / "muse" def _遍历py(根: Path) -> list[Path]: return sorted(p for p in 根.rglob("*.py") if "__pycache__" not in p.parts) def _导入名(文件: Path) -> set[str]: """AST 提取文件内全部 import 的顶层模块名。""" 树 = ast.parse(文件.read_text(encoding="utf-8")) 名单: set[str] = set() for 节点 in ast.walk(树): if isinstance(节点, ast.Import): for 别名 in 节点.names: 名单.add(别名.name.split(".")[0]) elif isinstance(节点, ast.ImportFrom) and 节点.module and 节点.level == 0: 名单.add(节点.module.split(".")[0]) return 名单 @pytest.mark.case_id( "TC-f4e7a41421c5", environment="离线,安装包与导入图", when="在无旧源码路径的环境装配模块并核对依赖。", contract="docs/系统架构/新版设计/总体架构.md", ) def test_skills_do_not_syspath_into_shared_implementations__f4e7a4() -> None: """技能目录支持自包含独立辅助脚本,禁止向 sys.path 注入或反向导入业务实现。""" 技能根 = 仓库根 / ".agent" / "skills" for p in 技能根.rglob("*.py"): if "__pycache__" in p.parts: continue 文本 = p.read_text(encoding="utf-8") assert "sys.path" not in 文本, f"技能脚本出现 sys.path 注入:{p.relative_to(仓库根)}" assert "import muse" not in 文本 and "from muse" not in 文本, ( f"技能脚本反向依赖业务单体:{p.relative_to(仓库根)}" ) for md in 技能根.rglob("*.md"): 文本 = md.read_text(encoding="utf-8") assert "sys.path" not in 文本, f"技能文档出现 sys.path 注入:{md.relative_to(仓库根)}" def _字符串常量(文件: Path) -> list[str]: """AST 提取全部字符串常量,排除模块/类/函数 docstring。""" 树 = ast.parse(文件.read_text(encoding="utf-8")) 文档: set[int] = set() for 节点 in ast.walk(树): if isinstance(节点, (ast.Module, ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)): 主体 = 节点.body if 主体 and isinstance(主体[0], ast.Expr) and isinstance(主体[0].value, ast.Constant): 文档.add(id(主体[0].value)) return [ 节点.value for 节点 in ast.walk(树) if isinstance(节点, ast.Constant) and isinstance(节点.value, str) and id(节点) not in 文档 ] @pytest.mark.case_id( "TC-ba6ad5c41f3c", environment="离线,安装包与导入图", when="在无旧源码路径的环境装配模块并核对依赖。", contract="docs/系统架构/新版设计/总体架构.md", ) def test_dashboard_does_not_import_skills__ba6ad5() -> None: """src/muse 不以代码依赖 .agent 技能目录(文档中的合同引用不算依赖)。""" for 文件 in _遍历py(源码根): for 值 in _字符串常量(文件): assert ".agent" not in 值, f"源码以路径方式引用 .agent:{文件.relative_to(仓库根)}" assert "skills/" not in 值, f"源码引用技能路径:{文件.relative_to(仓库根)}" @pytest.mark.case_id( "TC-1ec1b5f35798", environment="离线,安装包与导入图", when="在无旧源码路径的环境装配模块并核对依赖。", contract="docs/系统架构/新版设计/总体架构.md", ) def test_active_runtime_has_no_claude_cli_dependency__1ec1b5() -> None: """活跃运行代码不依赖专有外部 CLI(claude/dsh 等);子进程只允许工程工具。""" for 文件 in _遍历py(源码根): assert not _子进程违规(文件.read_text()), str(文件.relative_to(仓库根)) def _导入全名(代码: str, 模块: str) -> list[tuple[int, str]]: 包 = 模块.rsplit(".", 1)[0] 结果 = [] for 节点 in ast.walk(ast.parse(代码)): if isinstance(节点, ast.Import): 结果.extend((节点.lineno, 别名.name) for 别名 in 节点.names) elif isinstance(节点, ast.ImportFrom): 来源 = 节点.module or "" if 节点.level: 来源 = resolve_name("." * 节点.level + 来源, 包) 结果.extend((节点.lineno, f"{来源}.{别名.name}") for 别名 in 节点.names) return 结果 def _子进程违规(代码: str) -> list[str]: """识别子进程 API 的导入别名及静态命令;不会执行被扫描文件。""" 树 = ast.parse(代码) 别名, 赋值 = {}, {} for 节点 in ast.walk(树): if isinstance(节点, ast.Import): for 项 in 节点.names: 别名[项.asname or 项.name.split(".")[0]] = ( 项.name if 项.asname else 项.name.split(".")[0] ) elif isinstance(节点, ast.ImportFrom): for 项 in 节点.names: 别名[项.asname or 项.name] = f"{节点.module}.{项.name}" elif isinstance(节点, ast.Assign): for 目标 in 节点.targets: if isinstance(目标, ast.Name): 赋值[目标.id] = 节点.value def 全名(节点, 已见=frozenset()): if isinstance(节点, ast.Name): if 节点.id in 赋值 and 节点.id not in 已见: return 全名(赋值[节点.id], 已见 | {节点.id}) return 别名.get(节点.id, 节点.id) if isinstance(节点, ast.Attribute): return 全名(节点.value, 已见) + "." + 节点.attr return "" def 首项(节点, 已见=frozenset()): if isinstance(节点, ast.Name) and 节点.id in 赋值 and 节点.id not in 已见: return 首项(赋值[节点.id], 已见 | {节点.id}) if isinstance(节点, (ast.List, ast.Tuple)): return 首项(节点.elts[0], 已见) if 节点.elts else None if isinstance(节点, ast.Constant) and isinstance(节点.value, str): return 节点.value if isinstance(节点, ast.BinOp) and isinstance(节点.op, ast.Add): 左, 右 = 首项(节点.left, 已见), 首项(节点.right, 已见) return 左 + 右 if 左 is not None and 右 is not None else None return None API = {f"subprocess.{名}" for 名 in ("run", "Popen", "call", "check_call", "check_output")} API |= {"asyncio.create_subprocess_exec", "asyncio.create_subprocess_shell"} 错误 = [] for 节点 in ast.walk(树): if not isinstance(节点, ast.Call) or 全名(节点.func) not in API: continue 参数 = ( 节点.args[0] if 节点.args else next((k.value for k in 节点.keywords if k.arg in {"args", "program", "cmd"}), None) ) 命令 = 首项(参数) if 命令: 单词 = shlex.split(命令) if 单词 and Path(单词[0]).name.startswith(("claude", "dsh")): 错误.append(f"{节点.lineno}: 禁止专有CLI {Path(单词[0]).name}") return 错误 @pytest.mark.case_id( "NC-o04-subprocess-ast-forms", environment="离线,临时Python源文件", given="子进程run/Popen/异步创建及导入和赋值别名", when="静态检查禁止CLI的同一命令,不执行临时源", then=["直接调用、别名、异步及静态拼接均拒绝,合法工具不误报"], ) @pytest.mark.parametrize( "代码", [ "import subprocess; subprocess.run(['claude','-p'])", "import subprocess as p; p.Popen(('dsh','x'))", "from subprocess import run as launch; launch(args=['claude'])", "from subprocess import Popen as launch; launch(['/bin/claude'])", "import asyncio as a\nasync def f():\n await a.create_subprocess_exec('claude','-p')", "from asyncio import create_subprocess_exec as launch\n" "async def f():\n await launch('dsh')", "import subprocess as p; command=['clau'+'de']; launch=p.run; launch(command)", ], ids=[ "run", "popen-alias", "run-import", "popen-import", "async-alias", "async-import", "assigned-command", ], ) def test_子进程检查识别别名与异步(tmp_path, 代码): 文件 = tmp_path / "反例.py" 文件.write_text(代码) assert _子进程违规(文件.read_text()) assert not _子进程违规("import subprocess; subprocess.run(['git', 'status'])") assert not _子进程违规("class X: pass\nx=X(); x.run(['claude'])") @pytest.mark.case_id( "TC-5d7edda75476", environment="离线,安装包与导入图", when="在无旧源码路径的环境装配模块并核对依赖。", contract="docs/系统架构/新版设计/总体架构.md", ) def test_runtime_does_not_import_muse_business__5d7edd() -> None: """共享纯基础不反向导入业务;启动装配可以绑定具体实现。""" 业务模块 = { "作品规划", "故事世界", "资料研究", "知识方法", "正文写作", "审校修订", "作者经验", "交付连载", "上下文", "效果评测", } 基础文件 = list((源码根 / "共享").glob("*.py")) + [ 源码根 / "配置.py", 源码根 / "资源加载.py", ] assert 基础文件, "共享与装配文件缺失" for 文件 in 基础文件: 模块 = "muse." + ".".join(文件.relative_to(源码根).with_suffix("").parts) for _, 导入 in _导入全名(文件.read_text(), 模块): 部分 = 导入.split(".") assert not (len(部分) > 1 and 部分[0] == "muse" and 部分[1] in 业务模块), ( f"基础层导入业务模块:{文件.relative_to(仓库根)} -> {导入}" ) @pytest.mark.case_id( "TC-b141f1313b72", environment="离线,安装包与导入图", when="在无旧源码路径的环境装配模块并核对依赖。", contract="docs/系统架构/新版设计/总体架构.md", ) def test_dispatch_agent_task_is_thin_cli__b141f1() -> None: """python -m muse 是薄入口:只做参数解析与转交,无业务逻辑与连接。""" 文件 = 源码根 / "__main__.py" 文本 = 文件.read_text(encoding="utf-8") 导入 = _导入名(文件) assert 导入 <= {"__future__", "argparse", "sys", "pathlib", "muse"}, 导入 for 禁止 in ("socket", "psycopg", "http", "urllib", "subprocess"): assert 禁止 not in 文本, f"命令入口包含 {禁止}" # --------------------------------------------------------------------------- # 安装与导入边界 # --------------------------------------------------------------------------- @pytest.mark.case_id( "NC-import-any-cwd-offline", environment="离线,安装包与子进程", given="已安装的 muse 包与任意非仓库工作目录", when="在 / 目录下用安装的解释器导入 muse 全部顶层模块", then=["导入成功且期间任何 socket 连接尝试都会失败", "版本号与包身份一致"], contract="docs/系统架构/新版设计/文件设计/工程配置与部署.md", ) def test_import_from_any_cwd_without_external_connections__e0a1b2() -> None: """任意 cwd 导入 muse 全部顶层模块且不产生任何 socket 连接(安装验收)。""" 代码 = ( "import socket\n" "def _拒绝(self, 地址):\n" " raise AssertionError(f'导入期间尝试连接 {地址}')\n" "socket.socket.connect = _拒绝\n" "import muse, muse.启动, muse.配置, muse.资源加载, muse.共享\n" "print(muse.__version__)\n" ) 进程 = subprocess.run( [sys.executable, "-c", 代码], cwd="/", capture_output=True, text=True, timeout=60 ) assert 进程.returncode == 0, 进程.stderr assert 进程.stdout.strip() == muse.__version__ @pytest.mark.case_id( "NC-import-not-shadowed", environment="离线,安装包与子进程", given="仓库根存在旧实现目录 muse/ 与已安装的 src 布局新包", when="在仓库根解释器导入 muse", then=["muse.__file__ 解析到 src/muse,不被旧树同名目录遮挡"], contract="docs/系统架构/新版设计/文件设计/工程配置与部署.md", ) def test_import_resolves_to_new_package_not_old_tree__c1d2e3() -> None: """import muse 解析到 src/muse 安装包,不被旧树同名目录遮挡。""" 代码 = "import muse\nprint(muse.__file__)\n" 进程 = subprocess.run( [sys.executable, "-c", 代码], cwd=仓库根, capture_output=True, text=True, timeout=60, ) assert 进程.returncode == 0, 进程.stderr assert "src/muse" in 进程.stdout, 进程.stdout def _跨域导入(代码: str, 模块: str, 业务模块: set[str]) -> list[str]: """解析绝对/相对以及模块别名导入;装配入口由调用者单独排除。""" 本域 = 模块.split(".")[1] 错误 = [] for 行, 导入 in _导入全名(代码, 模块): 部分 = 导入.split(".") if len(部分) < 2 or 部分[0] != "muse" or 部分[1] == 本域: continue if 部分[1] in 业务模块 and (len(部分) < 3 or 部分[2] != "接口"): 错误.append(f"{行}: {导入}") if 本域 in 业务模块 and 部分[1] in {"编排", "接入"}: 错误.append(f"{行}: 业务反向依赖 {导入}") return 错误 @pytest.mark.case_id( "NC-w20-20d01a", environment="离线,真实源码与AST语法反例", given="实际源码树和各业务接口", when="扫描跨域依赖、存储导出及编排/接入游标调用", then=["跨模块只经公开接口,不反向依赖编排;不公开内部存储;编排和接入不操作私表"], contract="docs/系统架构/新版设计/总体架构.md", ) def test_跨模块仅经公共用例且编排不操作私表__20d01a() -> None: """公共类型与用例可导出;存储不能伪装成公开用例,接入/编排不能直写表。""" 业务模块 = {p.parent.name for p in 源码根.glob("*/接口.py")} - {"编排", "接入"} 违规 = [] for 文件 in _遍历py(源码根): 相对 = 文件.relative_to(源码根) if len(相对.parts) < 2: continue # 启动.py 负责选择具体实现,根部共享边界由独立规则保护。 模块 = "muse." + ".".join(相对.with_suffix("").parts) 代码 = 文件.read_text(encoding="utf-8") 违规.extend(f"{相对}:{项}" for 项 in _跨域导入(代码, 模块, 业务模块)) 树 = ast.parse(代码) if 相对.parts[0] in {"编排", "接入"}: for 节点 in ast.walk(树): if isinstance(节点, ast.Call) and isinstance(节点.func, ast.Attribute): if 节点.func.attr in {"execute", "executemany", "cursor"}: 违规.append(f"{相对}:{节点.lineno}: 接入/编排不能使用数据库游标") if 文件.name == "接口.py": 存储符号 = { 项.asname or 项.name for 节点 in ast.walk(树) if isinstance(节点, ast.ImportFrom) and (节点.module or "").endswith(".存储") for 项 in 节点.names } for 节点 in 树.body: if isinstance(节点, ast.Assign) and any( isinstance(目标, ast.Name) and 目标.id == "__all__" for 目标 in 节点.targets ): 泄露 = 存储符号 & set(ast.literal_eval(节点.value)) if 泄露: 违规.append(f"{相对}: 不能公开内部存储 {sorted(泄露)}") assert not 违规, "\n".join(违规) @pytest.mark.case_id( "NC-w20-20d02b", environment="离线,真实源码与AST语法反例", given="绝对、相对、模块别名和分层from导入变体", when="对合法公共用例和非法私有依赖进行同一检查", then=["私有依赖的变体一律拒绝,合法公开用例与本域内部导入不误报"], contract="docs/系统架构/新版设计/总体架构.md", ) def test_公共边界检查识别别名和相对导入__20d02b() -> None: """同一非法依赖的语法变体必须一致拒绝,合法公开入口与本域内部导入不误报。""" 业务 = {"正文写作", "任务运行"} for 代码 in ( "from muse.任务运行.模型 import 任务请求", "import muse.任务运行.存储 as 别名", "from ..任务运行.存储 import 任务存储", "from muse.任务运行 import 存储", "from muse import 任务运行", "from muse.编排.生成正文 import 发起生成正文", ): assert _跨域导入(代码, "muse.正文写作.接口", 业务), 代码 for 代码 in ( "from muse.任务运行.接口 import 任务请求", "from ..任务运行.接口 import 任务请求", "from muse.任务运行 import 接口", "import muse.任务运行.接口 as 任务", "from .存储 import 正文存储", ): assert not _跨域导入(代码, "muse.正文写作.接口", 业务), 代码 @pytest.mark.case_id( "NC-o04-dependency-fullnames", environment="离线AST依赖合同", given="深层相对导入、import别名、基础设施依赖公共业务类型", when="按模块依赖规则解析完整来源;共享禁止反向,基础设施允许公共接口", then=["真实模块全名不因别名或相对层级丢失", "基础设施只禁私有依赖,不增加无依据禁区"], ) def test_依赖解析保留全名与批准的基础设施边界(): assert _导入全名("from ...任务运行 import 存储 as s", "muse.正文写作.子包.服务") == [ (1, "muse.任务运行.存储") ] assert _导入全名("import muse.任务运行.接口 as task", "muse.共享.辅助") == [ (1, "muse.任务运行.接口") ] 业务 = {"任务运行", "资料研究"} for 代码 in ("from muse.任务运行.接口 import 模型请求", "from muse.资料研究 import 接口"): assert not _跨域导入(代码, "muse.基础设施.模型.客户端", 业务) assert _跨域导入("from muse.任务运行 import 存储", "muse.基础设施.模型.客户端", 业务) # --------------------------------------------------------------------------- # 作者自然语言入口与角色路由合同(原 AuthorInstructionContractTest) # --------------------------------------------------------------------------- 作者根 = 仓库根 / ".agent" / "作者" 场景根 = 作者根 / "场景" 标准索引表头 = ( "| 名称 | 相对地址 | 内容描述 | 使用场景 | 使用要求 |\n" "|------|----------|----------|----------|----------|" ) 场景清单 = ("定故事", "排故事", "塑人物", "写下一章", "改正文", "查作品") 技能链接 = re.compile(r"\[([^\]\n]+)\]\(([^)\n]+/SKILL\.md)\)") frontmatter名称 = re.compile(r"^name:\s*(.+?)\s*$", re.MULTILINE) # 已有新版实现并登记进运行资源包的技能:场景路由必须指向新版路径,不再回到旧 muse 树。 已替代技能 = { "完善故事基础设定": ".agent/skills/操作/完善故事基础设定/SKILL.md", "决定正文候选去留": ".agent/skills/操作/决定正文候选去留/SKILL.md", "确认规划候选": ".agent/skills/操作/确认规划候选/SKILL.md", } 退役技能别名 = { "生成下一章", "局部重写", "场景扩写", "正文润色", "作品定盘", "全书规划", "章级细纲", "全书解析", "章后抽取", "一致性检测", "质量评分", } def _章节(文本: str, 标题: str) -> str: 匹配 = re.search( rf"^## {re.escape(标题)}\s*$\n(?P<正文>.*?)(?=^## |\Z)", 文本, flags=re.MULTILINE | re.DOTALL, ) assert 匹配 is not None, f"缺少章节:## {标题}" return 匹配.group("正文") @pytest.mark.case_id( "TC-41d4cbb084d0", environment="离线,安装包与导入图", when="在无旧源码路径的环境装配模块并核对依赖。", contract="docs/系统架构/新版设计/总体架构.md", ) def test_author_layer_has_discoverable_structure__41d4cb() -> None: """作者层文件完整且各级索引可发现;路由不依赖技能自动发现。""" 期望 = { 作者根 / "目录.md", 作者根 / "指令.md", 作者根 / "原生中文写作.md", 场景根 / "目录.md", *(场景根 / f"{名称}.md" for 名称 in 场景清单), } 缺失 = sorted(str(p.relative_to(仓库根)) for p in 期望 if not p.is_file()) assert 缺失 == [], "作者指令层文件不完整:\n" + "\n".join(缺失) 根索引 = (仓库根 / ".agent" / "目录.md").read_text(encoding="utf-8") 项目入口 = (仓库根 / "AGENTS.md").read_text(encoding="utf-8") 作者索引 = (作者根 / "目录.md").read_text(encoding="utf-8") 场景索引 = (场景根 / "目录.md").read_text(encoding="utf-8") # 目录文件可携带导航元信息注记(父级目录派生行所需),表头从注记之后起算。 作者索引 = re.sub(r"\A(?:\n)+", "", 作者索引) 场景索引 = re.sub(r"\A(?:\n)+", "", 场景索引) assert "作者/目录.md" in 根索引 assert ".agent/作者/指令.md" in 项目入口 assert 作者索引.startswith(标准索引表头) assert 场景索引.startswith(标准索引表头) assert "[指令.md](指令.md)" in 作者索引 assert "[原生中文写作.md](原生中文写作.md)" in 作者索引 assert "[目录.md](场景/目录.md)" in 作者索引 for 场景 in 场景清单: assert f"[{场景}.md]({场景}.md)" in 场景索引 @pytest.mark.case_id( "TC-5e199d285247", environment="离线,安装包与导入图", when="在无旧源码路径的环境装配模块并核对依赖。", contract="docs/系统架构/新版设计/总体架构.md", ) def test_scenes_route_only_to_registered_chinese_skills__5e199d() -> None: """场景路由只指向真实存在且 frontmatter 名称一致的技能;已替代技能不再路由旧实现。""" 登记能力 = { Path(条目).as_posix() for 条目 in json.loads((仓库根 / "工具" / "资源登记.json").read_text(encoding="utf-8"))[ "能力文件" ] } for 场景 in 场景清单: 源 = 场景根 / f"{场景}.md" 路由章节 = _章节(源.read_text(encoding="utf-8"), "正式技能") for 标签, 原目标 in 技能链接.findall(路由章节): 目标 = (源.parent / 原目标).resolve() assert 目标.is_file(), f"{源}: 技能链接不存在:{原目标}" 匹配 = frontmatter名称.search(目标.read_text(encoding="utf-8")) assert 匹配 is not None, f"{目标}: 缺少 frontmatter name" assert 标签 == 匹配.group(1).strip(), f"{源}: 链接文字必须使用正式技能名" 相对 = 目标.relative_to(仓库根).as_posix() if 标签 in 已替代技能: assert 相对 == 已替代技能[标签], ( f"{源}: {标签} 已有新版实现,应路由 {已替代技能[标签]} 而不是 {相对}" ) 文本 = 目标.read_text(encoding="utf-8") 声明 = yaml.safe_load(文本.split("---", 2)[1]) if 文本.startswith("---\n") else {} if 声明.get("category") == "operation": assert 相对 in 登记能力, f"{源}: 未登记的运行操作技能:{相对}" assert 标签 not in 退役技能别名 # 作者层不是技能:不得出现作者入口冒充技能登记。 assert all(Path(路径).name != "目录.md" for 路径 in 登记能力) @pytest.mark.case_id( "TC-7239c72475e5", environment="离线,安装包与导入图", when="在无旧源码路径的环境装配模块并核对依赖。", contract="docs/系统架构/新版设计/总体架构.md", ) def test_interaction_and_native_chinese_contracts_are_complete__7239c7() -> None: """交互边界与原生中文写作合同的关键短语在作者层保持完整。""" 入口 = (作者根 / "指令.md").read_text(encoding="utf-8") for 必含 in ( "一轮只问一个关键问题", "二至三个中文选项", "采用", "修改", "丢弃", "Shadow", "Canonical", "只给正文", "主会话不代替角色", ): assert 必含 in 入口, f"作者指令缺少交互合同短语:{必含}" 写作 = (作者根 / "原生中文写作.md").read_text(encoding="utf-8") for 必含 in ( "中文语序", "具体名词、动作和关系优先", "作者声音、叙述者声音和角色声音", "不得凭空增加", "润色不得偷改情节事实", "扩写不得新开场景或改变落点", "重写不得越过作者指定范围", "精确诊断", ): assert 必含 in 写作, f"原生中文写作合同缺少:{必含}" 诊断场景 = (场景根 / "查作品.md").read_text(encoding="utf-8") 修改场景 = (场景根 / "改正文.md").read_text(encoding="utf-8") assert "只查不改" in 诊断场景 assert "作者指定范围" in 修改场景 @pytest.mark.case_id( "TC-4bead8f3ebfb", environment="离线,安装包与导入图", when="在无旧源码路径的环境装配模块并核对依赖。", contract="docs/系统架构/新版设计/总体架构.md", ) def test_role_prompts_route_with_registered_skill_names__4bead8() -> None: """五个角色文件与角色合同一致并登记进运行资源包;角色层不使用退役技能别名。""" 登记 = json.loads((仓库根 / "工具" / "资源登记.json").read_text(encoding="utf-8")) 能力 = set(登记["能力文件"]) 角色合同 = { "writer": "写手", "planner": "规划员", "extractor": "知识抽取员", "detector": "检测员", "judge": "评委", } for 角色ID, 名称 in 角色合同.items(): 相对 = f".agent/角色/{名称}.md" assert 相对 in 能力, f"角色未登记进运行资源包:{相对}" 文本 = (仓库根 / 相对).read_text(encoding="utf-8") 头部 = 文本.split("---")[1] assert f"id: {角色ID}" in 头部 assert f"name: {名称}" in 头部 assert "contract_version:" in 头部 for 别名 in 退役技能别名: assert f"`{别名}`" not in 文本, f"{相对}: 角色文件引用退役技能别名 {别名}" @pytest.mark.case_id( "TC-9c16ccc98756", environment="离线,安装包与导入图", when="在无旧源码路径的环境装配模块并核对依赖。", contract="docs/系统架构/新版设计/总体架构.md", ) @pytest.mark.安装包 def test_装配与结构入口在任意cwd解析安装产物__9c16cc() -> None: """按新版装配与结构入口验证安装产物;不固定旧 framework/runtime 目录或仓根层数。 旧三类 Studio 接入的可加载性由完整安装包业务入口验收承接;本用例只验证 当期装配入口、结构入口与任意 cwd 的安装产物可解析,不宣称未实现的 owner 已接通。 """ 代码 = ( "import socket\n" "def _拒绝(self, 地址):\n" " raise AssertionError(f'入口解析期间尝试连接 {地址}')\n" "socket.socket.connect = _拒绝\n" "from muse.启动 import 应用装配, 构建\n" "from muse.资源加载 import 加载清单, 核对资源\n" "清单 = 加载清单()\n" "assert 清单['发布身份'] and 清单['构建身份']\n" "assert 核对资源()['构建身份'] == 清单['构建身份']\n" "print(清单['发布身份'])\n" ) 进程 = subprocess.run( [sys.executable, "-c", 代码], cwd="/", capture_output=True, text=True, timeout=60 ) assert 进程.returncode == 0, 进程.stderr assert 进程.stdout.strip() @pytest.mark.case_id( "TC-254d1ec59a42", environment="离线,安装包与导入图", when="在无旧源码路径的环境装配模块并核对依赖。", contract="docs/系统架构/新版设计/总体架构.md", ) def test_声音归属不重复经B06合同拒绝__254d1e() -> None: """角色级“同时归属”不再由旧账本校验:B06 声音请求合同拒绝该输入。""" import pytest from pydantic import ValidationError from muse.审校修订.接口 import 声音提案请求, 声音样张引用 样张 = 声音样张引用("document", "chapter-1", 1, "a" * 64, "main") with pytest.raises(ValidationError): 声音提案请求( "synthetic:demo", 0, "voice", 1, {"叙事": "冷峻"}, (样张,), characters={"甲": ["嗯"], "乙": ["嗯"]}, ) with pytest.raises(ValidationError): 声音提案请求( "synthetic:demo", 0, "voice", 1, {"叙事": "冷峻"}, (样张,), untouchable_verbal_tics={"甲": ["嗯"], "乙": ["嗯"]}, )