实现侧: - 上下文:任务范围拆分为 范围校验/范围授权;索引按可发现口径重建、索引新鲜度改对称差;依赖校验统一快照漂移说明。 - 知识方法:方法与材料读取口径统一;超限方法材料按可选省略,核对路径不再二次计费;删除无合同的读时重算。 - 任务运行:新增 context.usage/tool.denied 事件类型;连接池常驻并在装配生命周期内开关;调用结算与核对分列。 - 效果评测/审校修订/交付连载/作者经验/作品规划:凭据冻结、标定消费、导出补证、事实引文核对等收尾修复。 - 资源加载:能力正文不再夹带索引用的导航注记(该注记此前进入角色与技能的模型提示)。 - 元数据:受保护骨架与代码保护属性对齐;字段校验与内置结构口径同步。 - 基础设施:环境预检进入装配生命周期;数据库连接运行期字段不参与相等比较;索引指纹归一化 jsonb 浮点。 - 删除被替代实现:7 份旧提示词模板与空壳 资料来源 读取器。 用例侧: - 用例身份与导航元信息迁移;夹具补生命周期、同库暴露与模板封存; - 本轮定向修复:方法材料省略、事实引文、迁移回执、额度与暂停用例、慢用例超时预算等。
682 lines
30 KiB
Python
682 lines
30 KiB
Python
"""模块依赖行为验证。
|
||
|
||
保护:安装产物边界与依赖方向——业务只经公共接口、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"
|
||
脚本 = [p for p in 技能根.rglob("*.py") if "__pycache__" not in p.parts]
|
||
assert 脚本 == [], [str(p.relative_to(仓库根)) for p in 脚本]
|
||
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={"甲": ["嗯"], "乙": ["嗯"]},
|
||
)
|