Files
writing-agent/tests/prune-creation-messages.test.ts

311 lines
9.2 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 { describe, expect, it } from "vitest";
import {
foldRunProcessMessages,
pruneCreationUnitMessages,
trimCreationDialogueMessages,
selectCreationDialogueForAi,
buildCreationDialogueTranscript,
type PrunableChatMessage,
} from "../src/server/prune-creation-messages.js";
import {
isDesignUnitArtifact,
isFinalWorkerSetArtifact,
isWorkerSetReadyForFinal,
listCreationUnits,
nextCreationUnitId,
} from "../src/skills/creation-units.js";
import type { ParsedWorkerSet } from "../src/skills/worker-set-parse.js";
import { Blackboard } from "../src/blackboard/blackboard.js";
import { compressAfterWorkerAccept } from "../src/runtime/compress-after-worker.js";
import { hasAcceptedWorkerSet } from "../src/skills/worker-declaration.js";
import { createSession } from "../src/runtime/phase-machine.js";
function msg(
partial: Partial<PrunableChatMessage> & Pick<PrunableChatMessage, "id" | "role" | "text">,
): PrunableChatMessage {
return { createdAt: "2026-01-01T00:00:00.000Z", ...partial };
}
describe("pruneCreationUnitMessages", () => {
it("is a no-op: does not delete user-visible messages", () => {
const messages: PrunableChatMessage[] = [
msg({ id: "1", role: "user", text: "我想玩校园末日", kind: "user_input" }),
msg({
id: "2",
role: "system",
text: "[Worker] design-core 运行中",
kind: "worker_running",
actor: "design-core",
}),
msg({
id: "3",
role: "system",
text: "[Worker] design-core 提问",
kind: "worker_questions",
actor: "design-core",
}),
msg({ id: "4", role: "user", text: "单角代入", kind: "user_input" }),
msg({
id: "5",
role: "system",
text: "[Worker] design-core 草稿",
kind: "worker_output",
actor: "design-core",
body: "{ draft }",
}),
msg({
id: "6",
role: "system",
text: "[Worker] design-core 定稿",
kind: "worker_output",
actor: "design-core",
body: "{ final }",
}),
];
const idsBefore = messages.map((m) => m.id);
const result = pruneCreationUnitMessages(messages, "design-core");
expect(result).toEqual({ removedCount: 0, productKept: false });
expect(messages.map((m) => m.id)).toEqual(idsBefore);
});
});
describe("selectCreationDialogueForAi / buildCreationDialogueTranscript", () => {
it("keeps full messages for browsing; AI transcript only last AI + all users", () => {
const messages: PrunableChatMessage[] = [
msg({ id: "u1", role: "user", text: "荒野求生", kind: "user_input" }),
msg({
id: "a1",
role: "system",
text: "draft1",
kind: "worker_output",
actor: "design-core",
}),
msg({ id: "u2", role: "user", text: "无其它幸存者", kind: "user_input" }),
msg({
id: "run",
role: "system",
text: "running",
kind: "worker_running",
actor: "design-core",
}),
msg({
id: "a2",
role: "system",
text: "draft2",
kind: "worker_output",
actor: "design-core",
body: "{ v2 }",
}),
];
// 用户浏览:原数组不动
const { removedCount } = trimCreationDialogueMessages(messages);
expect(removedCount).toBe(0);
expect(messages.map((m) => m.id)).toEqual(["u1", "a1", "u2", "run", "a2"]);
// 拼给 AI用户全量 + 最后一次 AI
expect(selectCreationDialogueForAi(messages).map((m) => m.id)).toEqual([
"u1",
"u2",
"a2",
]);
const transcript = buildCreationDialogueTranscript(messages);
expect(transcript).toContain("荒野求生");
expect(transcript).toContain("无其它幸存者");
expect(transcript).toContain("{ v2 }");
expect(transcript).not.toContain("draft1");
expect(transcript).not.toContain("running");
});
});
describe("foldRunProcessMessages", () => {
it("marks process messages compressed without deleting", () => {
const messages: PrunableChatMessage[] = [
msg({
id: "1",
role: "system",
text: "q",
kind: "worker_questions",
actor: "narrator",
}),
msg({
id: "2",
role: "system",
text: "out",
kind: "worker_output",
actor: "narrator",
}),
];
foldRunProcessMessages(messages, "narrator");
expect(messages).toHaveLength(2);
expect(messages.every((m) => m.compressed)).toBe(true);
});
});
describe("listCreationUnits", () => {
it("only lists filled fixed topics by default (not empty catalog blanks)", () => {
const parsed: ParsedWorkerSet = {
interaction: {
user_stance: "单角",
system_role: "世界",
output: "叙事",
},
narrative_guide: "世界不有求必应",
workers: [
{
ref: "narrator",
duty: "转述",
rationale: "可读",
acceptance: "review",
},
{ ref: null, duty: "缺口", gap: "待定" },
],
resident_context: [
{ id: "tone", content: "文风克制", position: "static" },
],
};
const units = listCreationUnits(parsed, {
acceptedUnitIds: ["fixed:interaction", "skeleton:interaction"],
currentUnitId: "worker:narrator",
});
expect(units.map((u) => u.id)).toEqual([
"phase:core",
"fixed:narrative_guide",
"worker:narrator",
"worker:#2",
"resident:tone",
]);
expect(units.find((u) => u.id === "phase:core")?.accepted).toBe(true);
expect(units.every((u) => u.id !== "fixed:input_protocol" || u.filled)).toBe(
true,
);
expect(units.find((u) => u.id === "fixed:input_protocol")).toBeUndefined();
expect(units.find((u) => u.id === "worker:narrator")?.current).toBe(true);
});
it("nextCreationUnitId prefers foundation fixed tags before workers", () => {
const parsed: ParsedWorkerSet = {
interaction: {
user_stance: "单角",
system_role: "世界",
output: "叙事",
},
workers: [{ ref: "narrator", duty: "转述", rationale: "r" }],
};
expect(nextCreationUnitId(parsed, ["fixed:interaction"])).toBe(
"fixed:narrative_guide",
);
});
it("nextCreationUnitId reaches workers after weighty fixed accepted", () => {
const parsed: ParsedWorkerSet = {
interaction: {
user_stance: "单角",
system_role: "世界",
output: "叙事",
},
narrative_guide: "边界清晰",
workers: [{ ref: "narrator", duty: "转述", rationale: "r" }],
};
expect(
nextCreationUnitId(parsed, [
"fixed:interaction",
"fixed:narrative_guide",
"fixed:aesthetics",
"fixed:input_protocol",
"fixed:core_premises",
]),
).toBe("worker:narrator");
});
});
describe("artifact classification", () => {
it("distinguishes unit vs final", () => {
expect(
isDesignUnitArtifact({
workerId: "design-core",
outputTags: ["设计.worker集.草稿"],
}),
).toBe(true);
expect(
isFinalWorkerSetArtifact({
workerId: "design-core",
outputTags: ["设计.worker集"],
}),
).toBe(true);
expect(
isDesignUnitArtifact({
workerId: "design-core",
outputTags: ["设计.worker集", "设计.worker集.草稿"],
}),
).toBe(false);
});
});
describe("hasAcceptedWorkerSet", () => {
it("does not treat draft-only accepted artifact as ready", () => {
const session = {
...createSession(),
artifacts: [
{
id: "a1",
workerId: "design-core",
outputTags: ["设计.worker集.草稿"],
status: "accepted" as const,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
],
};
expect(hasAcceptedWorkerSet(session)).toBe(false);
});
});
describe("compressAfterWorkerAccept unit mode", () => {
it("keeps draft active and does not write brief as final", () => {
const bb = new Blackboard();
bb.write({
tag: "设计.worker集.草稿",
content: '{ "workers": [{ "ref": "narrator" }] }',
source: "design-core",
});
bb.write({
tag: "用户.worker答复",
content: "过程问答",
source: "user",
});
const result = compressAfterWorkerAccept({
blackboard: bb,
workerId: "design-core",
outputTags: ["设计.worker集.草稿"],
summary: "单位 worker:narrator",
mode: "unit",
});
expect(result.archivedTags).toContain("用户.worker答复");
expect(bb.getContentByTag("设计.worker集.草稿")).toContain("narrator");
expect(bb.getLatestByTag("设计.worker集.草稿")?.metadata?.role).not.toBe(
"archived",
);
});
});
describe("isWorkerSetReadyForFinal", () => {
it("requires accepted filled units", () => {
const parsed: ParsedWorkerSet = {
interaction: {
user_stance: "单角",
system_role: "世界",
output: "叙事",
},
workers: [{ ref: "narrator", duty: "转述", rationale: "r" }],
};
expect(isWorkerSetReadyForFinal(parsed, []).ready).toBe(false);
expect(
isWorkerSetReadyForFinal(parsed, [
"fixed:interaction",
"worker:narrator",
]).ready,
).toBe(true);
});
});