"""T6 创作工具箱声明式描述符类型单测(Wave-0 契约)。 仅校验描述符**类型**本身(无 TOOLBOX 注册表 seeding——那是 Wave A): - `ContextStrategy`:注入策略字面量(4 基础 + 续写/原文输入 2 竞品策略); - `InputField`:声明式表单字段(label/type/required/default/help); - `IngestSpec`:可选入库目标(table 必须在 KNOWN_TABLES,None=纯预览); - `GeneratorTool`:生成器声明(legacy 带 legacy_route 且 spec 可为 None;新工具带 spec+schema)。 全部 frozen(不可变,全局 immutability 约定)——构造后改字段须抛错。 """ from __future__ import annotations import dataclasses import pytest from pydantic import BaseModel, ValidationError from ww_agents import AgentSpec from ww_shared import AppError, ErrorCode from ww_skills import ( ContextStrategy, GeneratorTool, IngestSpec, InputField, ) class _FakeOut(BaseModel): """构造测试用的 output_schema 占位(Pydantic 模型类)。""" value: str def _spec() -> AgentSpec: return AgentSpec( name="brainstorm", tier="light", system_prompt="x", output_schema=_FakeOut, reads=("projects",), writes=(), ) # ---- ContextStrategy ---- def test_context_strategy_values() -> None: # Arrange / Act from typing import get_args values = set(get_args(ContextStrategy)) # Assert:4 基础策略 + 竞品快赢 2 策略(续写读前文 / 原文输入)。 assert values == { "brief_only", "with_project", "with_world", "with_outline_chapter", "with_prior_chapter", "text_input", } # ---- InputField ---- def test_input_field_required_fields_and_defaults() -> None: # Arrange / Act field = InputField(name="brief", label="一句话需求", type="textarea") # Assert: required 默认 True,其余可空 assert field.name == "brief" assert field.label == "一句话需求" assert field.type == "textarea" assert field.required is True assert field.default is None assert field.help is None def test_input_field_accepts_optional_metadata() -> None: field = InputField( name="count", label="数量", type="number", required=False, default="3", help="一次生成几条", ) assert field.required is False assert field.default == "3" assert field.help == "一次生成几条" def test_input_field_is_frozen() -> None: field = InputField(name="brief", label="需求", type="text") with pytest.raises(ValidationError): field.label = "改名" # ---- IngestSpec ---- def test_ingest_spec_accepts_known_table() -> None: spec = IngestSpec(table="world_entities") assert spec.table == "world_entities" def test_ingest_spec_rejects_unknown_table() -> None: # 越权/不存在的入库表 → VALIDATION(不可信声明的第一道闸) with pytest.raises(AppError) as exc: IngestSpec(table="users") assert exc.value.code is ErrorCode.VALIDATION def test_ingest_spec_is_frozen() -> None: spec = IngestSpec(table="outline") with pytest.raises(ValidationError): spec.table = "characters" # ---- GeneratorTool ---- def test_generator_tool_new_tool_with_spec_and_schema() -> None: # Arrange / Act:新工具带 spec + output_schema,无 legacy_route tool = GeneratorTool( key="brainstorm", title="脑洞生成器", subtitle="突破想象,脑洞大开", spec=_spec(), output_schema=_FakeOut, context_strategy="brief_only", input_fields=[InputField(name="brief", label="需求", type="textarea", required=False)], ) # Assert assert tool.key == "brainstorm" assert tool.spec is not None assert tool.output_schema is _FakeOut assert tool.context_strategy == "brief_only" assert tool.ingest is None assert tool.legacy_route is None assert tool.genre is None assert len(tool.input_fields) == 1 def test_generator_tool_legacy_tool_without_spec() -> None: # legacy 工具:spec=None + 指向现有页面/端点 tool = GeneratorTool( key="worldbuilding", title="世界观生成器", subtitle="构建自洽世界", spec=None, output_schema=None, context_strategy="with_project", input_fields=[], legacy_route="/projects/{id}/world", ) assert tool.spec is None assert tool.output_schema is None assert tool.legacy_route == "/projects/{id}/world" def test_generator_tool_with_ingest_and_genre() -> None: tool = GeneratorTool( key="glossary", title="词条生成器", subtitle="批量造词条", spec=_spec(), output_schema=_FakeOut, context_strategy="with_world", input_fields=[], ingest=IngestSpec(table="world_entities"), genre="xuanhuan", ) assert tool.ingest is not None assert tool.ingest.table == "world_entities" assert tool.genre == "xuanhuan" def test_generator_tool_is_frozen() -> None: tool = GeneratorTool( key="brainstorm", title="脑洞生成器", subtitle="x", spec=_spec(), output_schema=_FakeOut, context_strategy="brief_only", input_fields=[], ) with pytest.raises(ValidationError): tool.title = "改名" def test_generator_tool_input_fields_default_empty() -> None: tool = GeneratorTool( key="x", title="t", subtitle="s", spec=None, output_schema=None, context_strategy="brief_only", input_fields=[], legacy_route="/x", ) # input_fields 是显式入参;frozen 后整体不可重绑 assert tool.input_fields == [] assert not dataclasses.is_dataclass(tool) # 用 Pydantic,非 dataclass