Files
Jyotisha/frontend/tests/session-context-summary.test.ts
T
Jesse_Chen 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

121 lines
4.5 KiB
TypeScript

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import {
CONSULTATION_HISTORY_TAIL_MAX_CHARS,
} from "../src/lib/consultation-session-history.ts";
import {
buildSummaryPrompt,
checkpointSessionContextSummary,
generateSessionContextSummary,
messagesForSummaryInput,
sanitizeSessionContextSummary,
shouldCheckpoint,
writeSessionContextSummary,
} from "../src/lib/session-context-summary.ts";
function overBudgetConversation() {
return [
{ role: "user" as const, text: `${"x".repeat(8_000)}`, requestId: "u1" },
{ role: "assistant" as const, text: `${"y".repeat(8_000)}`, requestId: "a1" },
{ role: "user" as const, text: "追问应期", requestId: "u2" },
{ role: "assistant" as const, text: "2027 年", requestId: "a2" },
];
}
test("checkpoint triggers only when the tail exceeds 16_000 characters", () => {
assert.equal(CONSULTATION_HISTORY_TAIL_MAX_CHARS, 16_000);
assert.equal(shouldCheckpoint([{ role: "user", text: "x".repeat(15_999) }], null), false);
assert.equal(shouldCheckpoint([{ role: "user", text: "x".repeat(16_001) }], null), true);
});
test("checkpoint prompt omits the last question-answer pair", () => {
const messages = overBudgetConversation();
const input = messagesForSummaryInput(messages, null);
assert.equal(input.length, 2);
assert.equal(input[0]?.requestId, "u1");
assert.equal(input[1]?.requestId, "a1");
assert.equal(input.some((turn) => turn.text === "追问应期"), false);
assert.equal(input.some((turn) => turn.text === "2027 年"), false);
const prompt = buildSummaryPrompt(null, messages);
assert.equal(prompt.includes("追问应期"), false);
assert.equal(prompt.includes("2027 年"), false);
assert.equal(prompt.includes("上一份摘要:无"), true);
});
test("summary sanitizer strips ISO dates, clock times, and emails", () => {
const cleaned = sanitizeSessionContextSummary(
"已问过的问题\n事业时机,邮箱 user@example.com,日期 1990-01-01 08:30\n已给出的结论\nblocked 项无",
);
assert.ok(cleaned);
assert.equal(cleaned.includes("1990-01-01"), false);
assert.equal(cleaned.includes("08:30"), false);
assert.equal(cleaned.includes("user@example.com"), false);
assert.equal(cleaned.includes("事业时机"), true);
});
test("generateSessionContextSummary times out to null and keeps the timer ref'd", async () => {
const source = readFileSync(new URL("../src/lib/session-context-summary.ts", import.meta.url), "utf8");
assert.doesNotMatch(source, /AbortSignal\.timeout\(/);
assert.doesNotMatch(source, /\.unref\(/);
assert.match(source, /clearTimeout/);
const pending = new Set<unknown>();
let created = 0;
const realSetTimeout = globalThis.setTimeout;
const realClearTimeout = globalThis.clearTimeout;
globalThis.setTimeout = ((handler: TimerHandler, delay?: number, ...args: unknown[]) => {
created += 1;
const id = realSetTimeout(handler, delay, ...args);
pending.add(id);
return id;
}) as typeof setTimeout;
globalThis.clearTimeout = ((id?: ReturnType<typeof setTimeout>) => {
pending.delete(id);
realClearTimeout(id);
}) as typeof clearTimeout;
try {
const text = await generateSessionContextSummary({
previous: null,
messages: overBudgetConversation(),
timeoutMs: 20,
generateText: () => new Promise(() => {}),
});
assert.equal(text, null);
assert.ok(created >= 1);
assert.equal(pending.size, 0);
} finally {
globalThis.setTimeout = realSetTimeout;
globalThis.clearTimeout = realClearTimeout;
}
});
test("writeSessionContextSummary abandons when updatedAt does not match", async () => {
const result = await writeSessionContextSummary({
seenUpdatedAt: "2026-09-06T00:00:00.000Z",
summary: {
version: 1,
text: "新摘要",
throughRequestId: "a1",
throughMessageIndex: 1,
messageCount: 2,
updatedAt: "2026-09-06T01:00:00.000Z",
},
update: async () => false,
});
assert.equal(result, "abandoned");
});
test("checkpoint writes a new summary when the tail is over budget", async () => {
const result = await checkpointSessionContextSummary({
messages: overBudgetConversation(),
summary: null,
generateText: async () => "已问过的问题\n事业\n已给出的结论\n应期 blocked\n用户补充的事实\n无\n未决与待追问\n继续",
update: async () => true,
now: () => new Date("2026-09-06T02:00:00.000Z"),
});
assert.equal(result, "written");
});