Files
writing-agent/tests/dialogue-history.test.ts
chenmoranzhi 9dc927282f 预设兼容:按真实抓包对齐装配与包装层,补上声明式工具与预设正则。
- 装配只认 prompt_order.enabled;squash_system_messages 只合并相邻 system(新增 system 档,连接符按酒馆用单换行)
- 包装层:new_chat_prompt、示例对话(new_example_chat_prompt + <START> 切段 + name)、角色卡三块与 personality_format/scenario_format、{{user}}/{{char}} 取值、任务契约并进预设条目
- 工具协议:按 ToolBindings / 伪工具协议数据声明真 tools(presetToolPolicy=preset),工具描述用到的条目不再重复作为消息发出
- 正则:promptOnly 作用于对话历史(含 depth 过滤)、写入时脚本改写回复;{{//}} 注释按配平扫描
- tools/st-request-diff:真假上游抓包对比工具、三份预设的机制结论与实测报告
2026-10-23 00:13:08 +08:00

204 lines
6.8 KiB
TypeScript
Raw Permalink 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 {
appendDialogueHistoryTurn,
parseHistoryMessages,
projectDialogueHistory,
buildHistoryFallback,
isDialogueHistoryRef,
} from "../src/skills/dialogue-history.js";
import { assembleWorkerContext } from "../src/skills/context-segments.js";
import { Blackboard } from "../src/blackboard/blackboard.js";
import {
contextOrderToSegments,
parseContextOrder,
applyContextOrderEdit,
synthesizeContextOrderFromWorkers,
} from "../src/skills/context-order.js";
describe("parseHistoryMessages", () => {
it("turns ## 用户 / ## 助手 blocks into real turns", () => {
const raw = appendDialogueHistoryTurn(
appendDialogueHistoryTurn("", { role: "助手", text: "门开了。" }),
{ role: "用户", text: "我推门。" },
);
expect(parseHistoryMessages(raw)).toEqual([
{ role: "assistant", content: "门开了。" },
{ role: "user", content: "我推门。" },
]);
});
it("accepts 【用户】 and 用户:single-line forms", () => {
expect(parseHistoryMessages("【用户】\n你好\n\n【助手】\n在。")).toEqual([
{ role: "user", content: "你好" },
{ role: "assistant", content: "在。" },
]);
expect(parseHistoryMessages("用户:你好\n助手:在。")).toEqual([
{ role: "user", content: "你好" },
{ role: "assistant", content: "在。" },
]);
});
it("keeps the projection note on the first turn", () => {
const turns = parseHistoryMessages(
"(仅最近 20 段,更早已省略)\n\n## 用户\n\n你好",
);
expect(turns).toHaveLength(1);
expect(turns[0]!.content).toContain("仅最近");
expect(turns[0]!.content).toContain("你好");
});
it("returns [] for text without role markers", () => {
expect(parseHistoryMessages("一段无结构的前情提要")).toEqual([]);
});
it("keeps a body line that looks like markdown heading", () => {
const raw = "## 用户\n\n我说## 这不是块头\n\n## 助手\n\n嗯";
const turns = parseHistoryMessages(raw);
expect(turns).toHaveLength(2);
expect(turns[0]!.content).toContain("这不是块头");
});
});
describe("dialogue history projection", () => {
it("recognizes history refs and projects by level", () => {
expect(isDialogueHistoryRef("对话.历史")).toBe(true);
expect(isDialogueHistoryRef("history")).toBe(true);
const long = Array.from({ length: 60 }, (_, i) => `## 用户\n\n第${i}句`).join(
"\n\n",
);
const summary = projectDialogueHistory(long, "summary");
expect(summary).toContain("仅最近");
// 裁到 n 段(summary = 20)
expect(summary.split("## 用户").length - 1).toBe(20);
expect(projectDialogueHistory(long, "fixed")).toContain("不注入");
});
it("keeps the whole window until it doubles(滞回,不每轮位移)", () => {
const turn = (n: number) =>
Array.from({ length: n }, (_, i) => `## 用户\n\n第${i}句`).join("\n\n");
// 20~40 段(summary 的 n=20,高水位=40)原样全给,前缀稳定
expect(projectDialogueHistory(turn(30), "summary")).not.toContain("仅最近");
expect(projectDialogueHistory(turn(30), "summary").split("## 用户").length - 1).toBe(
30,
);
// 超过高水位才裁,且一次裁到 20
expect(projectDialogueHistory(turn(41), "summary").split("## 用户").length - 1).toBe(
20,
);
});
it("assembles history tag in flat segment order", () => {
const bb = new Blackboard();
bb.write({
tag: "对话.历史",
content: appendDialogueHistoryTurn("", { role: "用户", text: "推门" }),
source: "test",
});
bb.write({ tag: "用户.最新输入", content: "再看一眼", source: "test" });
const text = assembleWorkerContext({
inputs: {},
segments: [
{
id: "p",
tier: "static",
tags: [],
label: "## persona",
inline: "GM",
projection: "fixed",
},
{
id: "h",
tier: "static",
tags: ["对话.历史"],
label: "## 对话.历史",
projection: "full",
},
{
id: "u",
tier: "dynamic",
tags: ["用户.最新输入"],
label: "## 用户.最新输入",
projection: "full",
},
],
blackboard: bb,
workerId: "world-simulator",
workerName: "主世界层",
outputTags: [],
});
const iPersona = text.indexOf("GM");
const iHist = text.indexOf("推门");
const iUser = text.indexOf("再看一眼");
expect(iPersona).toBeGreaterThanOrEqual(0);
expect(iHist).toBeGreaterThan(iPersona);
expect(iUser).toBeGreaterThan(iHist);
});
it("falls back to event stream when history empty", () => {
expect(
buildHistoryFallback({
eventStream: "门开了",
latestUser: "走进去",
}),
).toContain("门开了");
});
});
describe("flat context order", () => {
it("synthesizes history tag and moves across it", () => {
const doc = synthesizeContextOrderFromWorkers(
[
{
ref: "world-simulator",
name: "主世界层",
context: {
static: ["设计.叙事指南"],
dynamic: ["用户.最新输入"],
},
},
],
{ gm: true, narrator: true, perspective: false },
)!;
const gmSlot = doc.slots.find((s) => s.ref === "world-simulator")!;
expect(gmSlot.inserts.some((i) => i.ref === "对话.历史")).toBe(true);
const histIdx = gmSlot.inserts.findIndex((i) => i.ref === "对话.历史");
const moved = applyContextOrderEdit(doc, {
action: "set_anchor",
slotRef: "world-simulator",
index: gmSlot.inserts.length - 1,
anchor: "pre_history",
});
const movedGm = moved.slots.find((s) => s.ref === "world-simulator")!;
const newHist = movedGm.inserts.findIndex((i) => i.ref === "对话.历史");
const lastUser = movedGm.inserts.findIndex(
(i) => i.ref === "用户.最新输入",
);
expect(lastUser).toBeLessThan(newHist);
expect(histIdx).toBeGreaterThanOrEqual(0);
});
it("maps to segments in list order including history", () => {
const doc = parseContextOrder({
schema: "context-order.v1",
slots: [
{
ref: "world-simulator",
inserts: [
{ order: 0, ref: "worker.persona", projection: "fixed" },
{ order: 1, ref: "对话.历史", projection: "summary" },
{ order: 2, ref: "用户.最新输入", projection: "full" },
],
},
],
})!;
const segs = contextOrderToSegments({
slot: doc.slots[0]!,
personaText: "裁决",
});
expect(segs[0]!.inline).toContain("裁决");
expect(segs[1]!.tags).toEqual(["对话.历史"]);
expect(segs[2]!.tags).toEqual(["用户.最新输入"]);
});
});