133 lines
4.3 KiB
JavaScript
133 lines
4.3 KiB
JavaScript
|
|
/*
|
||
|
|
* 上下文窗口拼接 — buildContextWindow 单元测试
|
||
|
|
* 守护: 铸渊 · ICE-GL-ZY001
|
||
|
|
*
|
||
|
|
* 起因 (2026-05-09 冰朔点透):
|
||
|
|
* "模型本身没有记忆, 一轮对话都记不住, 是背后的 Agent 一直给他不断的喂上下文"
|
||
|
|
* PR-5 并行修复点: 这一层补丁补的就是这个.
|
||
|
|
*/
|
||
|
|
"use strict";
|
||
|
|
|
||
|
|
const test = require("node:test");
|
||
|
|
const assert = require("node:assert/strict");
|
||
|
|
|
||
|
|
const { buildContextWindow, DEFAULT_TURNS, DEFAULT_CHAR_CAP } = require("../lib/context-window");
|
||
|
|
|
||
|
|
function make(role, content) {
|
||
|
|
return { role, content };
|
||
|
|
}
|
||
|
|
|
||
|
|
test("空输入 → 空输出", () => {
|
||
|
|
const r = buildContextWindow([]);
|
||
|
|
assert.deepEqual(r.messages, []);
|
||
|
|
assert.equal(r.dropped, 0);
|
||
|
|
assert.equal(r.total_chars, 0);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("单轮对话 (只 user) → 原样返回", () => {
|
||
|
|
const r = buildContextWindow([make("user", "你好")]);
|
||
|
|
assert.deepEqual(r.messages, [make("user", "你好")]);
|
||
|
|
assert.equal(r.total_chars, 2);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("多轮对话 → 全部保留 (远小于 cap)", () => {
|
||
|
|
const dbMsgs = [
|
||
|
|
make("user", "你叫什么"),
|
||
|
|
make("assistant", "我叫曜冥"),
|
||
|
|
make("user", "你刚刚说啥来着")
|
||
|
|
];
|
||
|
|
const r = buildContextWindow(dbMsgs);
|
||
|
|
assert.equal(r.messages.length, 3);
|
||
|
|
// 模型现在能看到完整上下文 — "你刚刚说啥来着" 不再是孤儿
|
||
|
|
assert.equal(r.messages[2].content, "你刚刚说啥来着");
|
||
|
|
});
|
||
|
|
|
||
|
|
test("system 消息一律剥 (cc-002 三道关之一)", () => {
|
||
|
|
const dbMsgs = [
|
||
|
|
make("system", "(脏 DB 残留)"),
|
||
|
|
make("user", "Q1"),
|
||
|
|
make("assistant", "A1"),
|
||
|
|
make("system", "(中间也来一个)"),
|
||
|
|
make("user", "Q2")
|
||
|
|
];
|
||
|
|
const r = buildContextWindow(dbMsgs);
|
||
|
|
assert.equal(r.messages.length, 3);
|
||
|
|
assert.deepEqual(
|
||
|
|
r.messages.map((m) => m.role),
|
||
|
|
["user", "assistant", "user"]
|
||
|
|
);
|
||
|
|
// 没有任何 system 漏出
|
||
|
|
assert.equal(r.messages.filter((m) => m.role === "system").length, 0);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("滚动窗口 maxTurns=2 → 只保留最近 4 条", () => {
|
||
|
|
const dbMsgs = [];
|
||
|
|
for (let i = 1; i <= 5; i++) {
|
||
|
|
dbMsgs.push(make("user", "Q" + i));
|
||
|
|
dbMsgs.push(make("assistant", "A" + i));
|
||
|
|
}
|
||
|
|
// 收尾再加一条 user (新提问)
|
||
|
|
dbMsgs.push(make("user", "Q6"));
|
||
|
|
const r = buildContextWindow(dbMsgs, { maxTurns: 2 });
|
||
|
|
// 应保留最后 4 条 (Q5,A5,Q6) 之前的 + Q6 = 滑窗 4 条 = A4,Q5,A5,Q6
|
||
|
|
// 但开头 assistant 会被修剪 → Q5,A5,Q6
|
||
|
|
assert.deepEqual(
|
||
|
|
r.messages.map((m) => m.role),
|
||
|
|
["user", "assistant", "user"]
|
||
|
|
);
|
||
|
|
assert.equal(r.messages[r.messages.length - 1].content, "Q6");
|
||
|
|
});
|
||
|
|
|
||
|
|
test("字数 cap 触发 → 从最老的开始扔", () => {
|
||
|
|
const dbMsgs = [
|
||
|
|
make("user", "x".repeat(5000)),
|
||
|
|
make("assistant", "y".repeat(5000)),
|
||
|
|
make("user", "z".repeat(5000)),
|
||
|
|
make("assistant", "w".repeat(5000)),
|
||
|
|
make("user", "最新提问")
|
||
|
|
];
|
||
|
|
const r = buildContextWindow(dbMsgs, { maxChars: 12000 });
|
||
|
|
// 总字数应 ≤ 12000
|
||
|
|
assert.ok(r.total_chars <= 12000, "实际 " + r.total_chars + " 应 ≤ 12000");
|
||
|
|
// 最后一条必须是最新的 user 提问
|
||
|
|
assert.equal(r.messages[r.messages.length - 1].content, "最新提问");
|
||
|
|
assert.ok(r.dropped > 0, "应该扔了一些");
|
||
|
|
});
|
||
|
|
|
||
|
|
test("单条 user 超 cap → 至少保留这一条 (推理端要东西回复)", () => {
|
||
|
|
const giant = "a".repeat(100000);
|
||
|
|
const r = buildContextWindow([make("user", giant)], { maxChars: 1000 });
|
||
|
|
assert.equal(r.messages.length, 1);
|
||
|
|
assert.equal(r.messages[0].content, giant);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("开头 assistant (历史断头) → 修剪到第一个 user", () => {
|
||
|
|
const dbMsgs = [
|
||
|
|
make("assistant", "A0 (孤儿)"),
|
||
|
|
make("user", "Q1"),
|
||
|
|
make("assistant", "A1"),
|
||
|
|
make("user", "Q2")
|
||
|
|
];
|
||
|
|
const r = buildContextWindow(dbMsgs);
|
||
|
|
assert.equal(r.messages[0].role, "user");
|
||
|
|
assert.equal(r.messages[0].content, "Q1");
|
||
|
|
});
|
||
|
|
|
||
|
|
test("默认值合理 (DEFAULT_TURNS / DEFAULT_CHAR_CAP)", () => {
|
||
|
|
assert.ok(DEFAULT_TURNS >= 5 && DEFAULT_TURNS <= 50, "20 上下都算合理");
|
||
|
|
assert.ok(DEFAULT_CHAR_CAP >= 4000 && DEFAULT_CHAR_CAP <= 64000);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("非法消息 (缺 role/content) 直接过滤", () => {
|
||
|
|
const dbMsgs = [
|
||
|
|
{ role: "user" }, // 缺 content
|
||
|
|
{ content: "x" }, // 缺 role
|
||
|
|
null,
|
||
|
|
make("user", "正常的"),
|
||
|
|
make("assistant", "正常回")
|
||
|
|
];
|
||
|
|
const r = buildContextWindow(dbMsgs);
|
||
|
|
assert.equal(r.messages.length, 2);
|
||
|
|
assert.deepEqual(r.messages.map((m) => m.role), ["user", "assistant"]);
|
||
|
|
});
|