mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
505 lines
18 KiB
JavaScript
505 lines
18 KiB
JavaScript
// 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,
|
|
});
|
|
|
|
harness.chat = [
|
|
{ is_user: true, mes: "楼层里的稳定用户输入" },
|
|
{ is_user: false, mes: "好的。", is_system: false },
|
|
];
|
|
|
|
const persistedWriteResult = harness.result.persistRecallInjectionRecord({
|
|
recallInput: {
|
|
userMessage: "发送前捕获的权威输入",
|
|
source: "send-intent",
|
|
hookName: "GENERATION_AFTER_COMMANDS",
|
|
targetUserMessageIndex: 0,
|
|
authoritativeInputUsed: true,
|
|
boundUserFloorText: "楼层里的稳定用户输入",
|
|
},
|
|
result: {
|
|
selectedNodeIds: ["node-write-1"],
|
|
},
|
|
injectionText: "注入:楼层里的稳定用户输入",
|
|
tokenEstimate: 8,
|
|
});
|
|
assert.ok(persistedWriteResult?.record, "persistRecallInjectionRecord should write a record");
|
|
assert.equal(
|
|
persistedWriteResult.record.authoritativeInputUsed,
|
|
true,
|
|
"initial persisted record should keep authoritativeInputUsed",
|
|
);
|
|
assert.equal(
|
|
persistedWriteResult.record.boundUserFloorText,
|
|
"楼层里的稳定用户输入",
|
|
"initial persisted record should keep boundUserFloorText",
|
|
);
|
|
|
|
console.log(" ✓ persistRecallInjectionRecord stores authoritative input metadata");
|
|
|
|
// 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");
|
|
|
|
harness.chat = [
|
|
{ is_user: true, mes: "稳定楼层文本" },
|
|
{ is_user: false, mes: "好的。", is_system: false },
|
|
];
|
|
const staleMetadataRecord = buildPersistedRecallRecord({
|
|
injectionText: "注入:稳定楼层文本",
|
|
selectedNodeIds: ["node-stale-meta"],
|
|
recallInput: "发送前捕获文本",
|
|
recallSource: "send-intent",
|
|
hookName: "GENERATION_AFTER_COMMANDS",
|
|
tokenEstimate: 4,
|
|
manuallyEdited: false,
|
|
});
|
|
writePersistedRecallToUserMessage(harness.chat, 0, staleMetadataRecord);
|
|
const staleMetadataEnsureResult = harness.result.ensurePersistedRecallRecordForGeneration({
|
|
generationType: "regenerate",
|
|
recallResult: {
|
|
status: "completed",
|
|
didRecall: true,
|
|
ok: true,
|
|
injectionText: "注入:稳定楼层文本",
|
|
selectedNodeIds: ["node-stale-meta"],
|
|
recallInput: "发送前捕获文本",
|
|
source: "send-intent",
|
|
hookName: "GENERATION_AFTER_COMMANDS",
|
|
authoritativeInputUsed: false,
|
|
boundUserFloorText: "稳定楼层文本",
|
|
},
|
|
transaction: {
|
|
frozenRecallOptions: {
|
|
generationType: "regenerate",
|
|
targetUserMessageIndex: 0,
|
|
overrideUserMessage: "稳定楼层文本",
|
|
overrideSource: "chat-last-user",
|
|
authoritativeInputUsed: false,
|
|
boundUserFloorText: "稳定楼层文本",
|
|
},
|
|
},
|
|
recallOptions: {
|
|
generationType: "regenerate",
|
|
targetUserMessageIndex: 0,
|
|
overrideUserMessage: "稳定楼层文本",
|
|
authoritativeInputUsed: false,
|
|
boundUserFloorText: "稳定楼层文本",
|
|
},
|
|
hookName: "GENERATION_AFTER_COMMANDS",
|
|
});
|
|
assert.equal(
|
|
staleMetadataEnsureResult.persisted,
|
|
true,
|
|
"ensure should rewrite records whose metadata is stale even when text/nodeIds match",
|
|
);
|
|
const repairedMetadataRecord = readPersistedRecallFromUserMessage(harness.chat, 0);
|
|
assert.equal(
|
|
repairedMetadataRecord.boundUserFloorText,
|
|
"稳定楼层文本",
|
|
"ensure should repair missing boundUserFloorText",
|
|
);
|
|
|
|
console.log(" ✓ ensurePersistedRecallRecordForGeneration repairs stale metadata");
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// 2. ensurePersistedRecallRecordForGeneration: populated recallInput skip
|
|
// ═══════════════════════════════════════════════════════════════
|
|
|
|
harness.chat = [
|
|
{ is_user: true, mes: "去摩耶山看夜景" },
|
|
{ is_user: false, mes: "好的,我们出发吧。", is_system: false },
|
|
];
|
|
writePersistedRecallToUserMessage(harness.chat, 0, afterRecord);
|
|
|
|
// 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");
|
|
|
|
harness.chat = [
|
|
{ is_user: true, mes: "继续写摩耶山夜景" },
|
|
{ is_user: false, mes: "前一次回复。", is_system: false },
|
|
];
|
|
harness.result.cleanupGenerationRecallTransactions(Date.now() + 60000);
|
|
const afterCommandsRecallOptions = harness.result.buildGenerationAfterCommandsRecallInput(
|
|
"regenerate",
|
|
{},
|
|
harness.chat,
|
|
);
|
|
const afterCommandsContext = harness.result.createGenerationRecallContext({
|
|
hookName: "GENERATION_AFTER_COMMANDS",
|
|
generationType: "regenerate",
|
|
recallOptions: afterCommandsRecallOptions,
|
|
});
|
|
assert.ok(
|
|
afterCommandsContext.transaction,
|
|
"after-commands should create a history transaction",
|
|
);
|
|
harness.result.markGenerationRecallTransactionHookState(
|
|
afterCommandsContext.transaction,
|
|
"GENERATION_AFTER_COMMANDS",
|
|
"completed",
|
|
);
|
|
const beforeCombineNormalFallback = harness.result.buildNormalGenerationRecallInput(
|
|
harness.chat,
|
|
);
|
|
const beforeCombineContext = harness.result.createGenerationRecallContext({
|
|
hookName: "GENERATE_BEFORE_COMBINE_PROMPTS",
|
|
generationType: "normal",
|
|
recallOptions: beforeCombineNormalFallback,
|
|
});
|
|
assert.equal(
|
|
beforeCombineContext.transaction,
|
|
afterCommandsContext.transaction,
|
|
"before-combine should reuse the existing history transaction despite normal fallback input",
|
|
);
|
|
assert.equal(
|
|
beforeCombineContext.generationType,
|
|
"regenerate",
|
|
"before-combine should keep the transaction's history generation type",
|
|
);
|
|
assert.equal(
|
|
beforeCombineContext.shouldRun,
|
|
false,
|
|
"before-combine should not run recall again after after-commands completed",
|
|
);
|
|
|
|
console.log(" ✓ before-combine reuses existing history transaction");
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// 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: "send-intent",
|
|
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");
|