Files
Jyotisha/frontend/tests/consultation-session-history.test.ts
T
Jesse_ChenandCursor bf8ad0d1ff fix(web): keep consultation conclusions across turns and surface cache hits (BUG-555, BUG-556)
Session history was silently clipped to the first 4000 characters of the last 12 messages, so follow-ups could not see timing or audit tables. Keep an append-only tail plus a checkpoint summary, retry overflow in the same request, and expose cache hit rate in admin usage.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-06 15:16:18 +08:00

113 lines
4.5 KiB
TypeScript

import assert from "node:assert/strict";
import test from "node:test";
import {
CONSULTATION_HISTORY_MESSAGE_CHARS,
clipConsultationHistoryText,
consultationHistoryFromStoredMessages,
consultationHistoryWindow,
consultationUserTurnContent,
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("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);
});