// ST-BME: regression tests — reroll should reuse persisted recall record // // Covers: // 1. ensurePersistedRecallRecordForGeneration re-writes when existing record // has same injectionText/nodeIds but empty recallInput // 2. resolveReusablePersistedRecallRecord (inside runRecallController) reuses // a persisted record when recallInput matches the user floor text // 3. End-to-end: regenerate does NOT call retrieve when a valid persisted // record exists import assert from "node:assert/strict"; import { buildPersistedRecallRecord, readPersistedRecallFromUserMessage, writePersistedRecallToUserMessage, BME_RECALL_EXTRA_KEY, } from "../retrieval/recall-persistence.js"; import { runRecallController } from "../retrieval/recall-controller.js"; import { createGenerationRecallHarness } from "./helpers/generation-recall-harness.mjs"; import { normalizeRecallInputText, createRecallRunResult, createRecallInputRecord, isFreshRecallInputRecord, } from "../ui/ui-status.js"; import { defaultSettings } from "../runtime/settings-defaults.js"; // ═══════════════════════════════════════════════════════════════ // 1. ensurePersistedRecallRecordForGeneration: empty recallInput override // ═══════════════════════════════════════════════════════════════ const harness = await createGenerationRecallHarness({ realApplyFinal: true }); // Prime settings Object.assign(harness.settings, { ...defaultSettings, enabled: true, recallEnabled: true, }); // Set up chat: user + assistant harness.chat = [ { is_user: true, mes: "去摩耶山看夜景" }, { is_user: false, mes: "好的,我们出发吧。", is_system: false }, ]; // Pre-write a persisted record with EMPTY recallInput (simulates old bug) const emptyRecallInputRecord = buildPersistedRecallRecord({ injectionText: "注入:去摩耶山看夜景", selectedNodeIds: ["node-test-1"], recallInput: "", recallSource: "chat-tail-user", hookName: "GENERATION_AFTER_COMMANDS", tokenEstimate: 5, manuallyEdited: false, }); writePersistedRecallToUserMessage(harness.chat, 0, emptyRecallInputRecord); // Verify the record is written with empty recallInput const beforeRecord = readPersistedRecallFromUserMessage(harness.chat, 0); assert.ok(beforeRecord, "persisted record should exist before ensure"); assert.equal(beforeRecord.recallInput, "", "recallInput should be empty before fix"); assert.equal( beforeRecord.injectionText, "注入:去摩耶山看夜景", "injectionText should match", ); // Build a mock recall result with the same injectionText const mockRecallResult = { status: "completed", didRecall: true, ok: true, injectionText: "注入:去摩耶山看夜景", selectedNodeIds: ["node-test-1"], source: "chat-last-user", sourceLabel: "历史最后用户楼层", hookName: "GENERATION_AFTER_COMMANDS", authoritativeInputUsed: false, boundUserFloorText: "去摩耶山看夜景", }; // Build frozen recall options with overrideUserMessage const frozenRecallOptions = { generationType: "regenerate", targetUserMessageIndex: 0, overrideUserMessage: "去摩耶山看夜景", overrideSource: "chat-last-user", overrideSourceLabel: "历史最后用户楼层", lockedSource: "chat-last-user", lockedSourceLabel: "历史最后用户楼层", authoritativeInputUsed: false, boundUserFloorText: "去摩耶山看夜景", }; // Call ensurePersistedRecallRecordForGeneration const ensureResult = harness.result.ensurePersistedRecallRecordForGeneration({ generationType: "regenerate", recallResult: mockRecallResult, transaction: { frozenRecallOptions }, recallOptions: frozenRecallOptions, hookName: "GENERATION_AFTER_COMMANDS", }); // After fix: the record should be overwritten because existing recallInput is empty const afterRecord = readPersistedRecallFromUserMessage(harness.chat, 0); assert.ok(afterRecord, "persisted record should still exist after ensure"); assert.equal( afterRecord.recallInput, "去摩耶山看夜景", "recallInput should now be populated after ensure overwrites empty-recallInput record", ); assert.equal( afterRecord.boundUserFloorText, "去摩耶山看夜景", "boundUserFloorText should be populated", ); console.log(" ✓ ensurePersistedRecallRecordForGeneration overwrites record with empty recallInput"); // ═══════════════════════════════════════════════════════════════ // 2. ensurePersistedRecallRecordForGeneration: populated recallInput skip // ═══════════════════════════════════════════════════════════════ // Now the record has proper recallInput — calling ensure again should skip const ensureResult2 = harness.result.ensurePersistedRecallRecordForGeneration({ generationType: "regenerate", recallResult: mockRecallResult, transaction: { frozenRecallOptions }, recallOptions: frozenRecallOptions, hookName: "GENERATION_AFTER_COMMANDS", }); assert.equal( ensureResult2.reason, "already-up-to-date", "should skip when recallInput is already populated", ); console.log(" ✓ ensurePersistedRecallRecordForGeneration skips when recallInput is populated"); // ═══════════════════════════════════════════════════════════════ // 3. runRecallController: regenerate reuses persisted record // ═══════════════════════════════════════════════════════════════ // Set up a fresh chat with a properly persisted recall record const rerollChat = [ { is_user: true, mes: "明日去摩耶山看夜景" }, { is_user: false, mes: "好的,明天约好了。", is_system: false }, ]; const validRecord = buildPersistedRecallRecord({ injectionText: "注入:明日去摩耶山看夜景", selectedNodeIds: ["node-a"], recallInput: "明日去摩耶山看夜景", recallSource: "chat-tail-user", hookName: "GENERATION_AFTER_COMMANDS", tokenEstimate: 5, manuallyEdited: false, boundUserFloorText: "明日去摩耶山看夜景", }); writePersistedRecallToUserMessage(rerollChat, 0, validRecord); let retrieveCalled = false; const rerollRuntime = { getIsRecalling: () => false, getCurrentGraph: () => ({ nodes: [], edges: [] }), getSettings: () => ({ ...defaultSettings, enabled: true, recallEnabled: true, recallLlmContextMessages: 5, }), isGraphReadableForRecall: () => true, isGraphMetadataWriteAllowed: () => true, recoverHistoryIfNeeded: async () => true, getContext: () => ({ chat: rerollChat, chatId: "chat-reroll" }), nextRecallRunSequence: () => 1, beginStageAbortController: () => ({ signal: { aborted: false } }), finishStageAbortController: () => {}, setIsRecalling: () => {}, setActiveRecallPromise: () => {}, getActiveRecallPromise: () => null, setLastRecallStatus: () => {}, clampInt: (v, f, mn, mx) => { const n = Number(v); if (!Number.isFinite(n)) return f; return Math.min(mx, Math.max(mn, Math.trunc(n))); }, normalizeRecallInputText, createRecallInputRecord, createRecallRunResult, isFreshRecallInputRecord, getLatestUserChatMessage: (chat = []) => [...chat].reverse().find((m) => m?.is_user) || null, getLastNonSystemChatMessage: (chat = []) => [...chat].reverse().find((m) => !m?.is_system) || null, getRecallUserMessageSourceLabel: (s) => s, buildRecallRecentMessages: () => [], readPersistedRecallFromUserMessage, bumpPersistedRecallGenerationCount: (chat, idx) => { // no-op in test; just return the record return readPersistedRecallFromUserMessage(chat, idx); }, triggerChatMetadataSave: () => {}, schedulePersistedRecallMessageUiRefresh: () => {}, refreshPanelLiveState: () => {}, ensureVectorReadyIfNeeded: async () => {}, resolveRecallInput: (chat, limit, override) => { // Simulate resolveRecallInputController override path const overrideText = normalizeRecallInputText( override?.overrideUserMessage || override?.userMessage || "", ); return { userMessage: overrideText, generationType: String(override?.generationType || "normal"), targetUserMessageIndex: Number.isFinite(override?.targetUserMessageIndex) ? override.targetUserMessageIndex : null, source: override?.overrideSource || "chat-last-user", sourceLabel: override?.overrideSourceLabel || "历史最后用户楼层", reason: "override-bound", authoritativeInputUsed: Boolean(override?.authoritativeInputUsed), boundUserFloorText: normalizeRecallInputText( override?.boundUserFloorText || "", ), recentMessages: [], hookName: override?.hookName || "", deliveryMode: "immediate", }; }, applyRecallInjection: (_settings, _input, _recent, result) => ({ injectionText: result?.injectionText || "", applied: true, source: "persisted-reuse", mode: "module-injection", }), retrieve: async () => { retrieveCalled = true; return { injectionText: "should-not-appear", selectedNodeIds: ["node-b"], }; }, buildRecallRetrieveOptions: () => ({}), getEmbeddingConfig: () => ({}), getSchema: () => ({}), console, isAbortError: () => false, toastr: { error: () => {} }, getRecallHookLabel: () => "", setPendingRecallSendIntent: () => {}, }; // Simulate regenerate: override with the user floor text and generationType regenerate const rerollResult = await runRecallController(rerollRuntime, { overrideUserMessage: "明日去摩耶山看夜景", generationType: "regenerate", targetUserMessageIndex: 0, overrideSource: "chat-last-user", overrideSourceLabel: "历史最后用户楼层", hookName: "GENERATION_AFTER_COMMANDS", deliveryMode: "immediate", }); assert.equal(rerollResult.status, "completed", "reroll should complete"); assert.equal( rerollResult.reason, "persisted-user-floor-reused", "reroll should reuse persisted record, not run fresh recall", ); assert.equal( retrieveCalled, false, "retrieve() should NOT be called when persisted record is reused", ); assert.equal( rerollResult.injectionText, "注入:明日去摩耶山看夜景", "injection text should come from persisted record", ); console.log(" ✓ runRecallController reuses persisted record on regenerate"); // ═══════════════════════════════════════════════════════════════ // 4. runRecallController: regenerate with empty recallInput does NOT reuse // ═══════════════════════════════════════════════════════════════ const noReuseChat = [ { is_user: true, mes: "去看星星" }, { is_user: false, mes: "好的。", is_system: false }, ]; const emptyInputRecord = buildPersistedRecallRecord({ injectionText: "注入:去看星星", selectedNodeIds: ["node-c"], recallInput: "", recallSource: "chat-tail-user", hookName: "GENERATION_AFTER_COMMANDS", tokenEstimate: 3, manuallyEdited: false, }); writePersistedRecallToUserMessage(noReuseChat, 0, emptyInputRecord); let noReuseRetrieveCalled = false; const noReuseRuntime = { ...rerollRuntime, getContext: () => ({ chat: noReuseChat, chatId: "chat-no-reuse" }), readPersistedRecallFromUserMessage, retrieve: async () => { noReuseRetrieveCalled = true; return { injectionText: "新召回结果", selectedNodeIds: ["node-d"], }; }, resolveRecallInput: (chat, limit, override) => ({ userMessage: normalizeRecallInputText( override?.overrideUserMessage || "", ), generationType: String(override?.generationType || "normal"), targetUserMessageIndex: Number.isFinite(override?.targetUserMessageIndex) ? override.targetUserMessageIndex : null, source: override?.overrideSource || "chat-last-user", sourceLabel: override?.overrideSourceLabel || "", reason: "override-bound", authoritativeInputUsed: false, boundUserFloorText: "", recentMessages: [], hookName: override?.hookName || "", deliveryMode: "immediate", }), }; const noReuseResult = await runRecallController(noReuseRuntime, { overrideUserMessage: "去看星星", generationType: "regenerate", targetUserMessageIndex: 0, overrideSource: "chat-last-user", hookName: "GENERATION_AFTER_COMMANDS", deliveryMode: "immediate", }); assert.equal(noReuseResult.status, "completed", "no-reuse should complete"); assert.equal( noReuseRetrieveCalled, true, "retrieve() SHOULD be called when persisted record has empty recallInput", ); console.log(" ✓ runRecallController does NOT reuse record with empty recallInput"); // ═══════════════════════════════════════════════════════════════ console.log("recall-reroll-reuse tests passed");