Files
writing-agent/tests/preset-lab-workbench.test.ts
T
chenmoranzhi f3303038fe 新增预设工作台:编辑、版本树、结构检查、用例试跑放在同一页。
改预设要能一边写一边看装出来的 messages,还要多模型对照,所以把编辑与试跑拆成两个视图塞进一个页面;版本存完整快照、可分叉,用例全局共用一份、运行记录按预设存。顺带让读预设文件先剥 BOM,静态资源改成不缓存,免得改了脚本还要强刷。
2026-10-26 01:09:40 +08:00

221 lines
7.5 KiB
TypeScript
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.
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { Readable } from "node:stream";
import os from "node:os";
import path from "node:path";
import type { IncomingMessage, ServerResponse } from "node:http";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { getPreset } from "../src/preset/store.js";
import { createNativePreset } from "../src/preset/new-preset.js";
import type { PresetPackage } from "../src/types/preset.js";
vi.mock("../src/llm/client.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../src/llm/client.js")>();
return {
...actual,
OpenAiCompatibleProvider: class {
async complete(messages: unknown[], options?: { caller?: string }) {
return {
content: `mock:${options?.caller ?? "unknown"}`,
usage: { promptTokens: 1, completionTokens: 2, totalTokens: 3 },
model: "mock-model",
messages,
};
}
},
};
});
import { handlePresetLabApi } from "../src/server/preset-lab-handlers.js";
type Captured = { status: number; body: any };
async function call(
pathname: string,
init: { method?: string; body?: unknown; query?: string } = {},
): Promise<Captured> {
const raw = init.body === undefined ? "" : JSON.stringify(init.body);
const req = Readable.from(raw ? [Buffer.from(raw, "utf8")] : []) as unknown as IncomingMessage;
(req as any).method = init.method ?? "GET";
let status = 0;
let payload = "";
const res = {
writeHead(code: number) {
status = code;
},
end(chunk: string) {
payload = chunk ?? "";
},
} as unknown as ServerResponse;
const handled = await handlePresetLabApi(req, res, pathname, new URLSearchParams(init.query ?? ""));
expect(handled).toBe(true);
return { status, body: payload ? JSON.parse(payload) : null };
}
function writeProfiles(dir: string): void {
mkdirSync(dir, { recursive: true });
const now = new Date().toISOString();
writeFileSync(
path.join(dir, "profiles.json"),
JSON.stringify({
version: 1,
profiles: [
{
id: "p1",
name: "本地模型",
baseUrl: "https://example.invalid/v1",
apiKey: "test-key",
model: "mock-model",
createdAt: now,
updatedAt: now,
},
],
}),
"utf8",
);
}
let dir = "";
beforeEach(() => {
dir = mkdtempSync(path.join(os.tmpdir(), "preset-lab-workbench-"));
process.env.WRITING_AGENT_DATA_DIR = dir;
writeProfiles(dir);
});
afterEach(() => {
delete process.env.WRITING_AGENT_DATA_DIR;
rmSync(dir, { recursive: true, force: true });
vi.restoreAllMocks();
});
describe("工作台新建与写回", () => {
it("读取带 BOM 的预设文件", () => {
const presetsDir = path.join(dir, "presets");
mkdirSync(presetsDir, { recursive: true });
const preset = createNativePreset({ id: "bom", name: "BOM" });
writeFileSync(path.join(presetsDir, "bom.json"), `\uFEFF${JSON.stringify(preset)}`, "utf8");
expect(getPreset("bom")?.name).toBe("BOM");
});
it("新建原生预设并生成根版本", async () => {
const { status, body } = await call("/api/preset-lab/presets/create", {
method: "POST",
body: { name: "新预设", id: "fresh" },
});
expect(status).toBe(201);
expect(body.preset.id).toBe("fresh");
expect(body.preset.source).toBe("native");
expect(body.tree.nodes).toHaveLength(1);
expect(body.preset.prompts.some((p: any) => p.id === "chatHistory" && p.marker)).toBe(true);
expect(body.preset.prompts.some((p: any) => p.id === "currentTurn" && p.marker)).toBe(true);
expect(getPreset("fresh")?.name).toBe("新预设");
});
it("写回前拦截结构 error,force 才落盘", async () => {
const created = await call("/api/preset-lab/presets/create", {
method: "POST",
body: { name: "写回", id: "apply-me" },
});
const preset = created.body.preset as PresetPackage;
preset.prompts[0]!.content = "{{setvar::a::1";
const blocked = await call("/api/preset-lab/preset/apply", {
method: "POST",
body: { preset },
});
expect(blocked.status).toBe(400);
expect(blocked.body.error).toContain("结构检查");
const forced = await call("/api/preset-lab/preset/apply", {
method: "POST",
body: { preset, force: true },
});
expect(forced.status).toBe(200);
expect(getPreset("apply-me")?.prompts[0]!.content).toContain("setvar");
});
it("三花括号是合法的字面量花括号,不算未闭合宏", async () => {
const created = await call("/api/preset-lab/presets/create", {
method: "POST",
body: { name: "三花括号", id: "triple" },
});
const preset = created.body.preset as PresetPackage;
preset.prompts[0]!.content = "{{{setvar::SEMOD1::}}正文";
const checked = await call("/api/preset-lab/check", {
method: "POST",
body: { preset },
});
expect(checked.body.issues.filter((i: any) => i.level === "error")).toHaveLength(0);
});
it("重复引用同一模块是 error", async () => {
const created = await call("/api/preset-lab/presets/create", {
method: "POST",
body: { name: "重复", id: "dup-order" },
});
const preset = created.body.preset as PresetPackage;
preset.promptOrder.push({ ...preset.promptOrder[0]!, orderIndex: 99 });
const checked = await call("/api/preset-lab/check", {
method: "POST",
body: { preset },
});
expect(checked.body.issues.some((i: any) => i.level === "error" && i.message.includes("重复"))).toBe(true);
});
});
describe("草稿运行", () => {
it("run 使用请求里的草稿而不是旧版本文件", async () => {
const created = await call("/api/preset-lab/presets/create", {
method: "POST",
body: { name: "草稿", id: "draft-run" },
});
const preset = created.body.preset as PresetPackage;
preset.prompts[0]!.content = "草稿规则 {{char}}";
const group = await call("/api/preset-lab/groups/save", {
method: "POST",
body: {
presetId: preset.id,
name: "组",
cases: [{ name: "开门", charName: "林", userInput: "我推门" }],
},
});
const run = await call("/api/preset-lab/run", {
method: "POST",
body: {
presetId: preset.id,
groupId: group.body.id,
profileIds: ["p1"],
preset,
},
});
expect(run.status).toBe(200);
expect(run.body.results).toHaveLength(1);
expect(run.body.results[0].status).toBe("done");
expect(run.body.results[0].caseName).toBe("开门");
expect(run.body.results[0].content).toContain("mock:preset-test:p1");
expect(run.body.requests[0].messages.map((m: any) => m.content).join("\n")).toContain("草稿规则 林");
expect(run.body.requests[0].messages.map((m: any) => m.content).join("\n")).toContain("我推门");
const runs = await call("/api/preset-lab/runs", { query: `presetId=${preset.id}` });
expect(runs.body.runs).toHaveLength(1);
expect(runs.body.runs[0].id).toBe(run.body.id);
const one = await call("/api/preset-lab/run", {
query: `presetId=${preset.id}&runId=${run.body.id}`,
});
expect(one.body.results[0].caseName).toBe("开门");
const marked = await call("/api/preset-lab/verdict", {
method: "POST",
body: {
presetId: preset.id,
runId: run.body.id,
profileId: "p1",
caseId: run.body.results[0].caseId,
verdict: "success",
},
});
expect(marked.body.results[0].verdict).toBe("success");
});
});