Files
writer-work-flow/packages/skills/tests/test_skill_registry.py
Yaojia Wang 765dbdfbd4 feat: M4 文风 + M5 生成/多provider/Skill + Kimi Code 订阅接入 + 本地联调修复
M4(文风): style-auditor 双轨(提取指纹/漂移第四审)+ jobs 长任务框架(zombie reaper) + 回炉 refine + GET /style read-back。
M5(生成+扩展): worldbuilder/character-gen(入库 continuity 409 gate + partition_writes 白名单 + schema→JSONB 形变);
  网关多 provider 回退链/熔断/能力降级(Anthropic/Gemini 适配器);Skill registry + 表权限沙箱 + 规则;
  前端 角色生成器/世界观/Codex/规则页/技能库/⌘K 命令面板。
K1(Kimi Code 订阅接入): OAuth device-flow(kimi-code)+ 静态 Console key(kimi-code-key)两路径;
  coding 端点 KimiCLI 伪造头(实测 UA allow-list 门禁,缺则 403)+ JSON 模式结构化(thinking ⊥ tool_choice)。
本地联调修复: CORS 中间件;assemble 注入 premise+「写第N章」指令(修空 prompt 400);
  GET /outline·/draft read-back + 大纲/工作台/审稿页重载;写页 client/server 常量边界 + notFound 健壮化;
  字数 toLocaleString locale 水合;审稿页终稿从已存草稿 seed(修 accept 422)。
门禁: backend ruff/mypy(157)/alembic 无漂移/pytest 451 · frontend lint/tsc/vitest/build。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 10:39:58 +02:00

104 lines
3.1 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.

"""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行里只带 tierregistry 不解析具体 modelmodel 解析在网关)。
"""
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