Files
writer-work-flow/packages/skills/tests/test_toolbox.py
Yaojia Wang a5351320a2 feat(skills): Scope B A2 — 4 竞品生成器注册表+上下文分派+端点接线
注册表 TOOLBOX +4 entry(continue/expand/de-ai/teardown,全 preview-only writes=[]);
ContextStrategy +with_prior_chapter/text_input;ToolGenerateRequest +text 字段。
toolbox_context 分派新策略(续写→build_continuation_context、原文→build_text_input_context);
路由续写经 chapter_repo 读最新 accepted/draft 正文注入 builder(core 不 import apps/api,仿 accept_op)。
预览仅 commit ledger 不写业务表(#3);只声明 tier(#2);system_prompt 进缓存前缀(#9)。
单测:4 生成器 generate 预览 + 续写读前文/草稿路由 + 扩写/降AI text 路由 + 拆书结构化;
更新 registry/strategy 计数测试(15 工具 / 6 策略)。门禁绿:ruff/format/mypy 196 files/pytest 168(api)+32(skills)。
2026-06-23 19:10:35 +02:00

210 lines
5.8 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.

"""T6 创作工具箱声明式描述符类型单测Wave-0 契约)。
仅校验描述符**类型**本身(无 TOOLBOX 注册表 seeding——那是 Wave A
- `ContextStrategy`注入策略字面量4 基础 + 续写/原文输入 2 竞品策略);
- `InputField`声明式表单字段label/type/required/default/help
- `IngestSpec`可选入库目标table 必须在 KNOWN_TABLESNone=纯预览);
- `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))
# Assert4 基础策略 + 竞品快赢 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