通用执行路径驱动全部生成器("加生成器=加一份声明"):
- @llm: ww_agents +7 输出 schema + 7 spec(book-title/blurb/name/golden-finger/
glossary/opening/fine-outline,只声明 tier)+ build_outline_chapter_context
- @backend: ww_skills GeneratorTool 描述符 + TOOLBOX(11) + get_tool;3 通用端点
GET /skills/toolbox · POST .../skills/{tool_key}/generate(预览不写库,仅记账) ·
POST .../ingest(复用 continuity 409 + partition_writes 白名单);纯 context 派发
- @frontend: 工具箱落地页 RSC + 声明驱动 GeneratorRunner + lib/toolbox 纯函数
+ LeftNav「工具箱」+ ⌘K nav-toolbox/action-gen-*;legacy 3 跳现页
- @qa: tests/test_t6_toolbox_e2e.py 5 用例真 pg + mock 网关零 token,无端点 bug
- P2 收尾: 限流→decisions.md 记延后(单用户原型);noopener/Committable 早已修
守不变量 #2(只声明 tier)/#3(预览不写库,入库经验收 gate)/#9(缓存前缀)。无 DB 迁移。
门禁绿: 后端 ruff/format/mypy 195/alembic 无漂移/pytest 583;前端 lint/tsc/vitest 279/build。
spec 回写 PRODUCT_SPEC §7 + ARCHITECTURE §7.2 端点表。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
170 lines
5.1 KiB
TypeScript
170 lines
5.1 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import type { ToolDescriptorView, ToolInputFieldView } from "@/lib/api/types";
|
|
import {
|
|
buildGenerateRequest,
|
|
initialFieldValues,
|
|
isLegacyTool,
|
|
mapPreview,
|
|
missingRequiredFields,
|
|
previewRows,
|
|
resolveLegacyRoute,
|
|
} from "./toolbox";
|
|
|
|
const field = (over: Partial<ToolInputFieldView> = {}): ToolInputFieldView => ({
|
|
name: "brief",
|
|
label: "需求",
|
|
type: "textarea",
|
|
required: true,
|
|
...over,
|
|
});
|
|
|
|
describe("initialFieldValues", () => {
|
|
it("seeds defaults and empty strings", () => {
|
|
const values = initialFieldValues([
|
|
field({ name: "brief", default: undefined }),
|
|
field({ name: "count", type: "number", default: "3", required: false }),
|
|
]);
|
|
expect(values).toEqual({ brief: "", count: "3" });
|
|
});
|
|
|
|
it("handles undefined fields", () => {
|
|
expect(initialFieldValues(undefined)).toEqual({});
|
|
});
|
|
});
|
|
|
|
describe("missingRequiredFields", () => {
|
|
it("reports labels of blank required fields only", () => {
|
|
const fields = [
|
|
field({ name: "brief", label: "需求", required: true }),
|
|
field({ name: "count", label: "数量", required: false }),
|
|
];
|
|
expect(missingRequiredFields(fields, { brief: " ", count: "" })).toEqual([
|
|
"需求",
|
|
]);
|
|
});
|
|
|
|
it("passes when all required present", () => {
|
|
expect(
|
|
missingRequiredFields([field()], { brief: "一个脑洞" }),
|
|
).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("buildGenerateRequest", () => {
|
|
it("trims brief and maps known number fields", () => {
|
|
expect(
|
|
buildGenerateRequest({ brief: " 修真 ", count: "5", chapter_no: "3" }),
|
|
).toEqual({ brief: "修真", count: 5, chapter_no: 3 });
|
|
});
|
|
|
|
it("omits blank/invalid numbers and empty kind", () => {
|
|
expect(
|
|
buildGenerateRequest({ brief: "x", count: "", chapter_no: "abc", kind: " " }),
|
|
).toEqual({ brief: "x" });
|
|
});
|
|
|
|
it("includes kind when present", () => {
|
|
expect(buildGenerateRequest({ brief: "x", kind: "门派" })).toEqual({
|
|
brief: "x",
|
|
kind: "门派",
|
|
});
|
|
});
|
|
|
|
it("defaults brief to empty string", () => {
|
|
expect(buildGenerateRequest({})).toEqual({ brief: "" });
|
|
});
|
|
});
|
|
|
|
describe("previewRows", () => {
|
|
it("picks the first array field as records", () => {
|
|
const rows = previewRows({ ideas: [{ premise: "a" }, { premise: "b" }] });
|
|
expect(rows).toHaveLength(2);
|
|
expect(rows[0]).toEqual({ premise: "a" });
|
|
});
|
|
|
|
it("returns empty for missing/non-array", () => {
|
|
expect(previewRows(undefined)).toEqual([]);
|
|
expect(previewRows({ note: "x" })).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("mapPreview", () => {
|
|
it("maps IdeaListResult to heading + fields", () => {
|
|
const model = mapPreview("IdeaListResult", {
|
|
ideas: [{ premise: "废柴逆袭", hook: "系统觉醒", genre_fit: "玄幻" }],
|
|
});
|
|
expect(model.kind).toBe("IdeaListResult");
|
|
expect(model.items[0]?.heading).toBe("废柴逆袭");
|
|
expect(model.items[0]?.fields).toEqual([
|
|
{ label: "爽点", value: "系统觉醒" },
|
|
{ label: "题材契合", value: "玄幻" },
|
|
]);
|
|
});
|
|
|
|
it("maps BlurbResult text into body and angle into heading", () => {
|
|
const model = mapPreview("BlurbResult", {
|
|
variants: [{ angle: "悬念向", text: "一段简介" }],
|
|
});
|
|
expect(model.items[0]?.heading).toBe("悬念向");
|
|
expect(model.items[0]?.body).toBe("一段简介");
|
|
});
|
|
|
|
it("maps GlossaryResult rules into a joined field", () => {
|
|
const model = mapPreview("GlossaryResult", {
|
|
terms: [{ name: "灵气", type: "概念", rules: ["不可逆", "有上限"] }],
|
|
});
|
|
const ruleField = model.items[0]?.fields.find((f) => f.label === "硬规则");
|
|
expect(ruleField?.value).toBe("不可逆;有上限");
|
|
});
|
|
|
|
it("maps OpeningResult as pure body", () => {
|
|
const model = mapPreview("OpeningResult", {
|
|
variants: [{ text: "开篇第一段" }],
|
|
});
|
|
expect(model.items[0]?.heading).toBeNull();
|
|
expect(model.items[0]?.body).toBe("开篇第一段");
|
|
});
|
|
|
|
it("falls back gracefully for unknown output_kind", () => {
|
|
const model = mapPreview("MysteryResult", {
|
|
things: [{ a: "first", b: "second" }],
|
|
});
|
|
expect(model.items[0]?.heading).toBe("first");
|
|
expect(model.items[0]?.fields).toEqual([{ label: "b", value: "second" }]);
|
|
});
|
|
|
|
it("yields no items for empty preview", () => {
|
|
expect(mapPreview("IdeaListResult", undefined).items).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("resolveLegacyRoute", () => {
|
|
it("substitutes the {id} placeholder", () => {
|
|
expect(resolveLegacyRoute("/projects/{id}/codex?gen=world", "p9")).toBe(
|
|
"/projects/p9/codex?gen=world",
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("isLegacyTool", () => {
|
|
const tool = (over: Partial<ToolDescriptorView>): ToolDescriptorView => ({
|
|
key: "k",
|
|
title: "t",
|
|
subtitle: "s",
|
|
is_legacy: false,
|
|
ingestable: false,
|
|
...over,
|
|
});
|
|
|
|
it("is true only with legacy flag and a route", () => {
|
|
expect(
|
|
isLegacyTool(tool({ is_legacy: true, legacy_route: "/x/{id}" })),
|
|
).toBe(true);
|
|
expect(isLegacyTool(tool({ is_legacy: true }))).toBe(false);
|
|
expect(
|
|
isLegacyTool(tool({ is_legacy: false, legacy_route: "/x/{id}" })),
|
|
).toBe(false);
|
|
});
|
|
});
|