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

161 lines
6.0 KiB
TypeScript
Raw 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 assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import {
consultationHistoryCheckpointChars,
} from "../src/lib/consultation-session-history.ts";
import {
buildSummaryPrompt,
checkpointSessionContextSummary,
generateSessionContextSummary,
messagesForSummaryInput,
resolveInheritedContextSummary,
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 the derived threshold", () => {
// Former value: hard-coded CONSULTATION_HISTORY_TAIL_MAX_CHARS = 16_000.
// 128k still checkpoints at 16_000 (0.4 × 40_000 budget).
assert.equal(consultationHistoryCheckpointChars(128_000), 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);
assert.equal(shouldCheckpoint([{ role: "user", text: "x".repeat(2_400) }], null, { contextWindow: 64_000 }), false);
assert.equal(shouldCheckpoint([{ role: "user", text: "x".repeat(2_401) }], null, { contextWindow: 64_000 }), 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("inherited context summary copies owned text and skips foreign or empty sources", async () => {
const source = {
version: 1 as const,
text: "已问过的问题\n事业时机",
throughRequestId: "a1",
throughMessageIndex: 3,
messageCount: 4,
updatedAt: "2026-09-15T00:00:00.000Z",
};
const copied = await resolveInheritedContextSummary({
continuedFromSessionId: "11111111-1111-4111-8111-111111111111",
loadOwnedSummary: async () => source,
});
assert.deepEqual(copied, source);
const foreign = await resolveInheritedContextSummary({
continuedFromSessionId: "22222222-2222-4222-8222-222222222222",
loadOwnedSummary: async () => null,
});
assert.equal(foreign, null);
const empty = await resolveInheritedContextSummary({
continuedFromSessionId: "11111111-1111-4111-8111-111111111111",
loadOwnedSummary: async () => ({ version: 1, text: " " }),
});
assert.equal(empty, null);
const skipped = await resolveInheritedContextSummary({
loadOwnedSummary: async () => {
throw new Error("should not load");
},
});
assert.equal(skipped, null);
});
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");
});