"""T5.5 Skill registry loader 单测(ARCH §5.6 加载段)。 `SkillRegistry` 从 `SkillRepo`(DB 行抽象)加载声明式 `AgentSpec`(builtin/custom/community), 提供 `get(name)` / `list_scope(scope)` / `names()`。加载时跑 `validate_declaration`—— 越权声明(reads/writes 指向未知表)的 skill 直接拒绝(不入册),守 §5.6 表权限契约。 纯内存 fake `SkillRepo`,无 DB:注册行只是声明数据,registry 转成 frozen AgentSpec。 不变量 #2:行里只带 tier,registry 不解析具体 model(model 解析在网关)。 """ from __future__ import annotations import pytest from ww_agents import AgentSpec from ww_shared import AppError, ErrorCode from ww_skills import SkillRecord, SkillRegistry def _record( name: str, *, scope: str = "custom", reads: list[str] | None = None, writes: list[str] | None = None, ) -> SkillRecord: return SkillRecord( name=name, scope=scope, tier="writer", system_prompt=f"prompt for {name}", reads=reads or [], writes=writes or [], genre=None, ) class _FakeSkillRepo: def __init__(self, records: list[SkillRecord]) -> None: self._records = records async def list_all(self) -> list[SkillRecord]: return list(self._records) @pytest.mark.asyncio async def test_load_builds_specs_keyed_by_name() -> None: repo = _FakeSkillRepo( [ _record( "worldgen", scope="builtin", reads=["world_entities"], writes=["world_entities"], ), _record("cpgen", scope="custom", reads=["characters"]), ] ) registry = await SkillRegistry.load(repo) assert set(registry.names()) == {"worldgen", "cpgen"} spec = registry.get("worldgen") assert isinstance(spec, AgentSpec) assert spec.tier == "writer" assert spec.reads == ["world_entities"] assert spec.scope == "builtin" @pytest.mark.asyncio async def test_get_missing_skill_raises_not_found() -> None: registry = await SkillRegistry.load(_FakeSkillRepo([_record("a")])) with pytest.raises(AppError) as exc: registry.get("missing") assert exc.value.code is ErrorCode.NOT_FOUND @pytest.mark.asyncio async def test_list_scope_filters_by_scope() -> None: repo = _FakeSkillRepo( [ _record("b1", scope="builtin"), _record("c1", scope="custom"), _record("c2", scope="custom"), ] ) registry = await SkillRegistry.load(repo) assert {s.name for s in registry.list_scope("custom")} == {"c1", "c2"} assert {s.name for s in registry.list_scope("builtin")} == {"b1"} @pytest.mark.asyncio async def test_load_rejects_over_permission_skill() -> None: # 越权声明(reads 指向未知表)→ 加载即拒绝(守 §5.6),不静默入册。 repo = _FakeSkillRepo([_record("evil", reads=["secret_table"])]) with pytest.raises(AppError) as exc: await SkillRegistry.load(repo) assert exc.value.code is ErrorCode.VALIDATION