Files
writing-agent/tests/experience-anchor.test.ts
T

204 lines
7.0 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { AgentDriver } from "../src/runtime/driver.js";
import { buildDictateMessages, runDictateTurn } from "../src/dictate/index.js";
import { scoreExperienceAnchor } from "../src/dictate/experience-anchor.js";
const anchored = {
体验锚定: {
站位: "完全代入",
核心感觉: "垄断生存资源的掌控感",
轮转停点: "用户要做决定时停笔等用户",
},
};
describe("experience anchor score", () => {
it("scores the three fields and ignores self-score", () => {
const onlySelf = scoreExperienceAnchor(
JSON.stringify({
自评: { 维度: [{ 名: "美学纲领", 分数: 10 }] },
正文: { 设定逻辑: { 完备度判断: { 核心状态: "核心明确", 判断依据: "我觉得够了" } } },
}),
);
expect(onlySelf.anchored).toBe(false);
expect(onlySelf.passed).toBe(0);
const full = scoreExperienceAnchor(JSON.stringify(anchored));
expect(full.anchored).toBe(true);
expect(full.passed).toBe(3);
expect(full.items.map((item) => item.id)).toEqual(["站位", "核心感觉", "轮转停点"]);
});
it("rejects placeholders and unanswered stance", () => {
const score = scoreExperienceAnchor(
JSON.stringify({
体验锚定: {
站位: "代入还是旁观",
核心感觉: "未定",
轮转停点: "等用户",
},
}),
);
expect(score.anchored).toBe(false);
expect(score.items.every((item) => !item.ok)).toBe(true);
});
it("falls back to aesthetics body fields and prose labels", () => {
const fromBody = scoreExperienceAnchor(
JSON.stringify({
正文: {
设定逻辑: {
参与方式: { 用户与user关系: { 结论: "完全代入" } },
内容维度: { 核心感觉: { 已知: "求生时的紧张感" } },
},
交互范式: {
描写权限: { 思考与抉择: "要替用户做决定时必须停笔等用户" },
},
},
}),
);
expect(fromBody.anchored).toBe(true);
const prose = scoreExperienceAnchor(
["站位:旁观", "核心感觉:看棋子互相算计", "轮转停点:场面收束前停笔等用户下令"].join("\n"),
);
expect(prose.anchored).toBe(true);
expect(prose.items.find((item) => item.id === "站位")?.value).toBe("旁观");
});
it("does not inject a fixed anchor gate into dictate context", () => {
const body =
buildDictateMessages({
systemPrompt: "SYS",
products: [
{
tag: "设计.美学纲领与交互范式",
content: JSON.stringify({
体验锚定: { 站位: "完全代入", 核心感觉: "未定", 轮转停点: "未定" },
}),
},
],
dialogue: [],
})[1]?.content ?? "";
expect(body).toContain("当前产物");
expect(body).toContain("设计.美学纲领与交互范式");
expect(body).not.toContain("体验锚定(程序打分");
expect(body).not.toContain("不得 insert 开场白");
});
it("does not block opening on a fixed 3/3 anchor count", async () => {
const products = new Map<string, string>();
const driver: AgentDriver = {
async run(input) {
const first = await input.handleStep([
{
id: "o1",
name: "insert",
arguments: JSON.stringify({ position: "设计.开场白", content: "@玩家睁眼。" }),
},
]);
expect(first.kind).toBe("continue");
if (first.kind === "continue") {
const payload = JSON.parse(first.results[0]!.content) as {
ok?: boolean;
error?: string;
};
expect(payload.ok).toBe(true);
expect(payload.error).toBeUndefined();
}
const sameStep = await input.handleStep([
{
id: "a1",
name: "insert",
arguments: JSON.stringify({
position: "设计.美学纲领与交互范式",
content: JSON.stringify(anchored),
}),
},
{
id: "o2",
name: "insert",
arguments: JSON.stringify({ position: "设计.开场白", content: "@玩家睁眼。" }),
},
]);
expect(sameStep.kind).toBe("continue");
if (sameStep.kind === "continue") {
const aesthetics = JSON.parse(sameStep.results[0]!.content) as {
ok?: boolean;
experience_anchor?: unknown;
};
const opening = JSON.parse(sameStep.results[1]!.content) as { ok?: boolean };
expect(aesthetics.ok).toBe(true);
expect(aesthetics.experience_anchor).toBeUndefined();
expect(opening.ok).toBe(true);
}
return { iterations: 1, stop: { kind: "text", content: "已补三件并写开场。" } };
},
};
const result = await runDictateTurn({
llm: {} as never,
recipeName: "快穿短局",
dialogue: [{ role: "user", text: "先开场" }],
driver,
handlers: {
listProducts: () => [...products.entries()].map(([tag, content]) => ({ tag, content })),
writeProduct: (tag, content) => {
products.set(tag, content);
},
deleteProduct: (tag) => products.delete(tag),
readTag: () => undefined,
writeTag: () => undefined,
deleteTag: () => false,
clearDialogue: () => undefined,
},
});
expect(products.has("设计.开场白")).toBe(true);
expect(result.wroteTags).toContain("设计.美学纲领与交互范式");
expect(result.wroteTags).toContain("设计.开场白");
});
it("does not block opening for recipes that skip the aesthetics gate", async () => {
const products = new Map<string, string>();
const driver: AgentDriver = {
async run(input) {
const outcome = await input.handleStep([
{
id: "o1",
name: "insert",
arguments: JSON.stringify({ position: "设计.开场白", content: "@玩家睁眼。" }),
},
]);
expect(outcome.kind).toBe("continue");
if (outcome.kind === "continue") {
const payload = JSON.parse(outcome.results[0]!.content) as { ok?: boolean; error?: string };
expect(payload.ok).toBe(true);
expect(payload.error).toBeUndefined();
}
return { iterations: 1, stop: { kind: "text", content: "开场已写。" } };
},
};
await runDictateTurn({
llm: {} as never,
recipeName: "文本生成器",
dialogue: [{ role: "user", text: "来一段开篇" }],
driver,
handlers: {
listProducts: () => [...products.entries()].map(([tag, content]) => ({ tag, content })),
writeProduct: (tag, content) => {
products.set(tag, content);
},
deleteProduct: () => false,
readTag: () => undefined,
writeTag: () => undefined,
deleteTag: () => false,
clearDialogue: () => undefined,
},
});
expect(products.get("设计.开场白")).toContain("@玩家");
});
});