Files
writer-work-flow/apps/web/lib/workbench/injection.test.ts
Yaojia Wang 6449c3103a feat(ux): F1 注入面板可控版 — 📌置顶/✕排除/近N章步进器 + 已排除恢复
- injection.ts 加 override 纯函数(currentOverride/withPinToggled/withExcluded/withRestored/withRecentN/isPinned,钳 1..20)+ author_pin 徽标
- useInjection 加 saving 态 + togglePin/exclude/restore/setRecentN,PUT 后以服务端确定结果为准(不变量 #6);写失败可读文案、不改本地态(天然回滚)
- ChapterAssistant:每实体置顶/排除按钮 + 「近 N 章」步进器 + 「已排除」恢复区
- 13 新 vitest(override 纯函数);前端门禁绿(lint/tsc/vitest 183/build)

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

104 lines
3.1 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
currentOverride,
isPinned,
kindLabel,
reasonLabel,
withExcluded,
withPinToggled,
withRecentN,
withRestored,
type InjectionOverrideRequest,
type InjectionResponse,
} from "./injection";
const REF = { kind: "character", name: "林动" } as const;
const emptyOverride = (): InjectionOverrideRequest => ({
pinned: [],
excluded: [],
recent_n: 5,
});
describe("reasonLabel", () => {
it("maps each known selection reason to a Chinese badge", () => {
expect(reasonLabel("explicit_beat")).toBe("本章点名");
expect(reasonLabel("main_character")).toBe("主角常驻");
expect(reasonLabel("recent_digest")).toBe("近章出现");
expect(reasonLabel("foreshadow_window")).toBe("伏笔窗口");
expect(reasonLabel("author_pin")).toBe("作者置顶");
});
it("falls back to the raw code for an unknown reason", () => {
expect(reasonLabel("future_reason")).toBe("future_reason");
});
});
describe("kindLabel", () => {
it("maps entity kinds to Chinese", () => {
expect(kindLabel("character")).toBe("角色");
expect(kindLabel("world_entity")).toBe("设定");
});
it("falls back to the raw kind when unknown", () => {
expect(kindLabel("mystery")).toBe("mystery");
});
});
describe("currentOverride", () => {
it("extracts pinned/excluded/recent_n from a response", () => {
const data = {
project_id: "p",
chapter_no: 1,
selected: [],
recent_n: 3,
pinned: [REF],
excluded: [{ kind: "world_entity", name: "玄铁" }],
} as unknown as InjectionResponse;
expect(currentOverride(data)).toEqual({
pinned: [REF],
excluded: [{ kind: "world_entity", name: "玄铁" }],
recent_n: 3,
});
});
});
describe("withPinToggled", () => {
it("pins an unpinned ref and removes it from excluded", () => {
const next = withPinToggled(withExcluded(emptyOverride(), REF), REF);
expect(isPinned(next, REF)).toBe(true);
expect(next.excluded).toEqual([]);
});
it("unpins an already-pinned ref", () => {
const pinned = withPinToggled(emptyOverride(), REF);
const next = withPinToggled(pinned, REF);
expect(isPinned(next, REF)).toBe(false);
});
});
describe("withExcluded / withRestored", () => {
it("excludes a ref, removing it from pinned, then restores it", () => {
const pinnedThenExcluded = withExcluded(withPinToggled(emptyOverride(), REF), REF);
expect(isPinned(pinnedThenExcluded, REF)).toBe(false);
expect(pinnedThenExcluded.excluded).toEqual([REF]);
const restored = withRestored(pinnedThenExcluded, REF);
expect(restored.excluded).toEqual([]);
});
it("is idempotent when excluding twice", () => {
const once = withExcluded(emptyOverride(), REF);
const twice = withExcluded(once, REF);
expect(twice.excluded).toEqual([REF]);
});
});
describe("withRecentN", () => {
it("clamps below the minimum and above the maximum", () => {
expect(withRecentN(emptyOverride(), 0).recent_n).toBe(1);
expect(withRecentN(emptyOverride(), 999).recent_n).toBe(20);
expect(withRecentN(emptyOverride(), 7).recent_n).toBe(7);
});
});