写作工作台重构剩余 3 项(多 agent 并行构建各自新文件,主线统一集成): - WFW-5 内容感知书名(#6):buildManuscriptExcerpt 取正文首尾摘录(硬上界控 token); GeneratorRunner 加 manuscriptText,有 brief 字段时出现「用当前正文」按钮把摘录填入 brief,让 book-title 等据正文反推。纯前端零后端;HITL——只填表单,生成仍需作者触发。 - WFW-6 上下文速查抽屉(#7):ContextDrawer 视口无关右侧 slide-over,设定库/大纲完整 只读速查 + 伏笔/规则/文风摘要跳链;底栏「速查」按钮触发。加法式、CONTEXT_DRAWER_ENABLED 一键回滚、旧侧栏/全宽页路由全保留。 - WFW-7 genre 采集 + 无大纲降级:项目无题材时首次写章前弹 GenrePicker 采集(临时并入 本次 directive 不落库);chapters 为空时编辑器显式提示「尚无大纲,将按设定自由生成」。 各项 TDD:manuscriptExcerpt/useGenreGate/contextDrawer/useContextDrawer 共 32 新单测。 门禁全绿:vitest 629 / tsc / lint / build / coverage 95.43%。
113 lines
3.1 KiB
TypeScript
113 lines
3.1 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import type { OutlineChapterView, WorldEntityCardView } from "@/lib/api/types";
|
|
import {
|
|
CONTEXT_TABS,
|
|
contextTabHref,
|
|
toCodexGroups,
|
|
toOutlineRows,
|
|
} from "./contextDrawer";
|
|
|
|
const entity = (
|
|
type: string,
|
|
name: string,
|
|
rules: string[] = [],
|
|
): WorldEntityCardView => ({ type, name, rules });
|
|
|
|
describe("contextTabHref", () => {
|
|
it("按 Tab key 拼出完整编辑页路由", () => {
|
|
// Arrange / Act / Assert
|
|
expect(contextTabHref("p1", "codex")).toBe("/projects/p1/codex");
|
|
expect(contextTabHref("p1", "foreshadow")).toBe("/projects/p1/foreshadow");
|
|
expect(contextTabHref("p1", "style")).toBe("/projects/p1/style");
|
|
});
|
|
|
|
it("五个 Tab 覆盖全部读为主入口", () => {
|
|
expect(CONTEXT_TABS.map((t) => t.key)).toEqual([
|
|
"codex",
|
|
"outline",
|
|
"foreshadow",
|
|
"rules",
|
|
"style",
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("toCodexGroups", () => {
|
|
it("空列表返回空分组", () => {
|
|
expect(toCodexGroups([])).toEqual([]);
|
|
});
|
|
|
|
it("按 type 归组并保持首次出现顺序", () => {
|
|
const entities = [
|
|
entity("势力", "雾港议会"),
|
|
entity("地理", "北境"),
|
|
entity("势力", "红手会"),
|
|
];
|
|
|
|
const groups = toCodexGroups(entities);
|
|
|
|
expect(groups.map((g) => g.type)).toEqual(["势力", "地理"]);
|
|
expect(groups[0]?.entities.map((e) => e.name)).toEqual([
|
|
"雾港议会",
|
|
"红手会",
|
|
]);
|
|
expect(groups[1]?.entities.map((e) => e.name)).toEqual(["北境"]);
|
|
});
|
|
|
|
it("空白/缺省 type 归入「未分类」", () => {
|
|
const groups = toCodexGroups([entity(" ", "无名之物")]);
|
|
expect(groups[0]?.type).toBe("未分类");
|
|
});
|
|
|
|
it("不修改入参数组", () => {
|
|
const entities = [entity("势力", "甲"), entity("势力", "乙")];
|
|
const snapshot = [...entities];
|
|
toCodexGroups(entities);
|
|
expect(entities).toEqual(snapshot);
|
|
});
|
|
});
|
|
|
|
describe("toOutlineRows", () => {
|
|
const chapter = (
|
|
no: number,
|
|
volume: number,
|
|
beats: string[],
|
|
codes: string[] = [],
|
|
): OutlineChapterView => ({
|
|
no,
|
|
volume,
|
|
beats,
|
|
foreshadow_windows: codes.map((code) => ({ code })),
|
|
});
|
|
|
|
it("空列表返回空数组", () => {
|
|
expect(toOutlineRows([])).toEqual([]);
|
|
});
|
|
|
|
it("按章号升序整形并抽出节拍与伏笔代号", () => {
|
|
const rows = toOutlineRows([
|
|
chapter(2, 1, ["决战"], ["F2"]),
|
|
chapter(1, 1, ["开场", "引入"], ["F1"]),
|
|
]);
|
|
|
|
expect(rows.map((r) => r.no)).toEqual([1, 2]);
|
|
expect(rows[0]?.beats).toEqual(["开场", "引入"]);
|
|
expect(rows[0]?.foreshadowCodes).toEqual(["F1"]);
|
|
expect(rows[1]?.foreshadowCodes).toEqual(["F2"]);
|
|
});
|
|
|
|
it("缺省 beats / foreshadow_windows 回退为空数组", () => {
|
|
const rows = toOutlineRows([{ no: 1, volume: 1 }]);
|
|
expect(rows[0]?.beats).toEqual([]);
|
|
expect(rows[0]?.foreshadowCodes).toEqual([]);
|
|
});
|
|
|
|
it("不修改入参数组", () => {
|
|
const chapters = [chapter(2, 1, []), chapter(1, 1, [])];
|
|
const snapshot = chapters.map((c) => c.no);
|
|
toOutlineRows(chapters);
|
|
expect(chapters.map((c) => c.no)).toEqual(snapshot);
|
|
});
|
|
});
|