- 维护索引按 git 视角枚举(已跟踪 + 未跟踪且未忽略):被 .gitignore 忽略的运行痕迹与 过程性检查报告不再进入目录派生或影响索引门禁;非 git 场景退回原磁盘枚举 - conftest 验证闸门在 `验证锁 --执行` 启动的会话内不再取共享锁,修复 `make 索引生成` 在独占锁内收集用例时的自锁(原 180s 超时失败 → 现 6s 通过) - .gitignore 登记过程性检查报告与工具会话缓存(.zcode/):可复跑,不入历史 - 新增两条测试:忽略内容不参与目录派生、独占锁内收集不自锁
315 lines
13 KiB
Python
315 lines
13 KiB
Python
"""技能与文档索引行为验证。
|
||
|
||
保护:技能目录合同(SKILL.md 完整、名称唯一且全中文、frontmatter 一致)
|
||
与活跃文档索引(目录.md 标准索引、链接可达)。
|
||
|
||
测试针对现行能力与索引合同,不维护历史过程状态。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import subprocess
|
||
import sys
|
||
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 文件
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"NC-capability-stable-identity",
|
||
environment="离线资源编译",
|
||
given="显式角色frontmatter及正文",
|
||
when="编译并移动物理文件",
|
||
then=["稳定身份、包内路径及正文哈希不因源路径移动改变"],
|
||
contract="docs/系统架构/新版设计/编排与能力资源.md",
|
||
)
|
||
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"]
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"NC-capability-command-registry",
|
||
environment="离线资源编译",
|
||
given="操作技能引用具名命令",
|
||
when="核对公开操作及重复身份",
|
||
then=["未实现命令和重复能力身份拒绝;有效引用可进入目录"],
|
||
contract="docs/系统架构/新版设计/编排与能力资源.md",
|
||
)
|
||
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"})
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"NC-capability-runtime-resource",
|
||
environment="离线实际包资源",
|
||
given="实际资源包中的五角色与两操作",
|
||
when="经公开资源读取及角色策略接口加载",
|
||
then=["按稳定身份和冻结哈希取正文,不将frontmatter写入提示,未知哈希拒绝"],
|
||
contract="docs/系统架构/新版设计/编排与能力资源.md",
|
||
)
|
||
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 目录
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"TC-5acb683e6f5c",
|
||
environment="离线,临时资源目录",
|
||
when="读取或生成登记资源及导航。",
|
||
contract="docs/系统架构/新版设计/编排与能力资源.md",
|
||
)
|
||
def test_missing_skills_directory_fails_closed__5acb68(tmp_path: Path) -> None:
|
||
"""技能根目录缺失时明确失败,不静默跳过。"""
|
||
问题 = 检查技能目录(tmp_path)
|
||
assert any("不存在" in p for p in 问题), 问题
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"TC-e8450b6e7c65",
|
||
environment="离线,临时资源目录",
|
||
when="读取或生成登记资源及导航。",
|
||
contract="docs/系统架构/新版设计/编排与能力资源.md",
|
||
)
|
||
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 问题), 问题
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"TC-7b1e4d9c35a2",
|
||
environment="离线,临时 git 工作树",
|
||
when="读取或生成登记资源及导航。",
|
||
contract="docs/系统架构/新版设计/编排与能力资源.md",
|
||
)
|
||
def test_索引只覆盖会提交内容而忽略运行痕迹__7b1e4d(tmp_path: Path) -> None:
|
||
"""被 .gitignore 忽略的运行痕迹不进入目录派生,也不影响索引一致性。"""
|
||
根 = tmp_path
|
||
subprocess.run(["git", "init", "-q"], cwd=根, check=True)
|
||
(根 / ".gitignore").write_text("docs/噪音/\n", encoding="utf-8")
|
||
(根 / "docs").mkdir()
|
||
(根 / "docs" / "目录.md").write_text(
|
||
"| 名称 | 相对地址 | 内容描述 | 使用场景 | 使用要求 |\n"
|
||
"|------|----------|----------|----------|----------|\n",
|
||
encoding="utf-8",
|
||
)
|
||
(根 / "docs" / "真实.md").write_text("# 真实\n\n正文。\n", encoding="utf-8")
|
||
(根 / "docs" / "噪音").mkdir()
|
||
(根 / "docs" / "噪音" / "跑痕.json").write_text("{}\n", encoding="utf-8")
|
||
|
||
写入目录(根)
|
||
文本 = (根 / "docs" / "目录.md").read_text(encoding="utf-8")
|
||
assert (根 / "docs" / "噪音" / "跑痕.json").is_file() # 痕迹在磁盘上,只是不参与派生
|
||
assert "真实.md" in 文本
|
||
assert "噪音" not in 文本
|
||
assert 检查派生目录(根) == []
|
||
|
||
|
||
def _角色字节(身份: str, 名称: str, 额外: str = "") -> bytes:
|
||
return (
|
||
f"---\nid: {身份}\nname: {名称}\ncategory: role\n"
|
||
f"contract_version: 1\ndescription: 合成角色\n{额外}---\n\n你是{名称},按合同执行。\n"
|
||
).encode()
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"TC-569f09681a64",
|
||
environment="离线,临时资源目录",
|
||
when="读取或生成登记资源及导航。",
|
||
contract="docs/系统架构/新版设计/编排与能力资源.md",
|
||
)
|
||
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()
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"TC-4da9120a63cb",
|
||
environment="离线,临时资源目录",
|
||
when="读取或生成登记资源及导航。",
|
||
contract="docs/系统架构/新版设计/编排与能力资源.md",
|
||
)
|
||
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 # 索引文件未被扫描进包
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"TC-a74ee65b8606",
|
||
environment="离线,临时资源目录",
|
||
when="读取或生成登记资源及导航。",
|
||
contract="docs/系统架构/新版设计/编排与能力资源.md",
|
||
)
|
||
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"])
|
||
合同 = (仓库根 / ".agent" / "角色" / "角色合同.md").read_text(encoding="utf-8")
|
||
for 角色, 中文名 in {
|
||
"writer": "网文写手",
|
||
"detector": "检测员",
|
||
"judge": "质量评委",
|
||
"extractor": "知识抽取员",
|
||
}.items():
|
||
assert 角色 in 合同 and 中文名 in 合同
|
||
for 身份 in ("writer", "planner"):
|
||
assert 定义["roles"][身份]["model_policy"] == "fixed"
|
||
assert 定义["roles"]["judge"]["model_policy"] == "judge-fixed"
|
||
for 别名, 正名 in 定义.get("aliases", {}).items():
|
||
assert 策略.读取角色提示(别名) == 策略.读取角色提示(正名)
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"TC-7d0984bb6019",
|
||
environment="离线,临时资源目录",
|
||
when="读取或生成登记资源及导航。",
|
||
contract="docs/系统架构/新版设计/编排与能力资源.md",
|
||
)
|
||
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"))
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"TC-fc9b819e3c0d",
|
||
environment="离线;scripted和资源指纹不证明真实模型",
|
||
when="运行新版公共场景裁决、实际CLI或安装包能力查询",
|
||
then=[
|
||
"旧Skill内CLI改为共享审校CLI;安装包建立规则技能独占propose_rule_from_cases,解析动作和稳定命令身份真实存在。"
|
||
],
|
||
contract="docs/系统架构/新版设计/验证设计/诊断行为场景.md",
|
||
)
|
||
def test_规则建立使用共享CLI且有独立命令归属__fc9b81():
|
||
from muse.接入.cli.入口 import _解析参数
|
||
from muse.资源加载 import 读取能力
|
||
|
||
args = _解析参数(["审校", "synthetic.toml", "案例规则", "request.json"])
|
||
assert args.动作 == "案例规则"
|
||
skill = 读取能力("operation", "operation.propose-review-rule")
|
||
assert skill["name"] == "建立机器味规则"
|
||
assert "propose_rule_from_cases" in skill["commands"]
|
||
assert "capture_quality_case" not in skill["commands"]
|