muse-agent-example/tests/架构/test_技能与文档索引.py
zizi b86516505f W02 安装工程、启动骨架与验证环境:pyproject/uv 安装工程、启动装配骨架、conftest 用例身份与 OS 离线隔离。
按 R2 串行阶段整理提交;包内文件为该阶段交付(含后续小增量),状态以工作包清单为准。
2026-09-10 19:25:40 +08:00

424 lines
18 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.md 完整、名称唯一且全中文、frontmatter 一致)
与活跃文档索引(目录.md 标准索引、链接可达)。
已实现子集(W02):目录与链接检查、技能目录基础合同。
依赖技能承接(W13+)、角色目录(W06)与旧库参数守卫(W03/W07)的用例
在对应包落地后逐例实现。
"""
from __future__ import annotations
import sys
import unicodedata
from pathlib import Path
import pytest
仓库根 = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(仓库根 / "工具"))
from 维护索引 import 检查技能目录, 检查目录链接 # noqa: E402
def _写能力样本(根: Path, 相对: str, 身份: str, 名称: str) -> Path:
import yaml
文件 = 根 / 相对
文件.parent.mkdir(parents=True, exist_ok=True)
声明 = {
"id": 身份,
"name": 名称,
"category": "method",
"contract_version": 1,
"description": "合成方法",
}
文件.write_text(
"---\n" + yaml.safe_dump(声明, allow_unicode=True) + "---\n\n只提出有依据的规划方法。\n"
)
return 文件
def test_passing_project_is_independent_of_current_repository__a189e4(tmp_path, monkeypatch):
from 构建资源包 import 编译能力资源
根 = tmp_path / "独立项目"
文件 = _写能力样本(根, ".agent/skills/alpha/SKILL.md", "alpha", "合成方法")
monkeypatch.chdir(tmp_path)
目录, 资源 = 编译能力资源(根, [文件.relative_to(根).as_posix()], set())
assert set(目录["method"]) == {"alpha"}
assert len(资源) == 1 and 资源["能力/method/alpha.md"] == 文件.read_bytes()
def test_nested_skill_path_is_accepted_from_manifest__c9cb85(tmp_path):
from 构建资源包 import 编译能力资源
相对 = ".agent/skills/planning/alpha/SKILL.md"
文件 = _写能力样本(tmp_path, 相对, "alpha", "合成方法")
目录, 资源 = 编译能力资源(tmp_path, [相对], set())
assert 目录["method"]["alpha"]["resource_path"] == "能力/method/alpha.md"
assert 资源["能力/method/alpha.md"] == 文件.read_bytes()
def test_chinese_only_policy_rejects_english_manifest_name__c089ce(tmp_path):
from 构建资源包 import 编译能力资源
_写能力样本(tmp_path, "SKILL.md", "alpha", "alpha")
with pytest.raises(ValueError, match="中文名称"):
编译能力资源(tmp_path, ["SKILL.md"], set())
def test_chinese_only_policy_allows_new_native_chinese_skill__b72bcc(tmp_path):
from 构建资源包 import 编译能力资源
a = _写能力样本(tmp_path, "规划/规划作品/SKILL.md", "plan-work", "规划作品")
b = _写能力样本(tmp_path, "写作/新增技能/SKILL.md", "new-method", "新增技能")
目录, 资源 = 编译能力资源(tmp_path, [p.relative_to(tmp_path).as_posix() for p in (a, b)], set())
assert {v["name"] for v in 目录["method"].values()} == {"规划作品", "新增技能"}
assert len(资源) == 2
def test_non_normalized_skill_name_is_rejected__5b50cc(tmp_path):
from 构建资源包 import 编译能力资源
_写能力样本(tmp_path, "SKILL.md", "alpha", "e\u0301-name")
with pytest.raises(ValueError, match="中文名称"):
编译能力资源(tmp_path, ["SKILL.md"], set())
def test_skill_name_with_decorative_punctuation_is_rejected__984f56(tmp_path):
from 构建资源包 import 编译能力资源
_写能力样本(tmp_path, "SKILL.md", "story", "故事结构!")
with pytest.raises(ValueError, match="中文名称"):
编译能力资源(tmp_path, ["SKILL.md"], set())
def test_资源按稳定身份编译且移动不改变身份__a66001(tmp_path):
from 构建资源包 import 编译能力资源
文件 = tmp_path / "初始.md"
文件.write_text(
"---\nid: writer\nname: 写手\ncategory: role\ncontract_version: 1\n"
"description: 合成职责\n---\n\n只执行固定任务。\n"
)
目录, 资源 = 编译能力资源(tmp_path, ["初始.md"], set())
assert 目录["role"]["writer"]["name"] == "写手"
assert 资源[目录["role"]["writer"]["resource_path"]] == 文件.read_bytes()
文件.rename(tmp_path / "迁移.md")
新目录, _ = 编译能力资源(tmp_path, ["迁移.md"], set())
assert 新目录["role"]["writer"]["sha256"] == 目录["role"]["writer"]["sha256"]
def test_技能只声明已实现命令且重复身份拒绝__a66002(tmp_path):
import pytest
from 构建资源包 import 编译能力资源
文件 = tmp_path / "SKILL.md"
文件.write_text(
"---\nid: inspect-task\nname: 查看任务\ncategory: operation\ncontract_version: 1\n"
"description: 合成操作\ncommands: [read_task]\n---\n\n按授权读取任务。\n"
)
with pytest.raises(ValueError, match="命令"):
编译能力资源(tmp_path, ["SKILL.md"], set())
目录, _ = 编译能力资源(tmp_path, ["SKILL.md"], {"read_task"})
assert 目录["operation"]["inspect-task"]["commands"] == ["read_task"]
(tmp_path / "副本.md").write_bytes(文件.read_bytes())
with pytest.raises(ValueError, match="重复"):
编译能力资源(tmp_path, ["SKILL.md", "副本.md"], {"read_task"})
def test_运行按发布目录读取角色正文和命令__a66003():
import pytest
from muse.任务运行.接口 import 角色策略目录
from muse.共享.错误 import 资源缺失错误
from muse.资源加载 import 读取能力, 读取能力目录
目录 = 读取能力目录()
assert set(目录["role"]) == {"writer", "planner", "extractor", "detector", "judge"}
角色 = 读取能力("role", "writer", 预期哈希=目录["role"]["writer"]["sha256"])
assert 角色["name"] == "写手" and "生成阶段" in 角色["正文"]
assert not 角色["正文"].startswith("---")
策略 = 角色策略目录.从发布包()
assert 策略.读取角色提示("writer") == 角色["正文"]
assert len(策略.资源发布身份) == 64
操作 = 读取能力("operation", "inspect-task")
assert "control_task" in 操作["commands"]
with pytest.raises(资源缺失错误):
读取能力("role", "writer", 预期哈希="0" * 64)
def _建技能(
根: Path, 分组: str, 名称: str, *, front名: str | None = None, 正文: str | None = None
) -> Path:
目录 = 根 / ".agent" / "skills" / 分组 / 名称
目录.mkdir(parents=True, exist_ok=True)
if 正文 is None:
有效名 = front名 if front名 is not None else 名称
正文 = f"---\nname: {有效名}\ndescription: 合成技能说明\n---\n\n# {名称}\n\n方法正文。\n"
(目录 / "SKILL.md").write_text(正文, encoding="utf-8")
return 目录
def test_agent_content_directories_have_standard_indexes__b35be1() -> None:
"""一级内容目录可从根索引发现,且使用固定五列表头。"""
根 = 仓库根 / ".agent"
根索引 = (根 / "目录.md").read_text()
表头 = "| 名称 | 相对地址 | 内容描述 | 使用场景 | 使用要求 |"
for 目录 in sorted(p for p in 根.iterdir() if p.is_dir()):
索引 = 目录 / "目录.md"
assert 索引.is_file(), f"缺少一级目录索引:{目录.name}"
assert 索引.read_text().startswith(表头), f"未使用固定五列表头:{索引}"
assert f"{目录.name}/目录.md" in 根索引, f"一级目录不可发现:{目录.name}"
def test_active_markdown_links_resolve__3734a6() -> None:
"""活跃执行知识(.agent 树)的 Markdown 相对链接可达。"""
问题: list[str] = []
import re
链接 = re.compile(r"\[[^\]]*\]\(([^)#\s]+)[)#]")
for 文件 in (仓库根 / ".agent").rglob("*.md"):
if "__pycache__" in 文件.parts:
continue
base = 文件.parent
for 序, 行 in enumerate(文件.read_text(encoding="utf-8").splitlines(), 1):
if 行.strip().startswith("|"):
continue # 目录表行由 检查目录链接 覆盖
for 地址 in 链接.findall(行):
if 地址.startswith(("http://", "https://", "mailto:")):
continue
if not (base / 地址).exists():
问题.append(f"{文件.relative_to(仓库根)}:{序} 链接不存在:{地址}")
assert 问题 == [], "\n".join(问题[:10])
def test_skill_directory_without_skill_file_fails__e8450b(tmp_path: Path) -> None:
"""技能目录缺少 SKILL.md 时被报告。"""
目录 = tmp_path / ".agent" / "skills" / "写作" / "无文件技能"
目录.mkdir(parents=True)
(目录 / "references.md").write_text("只有资料", encoding="utf-8")
问题 = 检查技能目录(tmp_path)
assert any("缺少 SKILL.md" in p for p in 问题), 问题
def test_empty_skill_file_is_reported__1d2b76(tmp_path: Path) -> None:
"""空 SKILL.md 被报告,不静默放行。"""
_建技能(tmp_path, "写作", "空技能", 正文="---\n")
问题 = 检查技能目录(tmp_path)
assert any("为空" in p for p in 问题), 问题
def test_frontmatter_name_must_match_skill_directory__703c7b(tmp_path: Path) -> None:
"""frontmatter name 必须与技能目录名一致。"""
_建技能(tmp_path, "写作", "真名技能", front名="别名技能")
问题 = 检查技能目录(tmp_path)
assert any("name 与目录不一致" in p for p in 问题), 问题
def test_duplicate_frontmatter_names_are_reported__eb80b5(tmp_path: Path) -> None:
"""同名技能(跨分组)被报告。"""
_建技能(tmp_path, "写作", "重复技能")
_建技能(tmp_path, "规划", "重复技能")
问题 = 检查技能目录(tmp_path)
assert any("重复" in p for p in 问题), 问题
def test_chinese_skill_name_is_accepted__de3efe(tmp_path: Path) -> None:
"""全中文技能名通过全部目录合同。"""
_建技能(tmp_path, "写作", "中文技能")
assert 检查技能目录(tmp_path) == []
def test_chinese_only_policy_rejects_english_skill_name__d5c460(tmp_path: Path) -> None:
"""英文技能名被中文唯一政策拒绝。"""
_建技能(tmp_path, "写作", "english-skill")
问题 = 检查技能目录(tmp_path)
assert any("全中文" in p for p in 问题), 问题
def test_non_normalized_skill_name_is_rejected__a87a41(tmp_path: Path) -> None:
"""非 NFC 规范形式的技能名被拒绝。"""
非规范 = "组合e\u0301音技能" # e + 组合变音符:NFC 会合成 é,与原名不同
assert unicodedata.normalize("NFC", 非规范) != 非规范
_建技能(tmp_path, "写作", 非规范)
问题 = 检查技能目录(tmp_path)
assert any("NFC" in p for p in 问题), 问题
def test_skill_name_with_whitespace_is_rejected__b68ab9(tmp_path: Path) -> None:
"""含空白的技能名被拒绝。"""
_建技能(tmp_path, "写作", "带 空白技能")
问题 = 检查技能目录(tmp_path)
assert any("含空白" in p for p in 问题), 问题
def test_missing_skills_directory_fails_closed__5acb68(tmp_path: Path) -> None:
"""技能根目录缺失时明确失败,不静默跳过。"""
问题 = 检查技能目录(tmp_path)
assert any("不存在" in p for p in 问题), 问题
def test_index_matches_disk_and_manifest__000974() -> None:
"""每个分组的 目录.md 索引与磁盘上的技能目录一一对应(不多不少)。"""
import re
技能根 = 仓库根 / ".agent" / "skills"
链接 = re.compile(r"\]\(([^/]+)/SKILL\.md\)")
for 分组 in sorted(p for p in 技能根.iterdir() if p.is_dir()):
目录文件 = 分组 / "目录.md"
assert 目录文件.is_file(), f"分组缺目录:{分组.name}"
索引名 = set(链接.findall(目录文件.read_text(encoding="utf-8")))
磁盘名 = {p.name for p in 分组.iterdir() if p.is_dir() and (p / "SKILL.md").is_file()}
assert 索引名 == 磁盘名, (
f"分组 {分组.name} 索引与磁盘不一致:"
f"索引多出 {sorted(索引名 - 磁盘名)},磁盘多出 {sorted(磁盘名 - 索引名)}"
)
def test_仓库当前树通过目录链接检查__b7c8d9() -> None:
"""当前仓库 目录.md 索引链接全部可达。"""
assert 检查目录链接(仓库根) == []
# ---------------------------------------------------------------------------
# W06 承接:能力目录与角色合同(原 SkillCatalogTest)
# ---------------------------------------------------------------------------
def _角色字节(身份: str, 名称: str, 额外: str = "") -> bytes:
return (
f"---\nid: {身份}\nname: {名称}\ncategory: role\n"
f"contract_version: 1\ndescription: 合成角色\n{额外}---\n\n你是{名称},按合同执行。\n"
).encode()
def test_current_role_catalog_is_exact_and_host_neutral__569f09() -> None:
"""已发布角色目录恰好五类且宿主中立:frontmatter 不携带任何模型绑定。"""
from muse.资源加载 import 解析能力声明, 读取能力目录
目录 = 读取能力目录()["role"]
assert set(目录) == {"writer", "planner", "extractor", "detector", "judge"}
from muse.资源加载 import 打开资源
for 身份, 条目 in 目录.items():
声明, 正文 = 解析能力声明(打开资源(条目["resource_path"]))
assert 声明["id"] == 身份 and 声明["category"] == "role"
assert "model" not in 声明 and "provider" not in 声明, f"{身份} 携带静态模型绑定"
assert 正文.strip()
def test_role_catalog_ignores_directory_index__4da912(tmp_path: Path) -> None:
"""能力目录只收登记文件;目录索引不是能力,不进入发布包。"""
import sys as _sys
_sys.path.insert(0, str(仓库根 / "工具"))
from 构建资源包 import 编译能力资源
角色 = ("writer", "planner", "extractor", "detector", "judge")
名称 = {
"writer": "写手",
"planner": "规划员",
"extractor": "知识抽取员",
"detector": "检测员",
"judge": "评委",
}
for r in 角色:
(tmp_path / "角色").mkdir(exist_ok=True)
(tmp_path / "角色" / f"{名称[r]}.md").write_bytes(_角色字节(r, 名称[r]))
(tmp_path / "角色" / "目录.md").write_text(
"| 名称 | 相对地址 | 内容描述 | 使用场景 | 使用要求 |\n"
"|------|----------|----------|----------|----------|\n",
encoding="utf-8",
)
目录, 资源 = 编译能力资源(tmp_path, [f"角色/{名称[r]}.md" for r in 角色], set())
assert set(目录["role"]) == set(角色)
assert len(资源) == 5 # 索引文件未被扫描进包
def test_role_contract_is_central_and_complete__a74ee6() -> None:
"""角色合同与策略是唯一中心:五角色、固定模型策略与正文提示都齐备。"""
import yaml
from muse.任务运行.角色策略 import 角色策略目录
from muse.资源加载 import 读取能力目录
定义 = yaml.safe_load((仓库根 / "配置" / "角色策略.yaml").read_text(encoding="utf-8"))
策略 = 角色策略目录(定义, 角色资源=读取能力目录()["role"])
合同 = (仓库根 / "muse" / "sot" / "角色合同.md").read_text(encoding="utf-8")
for 角色, 中文名 in {
"writer": "网文写手",
"detector": "检测员",
"judge": "质量评委",
"extractor": "知识抽取员",
}.items():
assert 角色 in 合同 and 中文名 in 合同
for 身份 in ("writer", "planner", "judge"):
assert 定义["roles"][身份]["model_policy"] == "fixed"
for 别名, 正名 in 定义.get("aliases", {}).items():
assert 策略.读取角色提示(别名) == 策略.读取角色提示(正名)
def test_role_frontmatter_rejects_static_model_binding__7d0984() -> None:
"""角色 frontmatter 出现 model 等未登记字段直接拒绝。"""
from muse.资源加载 import 解析能力声明
with pytest.raises(ValueError, match="未登记字段"):
解析能力声明(_角色字节("writer", "写手", 额外="model: claude-opus\n"))
with pytest.raises(ValueError, match="未登记字段"):
解析能力声明(_角色字节("planner", "规划员", 额外="provider: primary\n"))
def test_current_catalog_is_valid__0af776() -> None:
"""发布目录内每个能力都能按登记哈希读取且声明与目录一致。"""
from muse.资源加载 import 读取能力, 读取能力目录
目录 = 读取能力目录()
总数 = sum(len(v) for v in 目录.values())
assert 总数 > 0
for 分类, 条目组 in 目录.items():
for 身份, 条目 in 条目组.items():
资源 = 读取能力(分类, 身份, 预期哈希=条目["sha256"])
assert 资源["name"] == 条目["name"]
assert 资源["正文"].strip()
def test_directory_and_name_must_match__29d9f3(tmp_path: Path) -> None:
"""能力身份由声明而非文件位置决定:入包路径从身份派生,声明与目录一致。"""
import sys as _sys
_sys.path.insert(0, str(仓库根 / "工具"))
from muse.资源加载 import 解析能力声明
from 构建资源包 import 编译能力资源
# 源文件名与中文名无关;入包后由身份定位,不再允许目录名充当身份。
(tmp_path / "旧位置").mkdir()
源 = tmp_path / "旧位置" / "legacy-plan.md"
源.write_bytes(_角色字节("writer", "写手"))
目录, 资源 = 编译能力资源(tmp_path, ["旧位置/legacy-plan.md"], set())
assert "能力/role/writer.md" in 资源
声明, _ = 解析能力声明(资源["能力/role/writer.md"])
assert (声明["id"], 声明["name"]) == ("writer", "写手")
assert 目录["role"]["writer"]["resource_path"] == "能力/role/writer.md"
def test_chinese_skill_name_is_accepted__16a67a() -> None:
"""中文名称的能力声明被接受并原样保留。"""
from muse.资源加载 import 解析能力声明
声明, 正文 = 解析能力声明(_角色字节("writer", "写手"))
assert 声明["name"] == "写手"
assert "按合同执行" in 正文
def test_unknown_frontmatter_key_is_rejected__432390() -> None:
"""未知 frontmatter 键被拒绝;合法键集内仍通过。"""
from muse.资源加载 import 解析能力声明
with pytest.raises(ValueError, match="未登记字段"):
解析能力声明(_角色字节("judge", "评委", 额外="temperature: 0.7\n"))
声明, _ = 解析能力声明(_角色字节("judge", "评委"))
assert set(声明) == {"id", "name", "category", "contract_version", "description"}