Files
Jyotisha/frontend/tests/consultation-session-history.test.ts
T
jesse-ux 149e1ec4c3
Independent Staging Quality Gate / validate (push) Canceled after 3m8s
Independent Staging Quality Gate / publish (push) Canceled after 0s
fix(consult): drop traces, budget checkpoints, silent summary inherit
BUG-729: dropped history rounds leave an omission marker in the model-visible summary slot.
BUG-730: checkpoint threshold is 0.4 of historyBudgetChars (128k still 16,000).
BUG-731: session_full new chat copies owned context_summary on the server; clients send only continued_from_session_id.
2026-09-16 07:38:36 +08:00

171 lines
7.0 KiB
TypeScript

import assert from "node:assert/strict";
import test from "node:test";
import {
CONSULTATION_HISTORY_CHECKPOINT_BUDGET_RATIO,
CONSULTATION_HISTORY_MESSAGE_CHARS,
clipConsultationHistoryText,
consultationHistoryCheckpointChars,
consultationHistoryFromStoredMessages,
consultationHistoryWindow,
consultationUserTurnContent,
droppedRoundsMarker,
historyBudgetChars,
isContextOverflowError,
omissionMarker,
SESSION_CONTEXT_SUMMARY_HEADING,
} from "../src/lib/consultation-session-history.ts";
function numberedMessages(count: number, text: (index: number) => string) {
return Array.from({ length: count }, (_, index) => ({
role: index % 2 === 0 ? "user" : "assistant",
text: text(index),
requestId: `req-${index}`,
}));
}
test("20 unspecialized messages at 128k drop whole oldest turns to fit the budget", () => {
const body = "x".repeat(3_000);
const history = consultationHistoryWindow(numberedMessages(20, () => body), null, {
contextWindow: 128_000,
});
const budget = historyBudgetChars(128_000);
assert.equal(budget, 40_000);
assert.ok(history.droppedCount > 0);
assert.equal(history.tail.length, 20 - history.droppedCount);
assert.equal(history.tail[0]?.text, body);
assert.ok(history.tail.reduce((sum, message) => sum + message.text.length, 0) <= budget);
assert.equal(history.summaryText, null);
});
test("a summary throughMessageIndex keeps only later turns in the tail", () => {
const messages = numberedMessages(20, (index) => `keep-${index}`);
const history = consultationHistoryWindow(messages, {
version: 1,
text: "先前结论",
throughRequestId: "req-14",
throughMessageIndex: 14,
messageCount: 15,
updatedAt: "2026-09-06T00:00:00.000Z",
}, { contextWindow: 128_000 });
assert.deepEqual(history.tail.map((message) => message.text), [
"keep-15",
"keep-16",
"keep-17",
"keep-18",
"keep-19",
]);
assert.equal(history.summaryText, "先前结论");
assert.equal(history.droppedCount, 0);
});
test("a 20_000 character message is clipped to 12_000 with an omission count", () => {
const text = "y".repeat(20_000);
const clipped = clipConsultationHistoryText(text);
assert.equal(clipped.startsWith("y".repeat(CONSULTATION_HISTORY_MESSAGE_CHARS)), true);
assert.equal(clipped.includes(omissionMarker(8_000)), true);
assert.equal(clipped.length, CONSULTATION_HISTORY_MESSAGE_CHARS + omissionMarker(8_000).length);
const history = consultationHistoryWindow([
{ role: "assistant", text },
], null, { contextWindow: 128_000 });
assert.equal(history.tail[0]?.text, clipped);
});
test("historyBudgetChars uses 64k, 32k, and null-as-128k windows", () => {
assert.equal(historyBudgetChars(64_000), 6_000);
assert.equal(historyBudgetChars(32_000), 4_000);
assert.equal(historyBudgetChars(null), historyBudgetChars(128_000));
assert.equal(historyBudgetChars(undefined), 40_000);
});
test("checkpoint threshold stays below the history budget for legal context windows", () => {
assert.equal(CONSULTATION_HISTORY_CHECKPOINT_BUDGET_RATIO, 0.4);
const windows = [200_000, 128_000, 64_000, 32_000, null] as const;
for (const window of windows) {
const budget = historyBudgetChars(window);
const threshold = consultationHistoryCheckpointChars(window);
assert.ok(threshold < budget, `window=${window}`);
}
// 128k keeps the previous 16,000-character checkpoint.
assert.equal(consultationHistoryCheckpointChars(128_000), 16_000);
assert.equal(consultationHistoryCheckpointChars(null), 16_000);
assert.equal(consultationHistoryCheckpointChars(64_000), 2_400);
assert.equal(consultationHistoryCheckpointChars(32_000), 1_600);
});
test("dropped whole turns leave an omission marker in the user-turn summary slot", () => {
const body = "x".repeat(3_000);
const history = consultationHistoryWindow(numberedMessages(20, () => body), null, {
contextWindow: 128_000,
});
assert.ok(history.droppedCount > 0);
const withoutSummary = consultationUserTurnContent({
currentTime: "当前时间:2026-09-16 12:00(中国)",
instruction: "先加载 Jyotish Skill",
summaryText: history.summaryText,
droppedCount: history.droppedCount,
question: "刚才你说的那个时间",
});
assert.match(
withoutSummary,
new RegExp(`更早的 ${history.droppedCount} 轮问答未能进入本轮上下文,结论尚未并入会话摘要`),
);
assert.equal(withoutSummary.includes(SESSION_CONTEXT_SUMMARY_HEADING), false);
const withSummary = consultationUserTurnContent({
currentTime: "当前时间:2026-09-16 12:00(中国)",
instruction: "先加载 Jyotish Skill",
summaryText: "先前结论",
droppedCount: history.droppedCount,
question: "刚才你说的那个时间",
});
assert.match(withSummary, new RegExp(`更早的 ${history.droppedCount} 轮问答已并入上面的会话摘要`));
assert.ok(withSummary.indexOf(SESSION_CONTEXT_SUMMARY_HEADING) < withSummary.indexOf("刚才你说的那个时间"));
const kept = consultationUserTurnContent({
currentTime: "当前时间:2026-09-16 12:00(中国)",
instruction: "先加载 Jyotish Skill",
summaryText: "先前结论",
droppedCount: 0,
question: "刚才你说的那个时间",
});
assert.equal(kept.includes("更早的"), false);
assert.equal(droppedRoundsMarker(0, true), "");
});
test("stored consultation history can skip the in-flight request id", () => {
const history = consultationHistoryFromStoredMessages([
{ role: "user", text: "old", requestId: "keep" },
{ role: "user", text: "current", requestId: "skip" },
], { excludeRequestId: "skip" });
assert.deepEqual(history, [{ role: "user", text: "old" }]);
});
test("the summary block sits after the time line and before the question", () => {
const content = consultationUserTurnContent({
currentTime: "当前时间:2026-09-06 14:00(中国)",
name: "测",
instruction: "先加载 Jyotish Skill",
summaryText: "上次应期在 2027 年",
question: "你前面说的应期是哪年",
});
const timeAt = content.indexOf("当前时间:");
const summaryAt = content.indexOf(SESSION_CONTEXT_SUMMARY_HEADING);
const questionAt = content.indexOf("你前面说的应期是哪年");
assert.ok(timeAt >= 0 && summaryAt > timeAt && questionAt > summaryAt);
});
test("context overflow detection covers provider codes and related 400s", () => {
assert.equal(isContextOverflowError({ code: "context_length_exceeded" }), true);
assert.equal(isContextOverflowError(new Error("maximum context length exceeded")), true);
assert.equal(isContextOverflowError({ message: "prompt is too long" }), true);
assert.equal(isContextOverflowError({ message: "input is too long" }), true);
assert.equal(isContextOverflowError({ message: "too many tokens in the prompt" }), true);
assert.equal(isContextOverflowError({ status: 400, message: "max_tokens for this context" }), true);
assert.equal(isContextOverflowError({ status: 400, message: "invalid json" }), false);
assert.equal(isContextOverflowError(new Error("rate limit")), false);
});