实现侧: - 上下文:任务范围拆分为 范围校验/范围授权;索引按可发现口径重建、索引新鲜度改对称差;依赖校验统一快照漂移说明。 - 知识方法:方法与材料读取口径统一;超限方法材料按可选省略,核对路径不再二次计费;删除无合同的读时重算。 - 任务运行:新增 context.usage/tool.denied 事件类型;连接池常驻并在装配生命周期内开关;调用结算与核对分列。 - 效果评测/审校修订/交付连载/作者经验/作品规划:凭据冻结、标定消费、导出补证、事实引文核对等收尾修复。 - 资源加载:能力正文不再夹带索引用的导航注记(该注记此前进入角色与技能的模型提示)。 - 元数据:受保护骨架与代码保护属性对齐;字段校验与内置结构口径同步。 - 基础设施:环境预检进入装配生命周期;数据库连接运行期字段不参与相等比较;索引指纹归一化 jsonb 浮点。 - 删除被替代实现:7 份旧提示词模板与空壳 资料来源 读取器。 用例侧: - 用例身份与导航元信息迁移;夹具补生命周期、同库暴露与模板封存; - 本轮定向修复:方法材料省略、事实引文、迁移回执、额度与暂停用例、慢用例超时预算等。
750 lines
32 KiB
Python
750 lines
32 KiB
Python
"""技能与文档索引行为验证。
|
||
|
||
保护:技能目录合同(SKILL.md 完整、名称唯一且全中文、frontmatter 一致)
|
||
与活跃文档索引(目录.md 标准索引、链接可达)。
|
||
|
||
测试针对现行能力与索引合同,不维护历史过程状态。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
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 文件
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"TC-a189e40d3896",
|
||
environment="离线,临时资源目录",
|
||
given="不依赖当前仓的临时项目,仅登记一份alpha能力文件",
|
||
when="从其他cwd编译显式资源根",
|
||
then=["只得到一份具名能力和原字节,无异常"],
|
||
contract="docs/系统架构/新版设计/编排与能力资源.md",
|
||
)
|
||
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()
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"TC-c9cb853e4a71",
|
||
environment="离线,临时资源目录",
|
||
given="显式登记.agent/skills/planning/alpha/SKILL.md",
|
||
when="从嵌套物理路径编译能力",
|
||
then=["稳定包路径和原文件字节一致"],
|
||
contract="docs/系统架构/新版设计/编排与能力资源.md",
|
||
)
|
||
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()
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"TC-c089ceceda65",
|
||
environment="离线,临时资源目录",
|
||
given="能力name为原输入alpha",
|
||
when="构建中文能力目录",
|
||
then=["以名称错误拒绝编译"],
|
||
contract="docs/系统架构/新版设计/编排与能力资源.md",
|
||
)
|
||
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())
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"TC-b72bcc397057",
|
||
environment="离线,临时资源目录",
|
||
given="规划作品和新增技能两个中文方法",
|
||
when="编译两份原名称资源",
|
||
then=["两份能力与字节资源均被接纳"],
|
||
contract="docs/系统架构/新版设计/编排与能力资源.md",
|
||
)
|
||
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
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"TC-5b50cc5fa9e9",
|
||
environment="离线,临时资源目录",
|
||
given="原输入e加组合重音-name作为展示名",
|
||
when="解析并编译能力声明",
|
||
then=["名称无效时拒绝,不对调用方原输入静默改写"],
|
||
contract="docs/系统架构/新版设计/编排与能力资源.md",
|
||
)
|
||
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())
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"TC-984f56f78feb",
|
||
environment="离线,临时资源目录",
|
||
given="原输入故事结构!作为展示名",
|
||
when="解析并编译能力声明",
|
||
then=["以名称错误拒绝"],
|
||
contract="docs/系统架构/新版设计/编排与能力资源.md",
|
||
)
|
||
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())
|
||
|
||
|
||
@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-b35be1711a49",
|
||
environment="离线,临时资源目录",
|
||
given=".agent 下所有一级内容目录与根索引",
|
||
when="核对索引存在、固定五列表头和根索引发现路径",
|
||
then=["三种检查全部通过,缺失或不可发现时独立失败"],
|
||
contract="docs/系统架构/新版设计/编排与能力资源.md",
|
||
)
|
||
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}"
|
||
正文 = re.sub(r"\A(?:<!-- 导航元信息: .*? -->\n)+", "", 索引.read_text())
|
||
assert 正文.startswith(表头), f"未使用固定五列表头:{索引}"
|
||
assert f"{目录.name}/目录.md" in 根索引, f"一级目录不可发现:{目录.name}"
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"TC-3734a6672473",
|
||
environment="离线,临时资源目录",
|
||
when="读取或生成登记资源及导航。",
|
||
contract="docs/系统架构/新版设计/编排与能力资源.md",
|
||
)
|
||
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])
|
||
|
||
|
||
@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-1d2b76028e03",
|
||
environment="离线,临时资源目录",
|
||
when="读取或生成登记资源及导航。",
|
||
contract="docs/系统架构/新版设计/编排与能力资源.md",
|
||
)
|
||
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 问题), 问题
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"TC-703c7b148785",
|
||
environment="离线,临时资源目录",
|
||
when="读取或生成登记资源及导航。",
|
||
contract="docs/系统架构/新版设计/编排与能力资源.md",
|
||
)
|
||
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 问题), 问题
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"TC-eb80b52b2bdc",
|
||
environment="离线,临时资源目录",
|
||
when="读取或生成登记资源及导航。",
|
||
contract="docs/系统架构/新版设计/编排与能力资源.md",
|
||
)
|
||
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 问题), 问题
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"TC-de3efec07d8b",
|
||
environment="离线,临时资源目录",
|
||
when="读取或生成登记资源及导航。",
|
||
contract="docs/系统架构/新版设计/编排与能力资源.md",
|
||
)
|
||
def test_chinese_skill_name_is_accepted__de3efe(tmp_path: Path) -> None:
|
||
"""全中文技能名通过全部目录合同。"""
|
||
_建技能(tmp_path, "写作", "中文技能")
|
||
assert 检查技能目录(tmp_path) == []
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"TC-d5c460039ec2",
|
||
environment="离线,临时资源目录",
|
||
when="读取或生成登记资源及导航。",
|
||
contract="docs/系统架构/新版设计/编排与能力资源.md",
|
||
)
|
||
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 问题), 问题
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"TC-a87a412a67d0",
|
||
environment="离线,临时资源目录",
|
||
when="读取或生成登记资源及导航。",
|
||
contract="docs/系统架构/新版设计/编排与能力资源.md",
|
||
)
|
||
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 问题), 问题
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"TC-b68ab9a51c08",
|
||
environment="离线,临时资源目录",
|
||
when="读取或生成登记资源及导航。",
|
||
contract="docs/系统架构/新版设计/编排与能力资源.md",
|
||
)
|
||
def test_skill_name_with_whitespace_is_rejected__b68ab9(tmp_path: Path) -> None:
|
||
"""含空白的技能名被拒绝。"""
|
||
_建技能(tmp_path, "写作", "带 空白技能")
|
||
问题 = 检查技能目录(tmp_path)
|
||
assert any("含空白" in p for p in 问题), 问题
|
||
|
||
|
||
@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-000974f474ac",
|
||
environment="离线,临时资源目录",
|
||
when="读取或生成登记资源及导航。",
|
||
contract="docs/系统架构/新版设计/编排与能力资源.md",
|
||
)
|
||
def test_index_matches_disk_and_manifest__000974(tmp_path: Path) -> None:
|
||
"""每个分组的 目录.md 索引与磁盘上的技能目录一一对应(不多不少)。"""
|
||
assert 检查技能索引(仓库根) == []
|
||
from 维护索引 import 写入目录
|
||
|
||
技能 = _建技能(tmp_path, "写作", "新技能")
|
||
assert 检查技能索引(tmp_path) # 源新增后,双向检查不能遗漏新技能。
|
||
写入目录(tmp_path)
|
||
assert 检查技能索引(tmp_path) == []
|
||
assert 写入目录(tmp_path) == []
|
||
原索引 = (技能.parent / "目录.md").read_text()
|
||
assert "合成技能说明" in 原索引
|
||
assert "| | |" in 原索引 # 缺场景或要求时保持未知,不生成作者意图。
|
||
(技能.parent / "目录.md").write_text(原索引 + 原索引.splitlines()[-1] + "\n")
|
||
assert any("重复" in 问题 for 问题 in 检查技能索引(tmp_path))
|
||
写入目录(tmp_path)
|
||
assert 检查技能索引(tmp_path) == []
|
||
(技能 / "SKILL.md").unlink()
|
||
写入目录(tmp_path)
|
||
assert "新技能/SKILL.md" not in (技能.parent / "目录.md").read_text()
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"NC-index-links-current-tree",
|
||
environment="离线",
|
||
given="当前仓库全部 目录.md 索引",
|
||
when="运行目录链接检查",
|
||
then=["所有表格相对链接可达,无悬空索引"],
|
||
contract="docs/系统架构/新版设计/文件设计/工程配置与部署.md#file-87efcaf91ed97cf4",
|
||
)
|
||
def test_仓库当前树通过目录链接检查__b7c8d9() -> None:
|
||
"""当前仓库 目录.md 索引链接全部可达。"""
|
||
assert 检查目录链接(仓库根) == []
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 能力目录与角色合同(原 SkillCatalogTest)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
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-0af776e73f52",
|
||
environment="离线,临时资源目录",
|
||
when="读取或生成登记资源及导航。",
|
||
contract="docs/系统架构/新版设计/编排与能力资源.md",
|
||
)
|
||
def test_current_catalog_is_valid__0af776() -> None:
|
||
"""按批准资源列表和源码frontmatter独立核验实际读取结果。"""
|
||
import hashlib
|
||
import json
|
||
|
||
import yaml
|
||
|
||
from muse.资源加载 import 打开资源, 读取能力, 读取能力目录
|
||
|
||
登记 = json.loads((仓库根 / "工具/资源登记.json").read_text())["能力文件"]
|
||
期望 = {}
|
||
for 相对 in 登记:
|
||
原字节 = (仓库根 / 相对).read_bytes()
|
||
_, 头部, 正文 = 原字节.decode().split("---", 2)
|
||
声明 = yaml.safe_load(头部)
|
||
# 首部导航注记是索引工装,只服务目录派生;进模型上下文的能力正文不含它。
|
||
期望[声明["category"], 声明["id"]] = (
|
||
声明,
|
||
re.sub(r"\A\s*(?:<!--\s*导航元信息:.*?-->\s*)+", "", 正文, flags=re.S).strip(),
|
||
原字节,
|
||
)
|
||
目录 = 读取能力目录()
|
||
assert {(分类, 身份) for 分类, 组 in 目录.items() for 身份 in 组} == set(期望)
|
||
for (分类, 身份), (声明, 正文, 原字节) in 期望.items():
|
||
哈希 = hashlib.sha256(原字节).hexdigest()
|
||
条目 = 目录[分类][身份]
|
||
资源 = 读取能力(分类, 身份, 预期哈希=哈希)
|
||
assert 条目["sha256"] == 哈希
|
||
assert 打开资源(条目["resource_path"]) == 原字节
|
||
assert 资源["name"] == 声明["name"]
|
||
assert 资源["description"] == 声明["description"]
|
||
assert 资源["正文"] == 正文 and 正文
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"TC-29d9f3aef372",
|
||
environment="离线,临时资源目录",
|
||
when="读取或生成登记资源及导航。",
|
||
contract="docs/系统架构/新版设计/编排与能力资源.md",
|
||
)
|
||
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"
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"TC-16a67a06447a",
|
||
environment="离线,临时资源目录",
|
||
when="读取或生成登记资源及导航。",
|
||
contract="docs/系统架构/新版设计/编排与能力资源.md",
|
||
)
|
||
def test_chinese_skill_name_is_accepted__16a67a() -> None:
|
||
"""中文名称的能力声明被接受并原样保留。"""
|
||
from muse.资源加载 import 解析能力声明
|
||
|
||
声明, 正文 = 解析能力声明(_角色字节("writer", "写手"))
|
||
assert 声明["name"] == "写手"
|
||
assert "按合同执行" in 正文
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"TC-432390f6c7ef",
|
||
environment="离线,临时资源目录",
|
||
when="读取或生成登记资源及导航。",
|
||
contract="docs/系统架构/新版设计/编排与能力资源.md",
|
||
)
|
||
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"}
|
||
|
||
|
||
@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"]
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"TC-d04e74393b21",
|
||
environment="离线;scripted和资源指纹不证明真实模型",
|
||
when="运行新版公共场景裁决、实际CLI或安装包能力查询",
|
||
then=["记录案例技能只暴露采集、审阅、决定、撤回与读取,不暴露规则生成命令。"],
|
||
contract="docs/系统架构/新版设计/验证设计/诊断行为场景.md",
|
||
)
|
||
def test_记录案例技能不暴露规则生成命令__d04e74():
|
||
from muse.资源加载 import 读取能力
|
||
|
||
skill = 读取能力("operation", "operation.capture-quality-case")
|
||
assert "capture_quality_case" in skill["commands"]
|
||
assert "propose_rule_from_cases" not in skill["commands"]
|
||
assert {"open_quality_case_review", "decide_quality_case", "withdraw_quality_case"} <= set(
|
||
skill["commands"]
|
||
)
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"TC-2242b7ff5b75",
|
||
environment="离线;scripted和资源指纹不证明真实模型",
|
||
when="运行新版公共场景裁决、实际CLI或安装包能力查询",
|
||
then=[
|
||
"20个研究项目与15项能力、六个技能归属在新版权威矩阵逐项校验;路径存在只为导航,pending/partial与限制保留。"
|
||
],
|
||
contract="docs/系统架构/新版设计/验证设计/诊断行为场景.md",
|
||
)
|
||
def test_研究能力映射保留20项来源与六技能归属__2242b7():
|
||
import json
|
||
from pathlib import Path
|
||
|
||
root = Path(__file__).parents[2]
|
||
path = root / "docs/系统架构/新版设计/研究依据/质量能力承接.json"
|
||
data = json.loads(path.read_text())
|
||
assert data["schema_version"] == "quality-research-capabilities-v2"
|
||
capabilities = data["capabilities"]
|
||
assert len(capabilities) >= 12
|
||
cap_ids = {c["id"] for c in capabilities}
|
||
assert len(data["projects"]) == 20
|
||
assert len({p["id"] for p in data["projects"]}) == 20
|
||
for p in data["projects"]:
|
||
assert p["mapped_capabilities"] and set(p["mapped_capabilities"]) <= cap_ids
|
||
assert {c["owner_skill"] for c in capabilities} == {
|
||
"建立作品声音档案",
|
||
"预防机器味",
|
||
"诊断机器味",
|
||
"修正正文机器味",
|
||
"记录机器味案例",
|
||
"建立机器味规则",
|
||
}
|
||
for c in capabilities:
|
||
assert (root / c["implementation"]).is_file()
|
||
assert c["status"] in {"implemented", "partial", "pending"}
|
||
assert c["verification_limit"]
|
||
|
||
|
||
@pytest.mark.case_id(
|
||
"NC-o04-capability-frontmatter-independent",
|
||
environment="离线,当前源资源与损坏的派生目录",
|
||
given="派生目录和读取能力同时带有相同的错误名称或描述",
|
||
when="对照批准资源登记和源frontmatter独立核验",
|
||
then=["不能因读取能力原样回传目录元信息而错误放行"],
|
||
)
|
||
@pytest.mark.parametrize("字段", ["name", "description"], ids=["name", "description"])
|
||
def test_能力元信息同源错误不掩盖真实声明(monkeypatch, 字段):
|
||
from copy import deepcopy
|
||
|
||
from muse import 资源加载
|
||
|
||
目录 = deepcopy(资源加载.读取能力目录())
|
||
组 = next(组 for 组 in 目录.values() if 组)
|
||
next(iter(组.values()))[字段] = "合成错误声明"
|
||
monkeypatch.setattr(资源加载, "读取能力目录", lambda: 目录)
|
||
with pytest.raises(AssertionError):
|
||
test_current_catalog_is_valid__0af776()
|