muse-agent-example/tests/架构/test_模块依赖.py

688 lines
30 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.

"""模块依赖行为验证。
保护:安装产物边界与依赖方向——业务只经公共接口、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={"甲": ["嗯"], "乙": ["嗯"]},
)