"""SpecResolver 单测(Prompt 外置方案A · 步4)。 `SpecResolver` 统一内置(`SPECS`,纯内存、零 DB)与用户 skill(`SkillRegistry`,DB) 的只读解析入口。核心约束(不变量 #3 / 评审 HIGH): - `get(name)` 先查内置 `SPECS`(纯内存),命中内置 name **绝不触发任何 DB/registry 调用**; - 未命中内置才查 `SkillRegistry`;都无 → `AppError(NOT_FOUND)`; - `output_schema_for`:内置 → `SCHEMA_CATALOG[name]` 真类;纯用户 skill → None; name 精确字符串相等(无大小写/连字符归一),拼错近似 name → None 不报错; - `resolver.build` 纯合并,**不**做保留名冲突校验(守卫前移至 SkillRegistry 入库)。 """ from __future__ import annotations import pytest from ww_agents import ( SCHEMA_CATALOG, SPECS, AgentSpec, ) from ww_shared import AppError, ErrorCode from ww_skills import SpecResolver class _FakeRegistry: """断言用 fake:记录 get/names 是否被调用(内置 get 必须零触达)。""" def __init__(self, specs: dict[str, AgentSpec] | None = None) -> None: self._specs = dict(specs or {}) self.get_calls: list[str] = [] self.names_calls: int = 0 def get(self, name: str) -> AgentSpec: self.get_calls.append(name) spec = self._specs.get(name) if spec is None: raise AppError(ErrorCode.NOT_FOUND, f"skill not found: {name}") return spec def names(self) -> list[str]: self.names_calls += 1 return sorted(self._specs) def list_scope(self, scope: str) -> list[AgentSpec]: return [s for s in sorted(self._specs.values(), key=lambda s: s.name) if s.scope == scope] def _user_spec(name: str, scope: str = "custom") -> AgentSpec: return AgentSpec( name=name, tier="writer", system_prompt=f"prompt for {name}", input_schema=None, output_schema=None, reads=(), writes=(), scope=scope, ) # ---- #7 resolver 内置/用户对齐 + 零 DB ---- def test_get_builtin_returns_specs_instance_without_touching_registry() -> None: # Arrange:fake registry 本身有同名条目也无所谓——内置命中必须零触达。 fake = _FakeRegistry({"continuity": _user_spec("continuity")}) resolver = SpecResolver.build(fake) # Act spec = resolver.get("continuity") # Assert:拿到的是内置同一实例,且 registry.get 从未被调用(零 DB)。 assert spec is SPECS["continuity"] assert fake.get_calls == [] def test_get_user_skill_falls_through_to_registry() -> None: fake = _FakeRegistry({"my-skill": _user_spec("my-skill")}) resolver = SpecResolver.build(fake) spec = resolver.get("my-skill") assert spec.name == "my-skill" assert fake.get_calls == ["my-skill"] def test_get_unknown_everywhere_raises_not_found() -> None: fake = _FakeRegistry() resolver = SpecResolver.build(fake) with pytest.raises(AppError) as exc: resolver.get("nope-not-here") assert exc.value.code is ErrorCode.NOT_FOUND assert fake.get_calls == ["nope-not-here"] def test_names_merges_builtin_and_user() -> None: fake = _FakeRegistry({"my-skill": _user_spec("my-skill")}) resolver = SpecResolver.build(fake) names = resolver.names() assert "continuity" in names assert "my-skill" in names def test_list_scope_filters_builtin_and_user() -> None: fake = _FakeRegistry({"my-skill": _user_spec("my-skill", scope="custom")}) resolver = SpecResolver.build(fake) builtin = resolver.list_scope("builtin") custom = resolver.list_scope("custom") assert all(s.scope == "builtin" for s in builtin) assert {s.name for s in custom} == {"my-skill"} # ---- #9 output_schema_for ---- def test_output_schema_for_builtin_returns_real_type() -> None: fake = _FakeRegistry() resolver = SpecResolver.build(fake) assert resolver.output_schema_for("continuity") is SCHEMA_CATALOG["continuity"] def test_output_schema_for_refiner_is_none() -> None: fake = _FakeRegistry() resolver = SpecResolver.build(fake) assert resolver.output_schema_for("refiner") is None def test_output_schema_for_pure_user_skill_is_none() -> None: fake = _FakeRegistry({"my-skill": _user_spec("my-skill")}) resolver = SpecResolver.build(fake) assert resolver.output_schema_for("my-skill") is None def test_output_schema_for_misspelled_name_is_none_no_fuzzy_match() -> None: # 拼错近似 name(下划线)不得误命中连字符内置 "character-gen"(精确字符串相等)。 fake = _FakeRegistry() resolver = SpecResolver.build(fake) assert resolver.output_schema_for("character_gen") is None assert resolver.output_schema_for("character-gen") is not None