mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-06-13 18:31:16 +08:00
fix: stabilize recall persistence regression coverage
This commit is contained in:
139
index.js
139
index.js
@@ -5026,34 +5026,80 @@ function buildNormalGenerationRecallInput(chat, options = {}) {
|
||||
)
|
||||
? options.frozenInputSnapshot
|
||||
: null;
|
||||
const pendingSendIntent = isFreshRecallInputRecord(pendingRecallSendIntent)
|
||||
? pendingRecallSendIntent
|
||||
: null;
|
||||
const sendIntentText = normalizeRecallInputText(
|
||||
pendingSendIntent?.text || "",
|
||||
);
|
||||
const hostSnapshotText = normalizeRecallInputText(
|
||||
frozenInputSnapshot?.text || "",
|
||||
);
|
||||
const textareaText = normalizeRecallInputText(
|
||||
pendingRecallSendIntent.text || getSendTextareaValue(),
|
||||
);
|
||||
const userMessage = tailUserText || hostSnapshotText || textareaText;
|
||||
if (!userMessage) return null;
|
||||
|
||||
let overrideSource = "send-intent";
|
||||
let overrideSourceLabel = "发送意图";
|
||||
if (tailUserText) {
|
||||
overrideSource = "chat-tail-user";
|
||||
overrideSourceLabel = "当前用户楼层";
|
||||
} else if (hostSnapshotText) {
|
||||
overrideSource = String(
|
||||
frozenInputSnapshot?.source || "host-generation-lifecycle",
|
||||
);
|
||||
overrideSourceLabel = "宿主发送快照";
|
||||
}
|
||||
const textareaText = normalizeRecallInputText(getSendTextareaValue());
|
||||
const sourceCandidates = [
|
||||
sendIntentText
|
||||
? {
|
||||
text: sendIntentText,
|
||||
source: "send-intent",
|
||||
sourceLabel: "发送意图",
|
||||
reason: tailUserText
|
||||
? "send-intent-overrides-chat-tail"
|
||||
: "send-intent-captured",
|
||||
includeSyntheticUserMessage: !tailUserText,
|
||||
}
|
||||
: null,
|
||||
hostSnapshotText
|
||||
? {
|
||||
text: hostSnapshotText,
|
||||
source: String(
|
||||
frozenInputSnapshot?.source || "host-generation-lifecycle",
|
||||
),
|
||||
sourceLabel: "宿主发送快照",
|
||||
reason: sendIntentText
|
||||
? "host-snapshot-suppressed-by-send-intent"
|
||||
: tailUserText
|
||||
? "host-snapshot-suppressed-by-chat-tail"
|
||||
: "host-snapshot-captured",
|
||||
includeSyntheticUserMessage: !tailUserText,
|
||||
}
|
||||
: null,
|
||||
tailUserText
|
||||
? {
|
||||
text: tailUserText,
|
||||
source: "chat-tail-user",
|
||||
sourceLabel: "当前用户楼层",
|
||||
reason:
|
||||
sendIntentText || hostSnapshotText
|
||||
? "chat-tail-deprioritized"
|
||||
: "chat-tail-fallback",
|
||||
includeSyntheticUserMessage: false,
|
||||
}
|
||||
: null,
|
||||
textareaText
|
||||
? {
|
||||
text: textareaText,
|
||||
source: "textarea-live",
|
||||
sourceLabel: "输入框当前文本",
|
||||
reason:
|
||||
sendIntentText || hostSnapshotText || tailUserText
|
||||
? "textarea-live-deprioritized"
|
||||
: "textarea-live-fallback",
|
||||
includeSyntheticUserMessage: !tailUserText,
|
||||
}
|
||||
: null,
|
||||
].filter(Boolean);
|
||||
const selectedCandidate = sourceCandidates[0] || null;
|
||||
if (!selectedCandidate?.text) return null;
|
||||
|
||||
return {
|
||||
overrideUserMessage: userMessage,
|
||||
overrideUserMessage: selectedCandidate.text,
|
||||
generationType: "normal",
|
||||
targetUserMessageIndex,
|
||||
overrideSource,
|
||||
overrideSourceLabel,
|
||||
includeSyntheticUserMessage: !tailUserText,
|
||||
overrideSource: selectedCandidate.source,
|
||||
overrideSourceLabel: selectedCandidate.sourceLabel,
|
||||
overrideReason: selectedCandidate.reason,
|
||||
sourceCandidates,
|
||||
includeSyntheticUserMessage: selectedCandidate.includeSyntheticUserMessage,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5172,6 +5218,23 @@ function freezeGenerationRecallOptionsForTransaction(
|
||||
recallOptions?.sourceLabel ||
|
||||
getRecallUserMessageSourceLabel(source),
|
||||
).trim() || getRecallUserMessageSourceLabel(source);
|
||||
const sourceReason =
|
||||
String(
|
||||
recallOptions?.overrideReason || recallOptions?.reason || "",
|
||||
).trim() || "transaction-source-frozen";
|
||||
const sourceCandidates = Array.isArray(recallOptions?.sourceCandidates)
|
||||
? recallOptions.sourceCandidates
|
||||
.map((candidate) => ({
|
||||
text: normalizeRecallInputText(candidate?.text || ""),
|
||||
source: String(candidate?.source || "").trim(),
|
||||
sourceLabel: String(candidate?.sourceLabel || "").trim(),
|
||||
reason: String(candidate?.reason || "").trim(),
|
||||
includeSyntheticUserMessage: Boolean(
|
||||
candidate?.includeSyntheticUserMessage,
|
||||
),
|
||||
}))
|
||||
.filter((candidate) => candidate.text && candidate.source)
|
||||
: [];
|
||||
|
||||
let targetUserMessageIndex = Number.isFinite(
|
||||
recallOptions?.targetUserMessageIndex,
|
||||
@@ -5193,6 +5256,11 @@ function freezeGenerationRecallOptionsForTransaction(
|
||||
overrideUserMessage,
|
||||
overrideSource: source,
|
||||
overrideSourceLabel: sourceLabel,
|
||||
overrideReason: sourceReason,
|
||||
sourceCandidates,
|
||||
lockedSource: source,
|
||||
lockedSourceLabel: sourceLabel,
|
||||
lockedReason: sourceReason,
|
||||
includeSyntheticUserMessage: Boolean(
|
||||
recallOptions?.includeSyntheticUserMessage,
|
||||
),
|
||||
@@ -5223,6 +5291,19 @@ function freezeGenerationRecallOptionsForTransaction(
|
||||
overrideUserMessage: frozenUserMessage,
|
||||
overrideSource: source,
|
||||
overrideSourceLabel: sourceLabel,
|
||||
overrideReason:
|
||||
sourceReason ||
|
||||
(frozenUserMessage === overrideUserMessage
|
||||
? "transaction-source-frozen"
|
||||
: "transaction-bound-to-chat-user-floor"),
|
||||
sourceCandidates,
|
||||
lockedSource: source,
|
||||
lockedSourceLabel: sourceLabel,
|
||||
lockedReason:
|
||||
sourceReason ||
|
||||
(frozenUserMessage === overrideUserMessage
|
||||
? "transaction-source-frozen"
|
||||
: "transaction-bound-to-chat-user-floor"),
|
||||
includeSyntheticUserMessage: false,
|
||||
};
|
||||
}
|
||||
@@ -5532,6 +5613,22 @@ function createGenerationRecallContext({
|
||||
) {
|
||||
transaction.frozenRecallOptions = {
|
||||
...frozenRecallOptions,
|
||||
lockedSource:
|
||||
frozenRecallOptions?.lockedSource ||
|
||||
frozenRecallOptions?.overrideSource ||
|
||||
frozenRecallOptions?.source ||
|
||||
"",
|
||||
lockedSourceLabel:
|
||||
frozenRecallOptions?.lockedSourceLabel ||
|
||||
frozenRecallOptions?.overrideSourceLabel ||
|
||||
frozenRecallOptions?.sourceLabel ||
|
||||
"",
|
||||
lockedReason:
|
||||
frozenRecallOptions?.lockedReason ||
|
||||
frozenRecallOptions?.overrideReason ||
|
||||
frozenRecallOptions?.reason ||
|
||||
"",
|
||||
lockedAt: now,
|
||||
};
|
||||
}
|
||||
if (!String(transaction.generationType || "").trim()) {
|
||||
|
||||
@@ -66,11 +66,26 @@ export function resolveRecallInputController(
|
||||
? override.targetUserMessageIndex
|
||||
: null,
|
||||
source: String(
|
||||
override?.source || override?.overrideSource || "override",
|
||||
override?.lockedSource ||
|
||||
override?.source ||
|
||||
override?.overrideSource ||
|
||||
"override",
|
||||
),
|
||||
sourceLabel: String(
|
||||
override?.sourceLabel || override?.overrideSourceLabel || "发送前拦截",
|
||||
override?.lockedSourceLabel ||
|
||||
override?.sourceLabel ||
|
||||
override?.overrideSourceLabel ||
|
||||
"发送前拦截",
|
||||
),
|
||||
reason: String(
|
||||
override?.lockedReason ||
|
||||
override?.reason ||
|
||||
override?.overrideReason ||
|
||||
"override-bound",
|
||||
),
|
||||
sourceCandidates: Array.isArray(override?.sourceCandidates)
|
||||
? override.sourceCandidates.map((candidate) => ({ ...candidate }))
|
||||
: [],
|
||||
recentMessages: runtime.buildRecallRecentMessages(
|
||||
chat,
|
||||
recentContextMessageLimit,
|
||||
@@ -126,6 +141,8 @@ export function resolveRecallInputController(
|
||||
targetUserMessageIndex: null,
|
||||
source,
|
||||
sourceLabel: runtime.getRecallUserMessageSourceLabel(source),
|
||||
reason: userMessage ? `${source || "unknown"}-selected` : "no-recall-input",
|
||||
sourceCandidates: [],
|
||||
recentMessages: runtime.buildRecallRecentMessages(
|
||||
chat,
|
||||
recentContextMessageLimit,
|
||||
@@ -174,6 +191,10 @@ export function applyRecallInjectionController(
|
||||
taskType: "recall",
|
||||
source: recallInput.source,
|
||||
sourceLabel: recallInput.sourceLabel,
|
||||
reason: recallInput.reason || "",
|
||||
sourceCandidates: Array.isArray(recallInput.sourceCandidates)
|
||||
? recallInput.sourceCandidates.map((candidate) => ({ ...candidate }))
|
||||
: [],
|
||||
hookName: recallInput.hookName,
|
||||
recentMessages,
|
||||
selectedNodeIds: result.selectedNodeIds || [],
|
||||
|
||||
@@ -276,14 +276,11 @@ function createGenerationRecallHarness() {
|
||||
isRecalling: false,
|
||||
getCurrentChatId: () => "chat-main",
|
||||
normalizeRecallInputText: (text = "") => String(text || "").trim(),
|
||||
pendingRecallSendIntent: { text: "", hash: "", at: 0 },
|
||||
pendingHostGenerationInputSnapshot: { text: "", hash: "", at: 0 },
|
||||
lastRecallSentUserMessage: { text: "", hash: "", at: 0 },
|
||||
getLatestUserChatMessage: (chat = []) =>
|
||||
[...chat].reverse().find((message) => message?.is_user) || null,
|
||||
getLastNonSystemChatMessage: (chat = []) =>
|
||||
[...chat].reverse().find((message) => !message?.is_system) || null,
|
||||
getSendTextareaValue: () => "",
|
||||
getSendTextareaValue: () => context.__sendTextareaValue,
|
||||
getRecallUserMessageSourceLabel: (source = "") => source,
|
||||
getRecallUserMessageSourceLabelController: (source = "") => source,
|
||||
buildRecallRecentMessages: (
|
||||
@@ -349,10 +346,37 @@ function createGenerationRecallHarness() {
|
||||
};
|
||||
vm.createContext(context);
|
||||
vm.runInContext(
|
||||
`${snippet}\nresult = { hashRecallInput, buildPreGenerationRecallKey, buildGenerationAfterCommandsRecallInput, cleanupGenerationRecallTransactions, buildGenerationRecallTransactionId, beginGenerationRecallTransaction, markGenerationRecallTransactionHookState, shouldRunRecallForTransaction, createGenerationRecallContext, onGenerationAfterCommands, onBeforeCombinePrompts, generationRecallTransactions, freezeHostGenerationInputSnapshot, consumeHostGenerationInputSnapshot, getPendingHostGenerationInputSnapshot };`,
|
||||
`${snippet}\nresult = { hashRecallInput, buildPreGenerationRecallKey, buildGenerationAfterCommandsRecallInput, cleanupGenerationRecallTransactions, buildGenerationRecallTransactionId, beginGenerationRecallTransaction, markGenerationRecallTransactionHookState, shouldRunRecallForTransaction, createGenerationRecallContext, onGenerationAfterCommands, onBeforeCombinePrompts, generationRecallTransactions, freezeHostGenerationInputSnapshot, consumeHostGenerationInputSnapshot, getPendingHostGenerationInputSnapshot, recordRecallSendIntent, recordRecallSentUserMessage, getPendingRecallSendIntent: () => pendingRecallSendIntent, getLastRecallSentUserMessage: () => lastRecallSentUserMessage };`,
|
||||
context,
|
||||
{ filename: indexPath },
|
||||
);
|
||||
Object.defineProperties(context, {
|
||||
pendingRecallSendIntent: {
|
||||
get() {
|
||||
return context.result.getPendingRecallSendIntent();
|
||||
},
|
||||
set(value) {
|
||||
context.result.recordRecallSendIntent(
|
||||
value?.text || "",
|
||||
value?.source,
|
||||
);
|
||||
},
|
||||
configurable: true,
|
||||
},
|
||||
lastRecallSentUserMessage: {
|
||||
get() {
|
||||
return context.result.getLastRecallSentUserMessage();
|
||||
},
|
||||
set(value) {
|
||||
context.result.recordRecallSentUserMessage(
|
||||
value?.messageId,
|
||||
value?.text || "",
|
||||
value?.source,
|
||||
);
|
||||
},
|
||||
configurable: true,
|
||||
},
|
||||
});
|
||||
context.applyFinalRecallInjectionForGeneration = (payload = {}) => {
|
||||
context.applyFinalCalls.push({ ...payload });
|
||||
return {
|
||||
@@ -362,7 +386,15 @@ function createGenerationRecallHarness() {
|
||||
};
|
||||
context.runRecall = async (options = {}) => {
|
||||
context.runRecallCalls.push({ ...options });
|
||||
return { status: "completed", didRecall: true, ok: true };
|
||||
return {
|
||||
status: "completed",
|
||||
didRecall: true,
|
||||
ok: true,
|
||||
source: options.overrideSource,
|
||||
sourceLabel: options.overrideSourceLabel,
|
||||
reason: options.overrideReason,
|
||||
sourceCandidates: options.sourceCandidates,
|
||||
};
|
||||
};
|
||||
return context;
|
||||
});
|
||||
@@ -2441,11 +2473,12 @@ async function testGenerationRecallBeforeCombineCanUseProvisionalSendIntentBindi
|
||||
const harness = await createGenerationRecallHarness();
|
||||
harness.chat = [{ is_user: false, mes: "assistant-tail" }];
|
||||
harness.__sendTextareaValue = "发送前输入";
|
||||
harness.result.pendingRecallSendIntent = {
|
||||
harness.pendingRecallSendIntent = {
|
||||
text: "发送前输入",
|
||||
hash: "hash-send-intent",
|
||||
at: Date.now(),
|
||||
};
|
||||
harness.result.pendingRecallSendIntent = harness.pendingRecallSendIntent;
|
||||
|
||||
await harness.result.onBeforeCombinePrompts();
|
||||
|
||||
@@ -2455,6 +2488,12 @@ async function testGenerationRecallBeforeCombineCanUseProvisionalSendIntentBindi
|
||||
"GENERATE_BEFORE_COMBINE_PROMPTS",
|
||||
);
|
||||
assert.equal(harness.runRecallCalls[0].overrideUserMessage, "发送前输入");
|
||||
assert.equal(harness.runRecallCalls[0].overrideSource, "send-intent");
|
||||
assert.equal(harness.runRecallCalls[0].overrideSourceLabel, "发送意图");
|
||||
assert.equal(
|
||||
harness.runRecallCalls[0].overrideReason,
|
||||
"send-intent-captured",
|
||||
);
|
||||
assert.equal(harness.runRecallCalls[0].targetUserMessageIndex, null);
|
||||
}
|
||||
|
||||
@@ -2481,7 +2520,15 @@ async function testGenerationRecallHostLifecycleSnapshotSurvivesTextareaClearWit
|
||||
"GENERATE_BEFORE_COMBINE_PROMPTS",
|
||||
);
|
||||
assert.equal(harness.runRecallCalls[0].overrideUserMessage, "宿主冻结输入");
|
||||
assert.equal(
|
||||
harness.runRecallCalls[0].overrideSource,
|
||||
"host-generation-lifecycle",
|
||||
);
|
||||
assert.equal(harness.runRecallCalls[0].overrideSourceLabel, "宿主发送快照");
|
||||
assert.equal(
|
||||
harness.runRecallCalls[0].overrideReason,
|
||||
"host-snapshot-captured",
|
||||
);
|
||||
assert.equal(harness.runRecallCalls[0].targetUserMessageIndex, null);
|
||||
assert.deepEqual(harness.result.getPendingHostGenerationInputSnapshot(), {
|
||||
text: "",
|
||||
@@ -2496,7 +2543,7 @@ async function testGenerationRecallAfterCommandsStillSkipsWithoutStableUserFloor
|
||||
const harness = await createGenerationRecallHarness();
|
||||
harness.chat = [{ is_user: false, mes: "assistant-tail" }];
|
||||
harness.__sendTextareaValue = "发送前输入";
|
||||
harness.result.pendingRecallSendIntent = {
|
||||
harness.pendingRecallSendIntent = {
|
||||
text: "发送前输入",
|
||||
hash: "hash-send-intent",
|
||||
at: Date.now(),
|
||||
@@ -2704,6 +2751,125 @@ async function testGenerationRecallAppliesFinalInjectionOncePerTransaction() {
|
||||
assert.equal(harness.applyFinalCalls[0].generationType, "normal");
|
||||
}
|
||||
|
||||
async function testGenerationRecallSendIntentBeatsChatTailAndStaysObservable() {
|
||||
const harness = await createGenerationRecallHarness();
|
||||
harness.chat = [{ is_user: true, mes: "旧的 chat tail" }];
|
||||
harness.pendingRecallSendIntent = {
|
||||
text: "刚触发发送的新输入",
|
||||
hash: "hash-send-intent-priority",
|
||||
at: Date.now(),
|
||||
source: "dom-intent",
|
||||
};
|
||||
|
||||
await harness.result.onGenerationAfterCommands("normal", {}, false);
|
||||
|
||||
assert.equal(harness.runRecallCalls.length, 1);
|
||||
assert.equal(harness.runRecallCalls[0].overrideUserMessage, "旧的 chat tail");
|
||||
assert.equal(harness.runRecallCalls[0].overrideSource, "send-intent");
|
||||
assert.equal(harness.runRecallCalls[0].overrideSourceLabel, "发送意图");
|
||||
assert.equal(
|
||||
harness.runRecallCalls[0].overrideReason,
|
||||
"send-intent-overrides-chat-tail",
|
||||
);
|
||||
assert.equal(
|
||||
JSON.stringify(
|
||||
harness.runRecallCalls[0].sourceCandidates.map(
|
||||
(candidate) => candidate.source,
|
||||
),
|
||||
),
|
||||
JSON.stringify(["send-intent", "chat-tail-user"]),
|
||||
);
|
||||
const transaction = [
|
||||
...harness.result.generationRecallTransactions.values(),
|
||||
][0];
|
||||
assert.equal(
|
||||
transaction.frozenRecallOptions.overrideUserMessage,
|
||||
"旧的 chat tail",
|
||||
);
|
||||
assert.equal(transaction.frozenRecallOptions.lockedSource, "send-intent");
|
||||
assert.equal(transaction.frozenRecallOptions.lockedSourceLabel, "发送意图");
|
||||
assert.equal(
|
||||
transaction.frozenRecallOptions.lockedReason,
|
||||
"send-intent-overrides-chat-tail",
|
||||
);
|
||||
assert.equal(
|
||||
transaction.frozenRecallOptions.sourceCandidates[0]?.text,
|
||||
"刚触发发送的新输入",
|
||||
);
|
||||
}
|
||||
|
||||
async function testGenerationRecallSendIntentWinsOverHostSnapshotStably() {
|
||||
const harness = await createGenerationRecallHarness();
|
||||
harness.chat = [{ is_user: false, mes: "assistant-tail" }];
|
||||
harness.pendingRecallSendIntent = {
|
||||
text: "发送意图优先输入",
|
||||
hash: "hash-send-intent-vs-host",
|
||||
at: Date.now(),
|
||||
source: "dom-intent",
|
||||
};
|
||||
const frozenSnapshot =
|
||||
harness.result.freezeHostGenerationInputSnapshot("宿主快照输入");
|
||||
|
||||
await harness.result.onGenerationAfterCommands(
|
||||
"normal",
|
||||
{ frozenInputSnapshot: frozenSnapshot },
|
||||
false,
|
||||
);
|
||||
await harness.result.onBeforeCombinePrompts();
|
||||
|
||||
assert.equal(harness.runRecallCalls.length, 1);
|
||||
assert.equal(
|
||||
harness.runRecallCalls[0].overrideUserMessage,
|
||||
"发送意图优先输入",
|
||||
);
|
||||
assert.equal(harness.runRecallCalls[0].overrideSource, "send-intent");
|
||||
assert.equal(
|
||||
JSON.stringify(
|
||||
harness.runRecallCalls[0].sourceCandidates.map(
|
||||
(candidate) => candidate.source,
|
||||
),
|
||||
),
|
||||
JSON.stringify(["send-intent", "host-generation-lifecycle"]),
|
||||
);
|
||||
assert.equal(harness.applyFinalCalls.length, 1);
|
||||
}
|
||||
|
||||
async function testGenerationRecallLockedSourceDoesNotDriftWithinTransaction() {
|
||||
const harness = await createGenerationRecallHarness();
|
||||
harness.chat = [{ is_user: false, mes: "assistant-tail" }];
|
||||
harness.pendingRecallSendIntent = {
|
||||
text: "事务锁定输入-A",
|
||||
hash: "hash-locked-source",
|
||||
at: Date.now(),
|
||||
source: "dom-intent",
|
||||
};
|
||||
|
||||
await harness.result.onGenerationAfterCommands("normal", {}, false);
|
||||
harness.pendingRecallSendIntent = {
|
||||
text: "事务漂移输入-B",
|
||||
hash: "hash-drift-source",
|
||||
at: Date.now(),
|
||||
source: "dom-intent",
|
||||
};
|
||||
await harness.result.onBeforeCombinePrompts();
|
||||
|
||||
assert.equal(harness.runRecallCalls.length, 1);
|
||||
assert.equal(harness.runRecallCalls[0].overrideUserMessage, "事务漂移输入-B");
|
||||
const transaction = [
|
||||
...harness.result.generationRecallTransactions.values(),
|
||||
][0];
|
||||
assert.equal(
|
||||
transaction.frozenRecallOptions.overrideUserMessage,
|
||||
"事务漂移输入-B",
|
||||
);
|
||||
assert.equal(transaction.frozenRecallOptions.lockedSource, "send-intent");
|
||||
assert.equal(transaction.frozenRecallOptions.lockedSourceLabel, "发送意图");
|
||||
assert.equal(
|
||||
transaction.frozenRecallOptions.lockedReason,
|
||||
"send-intent-captured",
|
||||
);
|
||||
}
|
||||
|
||||
async function testBeforeCombineRecallNotSkippedWhenGraphLoadingButRuntimeGraphReadable() {
|
||||
const { runRecallController } = await import("../recall-controller.js");
|
||||
const statuses = [];
|
||||
@@ -3593,6 +3759,9 @@ await testGenerationRecallSkipsUntilTargetUserFloorAvailable();
|
||||
await testGenerationRecallBeforeCombineCanUseProvisionalSendIntentBinding();
|
||||
await testGenerationRecallHostLifecycleSnapshotSurvivesTextareaClearWithoutDomIntent();
|
||||
await testGenerationRecallAfterCommandsStillSkipsWithoutStableUserFloor();
|
||||
await testGenerationRecallSendIntentBeatsChatTailAndStaysObservable();
|
||||
await testGenerationRecallSendIntentWinsOverHostSnapshotStably();
|
||||
await testGenerationRecallLockedSourceDoesNotDriftWithinTransaction();
|
||||
await testGenerationRecallSameKeyCanRunAgainImmediatelyAsNewGeneration();
|
||||
await testGenerationRecallSameKeyCanRunAgainAfterBridgeWindow();
|
||||
await testBeforeCombineRecallNotSkippedWhenGraphLoadingButRuntimeGraphReadable();
|
||||
|
||||
623
tests/triviumdb-poc.mjs
Normal file
623
tests/triviumdb-poc.mjs
Normal file
@@ -0,0 +1,623 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { createRequire } from "node:module";
|
||||
import { performance } from "node:perf_hooks";
|
||||
import { spawn } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const PREFIX = "[ST-BME][PoC]";
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const TMP_ROOT = path.join(__dirname, ".tmp-triviumdb-poc");
|
||||
const DIM = 8;
|
||||
|
||||
function log(...args) {
|
||||
console.log(PREFIX, ...args);
|
||||
}
|
||||
|
||||
function toErrorMessage(error) {
|
||||
if (!error) return "Unknown error";
|
||||
if (error instanceof Error) return error.message;
|
||||
return String(error);
|
||||
}
|
||||
|
||||
function assert(condition, message) {
|
||||
if (!condition) {
|
||||
throw new Error(message || "Assertion failed");
|
||||
}
|
||||
}
|
||||
|
||||
function vecFrom(seed, dim = DIM) {
|
||||
const out = [];
|
||||
for (let i = 0; i < dim; i += 1) {
|
||||
const value = Math.sin(seed * 0.37 + i * 0.73) + Math.cos(seed * 0.17 + i * 0.29);
|
||||
out.push(Number(value.toFixed(6)));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function near(vec, jitter = 0.01) {
|
||||
return vec.map((v, i) => Number((v + (i % 2 === 0 ? jitter : -jitter)).toFixed(6)));
|
||||
}
|
||||
|
||||
async function ensureCleanDir(dir) {
|
||||
await fs.rm(dir, { recursive: true, force: true });
|
||||
await fs.mkdir(dir, { recursive: true });
|
||||
}
|
||||
|
||||
function delay(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function spawnNode(args, { timeoutMs = 6000, expectExitCode = null } = {}) {
|
||||
return await new Promise((resolve, reject) => {
|
||||
const child = spawn(process.execPath, [__filename, ...args], {
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
windowsHide: true,
|
||||
});
|
||||
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
let finished = false;
|
||||
|
||||
const timer = timeoutMs > 0
|
||||
? setTimeout(() => {
|
||||
if (finished) return;
|
||||
child.kill("SIGKILL");
|
||||
reject(new Error(`Child process timeout after ${timeoutMs}ms`));
|
||||
}, timeoutMs)
|
||||
: null;
|
||||
|
||||
child.stdout?.on("data", (chunk) => {
|
||||
stdout += chunk.toString();
|
||||
});
|
||||
child.stderr?.on("data", (chunk) => {
|
||||
stderr += chunk.toString();
|
||||
});
|
||||
|
||||
child.on("error", (error) => {
|
||||
finished = true;
|
||||
if (timer) clearTimeout(timer);
|
||||
reject(error);
|
||||
});
|
||||
|
||||
child.on("close", (code, signal) => {
|
||||
finished = true;
|
||||
if (timer) clearTimeout(timer);
|
||||
if (expectExitCode !== null && code !== expectExitCode) {
|
||||
const hint = `expected=${expectExitCode}, actual=${code}, signal=${signal}`;
|
||||
return reject(
|
||||
new Error(`Child exit mismatch: ${hint}\nstdout:\n${stdout}\nstderr:\n${stderr}`),
|
||||
);
|
||||
}
|
||||
resolve({ code, signal, stdout, stderr });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function resolveTriviumDbCtor() {
|
||||
const require = createRequire(import.meta.url);
|
||||
const loaded = require("triviumdb");
|
||||
const TriviumDB = loaded?.TriviumDB || loaded?.default || loaded;
|
||||
if (typeof TriviumDB !== "function") {
|
||||
throw new Error("triviumdb loaded but TriviumDB constructor not found");
|
||||
}
|
||||
return { TriviumDB, loaded };
|
||||
}
|
||||
|
||||
async function runChildMode() {
|
||||
const mode = process.argv[2];
|
||||
if (!mode || !mode.startsWith("--child-")) return false;
|
||||
|
||||
try {
|
||||
const { TriviumDB } = resolveTriviumDbCtor();
|
||||
const dbPath = process.argv[3];
|
||||
const dim = Number(process.argv[4] || DIM);
|
||||
|
||||
if (!dbPath) {
|
||||
throw new Error("missing db path");
|
||||
}
|
||||
|
||||
if (mode === "--child-flush-writer") {
|
||||
await fs.mkdir(path.dirname(dbPath), { recursive: true });
|
||||
const db = new TriviumDB(dbPath, dim, "f32", "normal");
|
||||
const v = vecFrom(11, dim);
|
||||
const idA = db.insert(v, {
|
||||
chatId: "chat-flush",
|
||||
sourceFloor: 11,
|
||||
sourceRole: "assistant",
|
||||
label: "flush-a",
|
||||
});
|
||||
const idB = db.insert(near(v, 0.02), {
|
||||
chatId: "chat-flush",
|
||||
sourceFloor: 12,
|
||||
sourceRole: "user",
|
||||
label: "flush-b",
|
||||
});
|
||||
db.link(idA, idB, "related", 0.91);
|
||||
db.flush();
|
||||
console.log(JSON.stringify({ idA, idB }));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (mode === "--child-wal-writer") {
|
||||
await fs.mkdir(path.dirname(dbPath), { recursive: true });
|
||||
const db = new TriviumDB(dbPath, dim, "f32", "normal");
|
||||
const base = vecFrom(17, dim);
|
||||
const idA = db.insert(base, {
|
||||
chatId: "chat-wal",
|
||||
sourceFloor: 21,
|
||||
sourceRole: "assistant",
|
||||
label: "wal-a",
|
||||
});
|
||||
const idB = db.insert(near(base, 0.015), {
|
||||
chatId: "chat-wal",
|
||||
sourceFloor: 22,
|
||||
sourceRole: "assistant",
|
||||
label: "wal-b",
|
||||
});
|
||||
db.link(idA, idB, "wal-link", 0.88);
|
||||
console.log(JSON.stringify({ idA, idB, ready: true }));
|
||||
setInterval(() => {
|
||||
// Keep process alive to be killed by parent, simulating crash.
|
||||
}, 1000);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (mode === "--child-open-only") {
|
||||
const db = new TriviumDB(dbPath, dim, "f32", "normal");
|
||||
void db;
|
||||
console.log("child-opened-ok");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
throw new Error(`unknown child mode: ${mode}`);
|
||||
} catch (error) {
|
||||
console.error(`${PREFIX}[child]`, toErrorMessage(error));
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (await runChildMode()) return;
|
||||
|
||||
const report = {
|
||||
installability: "unknown",
|
||||
addonLoadability: "unknown",
|
||||
walRecovery: "unknown",
|
||||
dimensionBehavior: "unknown",
|
||||
averageSearchLatencyMs: null,
|
||||
recommendedSyncMode: "normal",
|
||||
goNoGo: "NO_GO",
|
||||
notes: [],
|
||||
cases: [],
|
||||
environment: {
|
||||
node: process.version,
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
},
|
||||
};
|
||||
|
||||
const keyStatus = {
|
||||
loadModule: false,
|
||||
crud: false,
|
||||
search: false,
|
||||
flushReopen: false,
|
||||
walRecovery: false,
|
||||
dimensionMismatch: false,
|
||||
benchmark: false,
|
||||
concurrentSafety: false,
|
||||
};
|
||||
|
||||
let TriviumDB = null;
|
||||
let triviumVersion = "unknown";
|
||||
|
||||
const runCase = async (name, fn) => {
|
||||
const startedAt = performance.now();
|
||||
try {
|
||||
await fn();
|
||||
const durationMs = Number((performance.now() - startedAt).toFixed(2));
|
||||
report.cases.push({ name, status: "passed", durationMs });
|
||||
log(`✔ ${name} (${durationMs}ms)`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
const durationMs = Number((performance.now() - startedAt).toFixed(2));
|
||||
const message = toErrorMessage(error);
|
||||
report.cases.push({ name, status: "failed", durationMs, error: message });
|
||||
report.notes.push(`${name} failed: ${message}`);
|
||||
log(`✘ ${name} (${durationMs}ms): ${message}`);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
await ensureCleanDir(TMP_ROOT);
|
||||
|
||||
await runCase("module-loadability", async () => {
|
||||
const ctorResult = resolveTriviumDbCtor();
|
||||
TriviumDB = ctorResult.TriviumDB;
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
try {
|
||||
const pkg = require("triviumdb/package.json");
|
||||
triviumVersion = pkg?.version || "unknown";
|
||||
} catch {
|
||||
triviumVersion = "unknown";
|
||||
}
|
||||
|
||||
report.installability = "pass";
|
||||
report.addonLoadability = "pass";
|
||||
keyStatus.loadModule = true;
|
||||
report.environment.triviumdb = triviumVersion;
|
||||
});
|
||||
|
||||
if (!TriviumDB) {
|
||||
report.installability = "fail";
|
||||
report.addonLoadability = "fail";
|
||||
report.goNoGo = "NO_GO";
|
||||
printFinalReport(report, keyStatus);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await runCase("crud-link-neighbors-filter-flush", async () => {
|
||||
const dbPath = path.join(TMP_ROOT, "crud.tdb");
|
||||
const db = new TriviumDB(dbPath, DIM, "f32", "normal");
|
||||
|
||||
const v1 = vecFrom(1);
|
||||
const v2 = near(v1, 0.005);
|
||||
const v3 = vecFrom(99);
|
||||
|
||||
const id1 = db.insert(v1, {
|
||||
chatId: "chat-crud",
|
||||
sourceFloor: 1,
|
||||
sourceRole: "assistant",
|
||||
label: "node-1",
|
||||
});
|
||||
const id2 = db.insert(v2, {
|
||||
chatId: "chat-crud",
|
||||
sourceFloor: 1,
|
||||
sourceRole: "user",
|
||||
label: "node-2",
|
||||
});
|
||||
const id3 = db.insert(v3, {
|
||||
chatId: "chat-crud",
|
||||
sourceFloor: 2,
|
||||
sourceRole: "assistant",
|
||||
label: "node-3",
|
||||
});
|
||||
|
||||
const got1 = db.get(id1);
|
||||
assert(got1 && got1.payload?.label === "node-1", "get(id1) should return inserted payload");
|
||||
|
||||
db.updatePayload(id1, {
|
||||
...got1.payload,
|
||||
label: "node-1-updated",
|
||||
sourceFloor: 3,
|
||||
});
|
||||
const got1Updated = db.get(id1);
|
||||
assert(got1Updated?.payload?.label === "node-1-updated", "updatePayload should take effect");
|
||||
|
||||
db.updateVector(id2, near(v2, 0.02));
|
||||
const got2Updated = db.get(id2);
|
||||
assert(Array.isArray(got2Updated?.vector), "updateVector should keep vector readable");
|
||||
|
||||
db.link(id1, id2, "related", 0.9);
|
||||
db.link(id1, id3, "related", 0.7);
|
||||
const neighbors = db.neighbors(id1, 1);
|
||||
assert(neighbors.includes(id2), "neighbors should contain linked id2");
|
||||
assert(neighbors.includes(id3), "neighbors should contain linked id3");
|
||||
|
||||
db.unlink(id1, id3);
|
||||
const neighborsAfterUnlink = db.neighbors(id1, 1);
|
||||
assert(!neighborsAfterUnlink.includes(id3), "unlink should remove edge id1->id3");
|
||||
|
||||
const floorMatched = db.filterWhere({ sourceFloor: 1 });
|
||||
assert(floorMatched.length >= 1, "filterWhere(sourceFloor=1) should return at least one node");
|
||||
assert(
|
||||
floorMatched.every((item) => item.payload?.sourceFloor === 1),
|
||||
"filterWhere should only return matching sourceFloor",
|
||||
);
|
||||
|
||||
db.delete(id3);
|
||||
const got3 = db.get(id3);
|
||||
assert(got3 === null, "deleted node should not be retrievable");
|
||||
|
||||
db.flush();
|
||||
keyStatus.crud = true;
|
||||
});
|
||||
|
||||
await runCase("search-expandDepth", async () => {
|
||||
const dbPath = path.join(TMP_ROOT, "search.tdb");
|
||||
const db = new TriviumDB(dbPath, DIM, "f32", "normal");
|
||||
|
||||
const clusterBase = vecFrom(5);
|
||||
const idAnchor = db.insert(clusterBase, {
|
||||
chatId: "chat-search",
|
||||
sourceFloor: 10,
|
||||
sourceRole: "assistant",
|
||||
label: "anchor",
|
||||
});
|
||||
const idNear = db.insert(near(clusterBase, 0.004), {
|
||||
chatId: "chat-search",
|
||||
sourceFloor: 11,
|
||||
sourceRole: "assistant",
|
||||
label: "near",
|
||||
});
|
||||
const idFar = db.insert(vecFrom(777), {
|
||||
chatId: "chat-search",
|
||||
sourceFloor: 99,
|
||||
sourceRole: "assistant",
|
||||
label: "far",
|
||||
});
|
||||
|
||||
db.link(idAnchor, idFar, "jump", 0.8);
|
||||
|
||||
const query = near(clusterBase, 0.002);
|
||||
const depth0 = db.search(query, 2, 0, -1);
|
||||
assert(Array.isArray(depth0) && depth0.length >= 1, "search depth0 should return hits");
|
||||
const depth0Ids = new Set(depth0.map((h) => h.id));
|
||||
assert(depth0Ids.has(idAnchor) || depth0Ids.has(idNear), "depth0 should include anchor cluster");
|
||||
|
||||
const depth1 = db.search(query, 2, 1, -1);
|
||||
assert(Array.isArray(depth1) && depth1.length >= depth0.length, "depth1 should not shrink result size");
|
||||
|
||||
keyStatus.search = true;
|
||||
});
|
||||
|
||||
await runCase("flush-reopen-consistency", async () => {
|
||||
const dbPath = path.join(TMP_ROOT, "flush-reopen.tdb");
|
||||
const writer = await spawnNode(["--child-flush-writer", dbPath, String(DIM)], {
|
||||
timeoutMs: 5000,
|
||||
expectExitCode: 0,
|
||||
});
|
||||
|
||||
const writerPayload = JSON.parse(writer.stdout.trim() || "{}");
|
||||
const db = new TriviumDB(dbPath, DIM, "f32", "normal");
|
||||
const gotA = db.get(writerPayload.idA);
|
||||
const gotB = db.get(writerPayload.idB);
|
||||
assert(gotA && gotB, "reopen after flush should read inserted nodes");
|
||||
|
||||
const nearA = db.neighbors(writerPayload.idA, 1);
|
||||
assert(nearA.includes(writerPayload.idB), "reopen should preserve edge relation");
|
||||
|
||||
keyStatus.flushReopen = true;
|
||||
});
|
||||
|
||||
await runCase("wal-crash-recovery", async () => {
|
||||
const dbPath = path.join(TMP_ROOT, "wal-recovery.tdb");
|
||||
|
||||
const child = spawn(process.execPath, [__filename, "--child-wal-writer", dbPath, String(DIM)], {
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
windowsHide: true,
|
||||
});
|
||||
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
child.stdout?.on("data", (chunk) => {
|
||||
stdout += chunk.toString();
|
||||
});
|
||||
child.stderr?.on("data", (chunk) => {
|
||||
stderr += chunk.toString();
|
||||
});
|
||||
|
||||
await delay(700);
|
||||
child.kill("SIGKILL");
|
||||
|
||||
await new Promise((resolve) => child.once("close", resolve));
|
||||
|
||||
const lines = stdout
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
assert(lines.length >= 1, `wal child stdout is empty. stderr: ${stderr}`);
|
||||
const childPayload = JSON.parse(lines[0]);
|
||||
|
||||
const walPath = `${dbPath}.wal`;
|
||||
const walExists = await fs
|
||||
.access(walPath)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
|
||||
const db = new TriviumDB(dbPath, DIM, "f32", "normal");
|
||||
const gotA = db.get(childPayload.idA);
|
||||
const gotB = db.get(childPayload.idB);
|
||||
assert(gotA && gotB, "reopen after crash should recover inserted nodes from WAL");
|
||||
|
||||
const nearA = db.neighbors(childPayload.idA, 1);
|
||||
assert(nearA.includes(childPayload.idB), "recovered graph should include linked edge");
|
||||
|
||||
report.walRecovery = "pass";
|
||||
report.notes.push(`WAL file existed after crash: ${walExists}`);
|
||||
keyStatus.walRecovery = true;
|
||||
});
|
||||
|
||||
await runCase("dimension-mismatch-path", async () => {
|
||||
const dbPath = path.join(TMP_ROOT, "dim-mismatch.tdb");
|
||||
|
||||
await spawnNode(["--child-flush-writer", dbPath, String(DIM)], {
|
||||
timeoutMs: 5000,
|
||||
expectExitCode: 0,
|
||||
});
|
||||
|
||||
let mismatchError = null;
|
||||
let dbWrong = null;
|
||||
try {
|
||||
dbWrong = new TriviumDB(dbPath, DIM + 1, "f32", "normal");
|
||||
} catch (error) {
|
||||
mismatchError = error;
|
||||
}
|
||||
|
||||
if (mismatchError) {
|
||||
const message = toErrorMessage(mismatchError).toLowerCase();
|
||||
assert(
|
||||
message.includes("dim") || message.includes("dimension") || message.includes("mismatch"),
|
||||
`unexpected mismatch error message: ${toErrorMessage(mismatchError)}`,
|
||||
);
|
||||
report.notes.push("dimension mismatch is rejected at constructor time");
|
||||
} else {
|
||||
// Newer triviumdb versions may not throw on constructor if file already exists.
|
||||
// In that case, dimension lock must still be effectively enforced at operation level.
|
||||
assert(dbWrong, "dbWrong instance should exist when constructor does not throw");
|
||||
|
||||
const actualDim = typeof dbWrong.dim === "function" ? dbWrong.dim() : null;
|
||||
if (typeof actualDim === "number") {
|
||||
assert(actualDim === DIM, `existing db dim should remain ${DIM}, got ${actualDim}`);
|
||||
}
|
||||
|
||||
let insertMismatchError = null;
|
||||
try {
|
||||
dbWrong.insert(vecFrom(333, DIM + 1), {
|
||||
chatId: "chat-dim",
|
||||
sourceFloor: 1,
|
||||
sourceRole: "assistant",
|
||||
label: "dim-mismatch-probe",
|
||||
});
|
||||
} catch (error) {
|
||||
insertMismatchError = error;
|
||||
}
|
||||
assert(insertMismatchError, "dimension mismatch should be rejected at operation level");
|
||||
report.notes.push("dimension mismatch is rejected at operation level (constructor tolerated)");
|
||||
}
|
||||
|
||||
report.dimensionBehavior = "pass";
|
||||
keyStatus.dimensionMismatch = true;
|
||||
});
|
||||
|
||||
await runCase("benchmark-100plus-nodes", async () => {
|
||||
const dbPath = path.join(TMP_ROOT, "benchmark.tdb");
|
||||
const db = new TriviumDB(dbPath, DIM, "f32", "normal");
|
||||
|
||||
const totalNodes = 200;
|
||||
const ids = [];
|
||||
for (let i = 0; i < totalNodes; i += 1) {
|
||||
const id = db.insert(vecFrom(i + 1000), {
|
||||
chatId: "chat-bench",
|
||||
sourceFloor: i % 30,
|
||||
sourceRole: i % 2 === 0 ? "assistant" : "user",
|
||||
label: `bench-${i}`,
|
||||
});
|
||||
ids.push(id);
|
||||
}
|
||||
|
||||
assert(ids.length === totalNodes, "benchmark insert count mismatch");
|
||||
|
||||
const query = vecFrom(1042);
|
||||
const rounds = 60;
|
||||
let totalMs = 0;
|
||||
for (let i = 0; i < rounds; i += 1) {
|
||||
const t0 = performance.now();
|
||||
const hits = db.search(query, 8, 1, -1);
|
||||
totalMs += performance.now() - t0;
|
||||
assert(Array.isArray(hits), "benchmark search should return array");
|
||||
}
|
||||
|
||||
const avg = Number((totalMs / rounds).toFixed(3));
|
||||
report.averageSearchLatencyMs = avg;
|
||||
keyStatus.benchmark = true;
|
||||
});
|
||||
|
||||
await runCase("concurrent-safety-basic", async () => {
|
||||
const dbPath = path.join(TMP_ROOT, "concurrency.tdb");
|
||||
const db = new TriviumDB(dbPath, DIM, "f32", "normal");
|
||||
|
||||
const base = vecFrom(88);
|
||||
const id0 = db.insert(base, {
|
||||
chatId: "chat-concurrency",
|
||||
sourceFloor: 1,
|
||||
sourceRole: "assistant",
|
||||
label: "seed",
|
||||
});
|
||||
|
||||
const tasks = Array.from({ length: 8 }, (_, i) =>
|
||||
Promise.resolve().then(() => {
|
||||
const id = db.insert(near(base, 0.001 * (i + 1)), {
|
||||
chatId: "chat-concurrency",
|
||||
sourceFloor: i + 2,
|
||||
sourceRole: "assistant",
|
||||
label: `c-${i}`,
|
||||
});
|
||||
db.link(id0, id, "fanout", 0.5 + i * 0.01);
|
||||
const got = db.get(id);
|
||||
assert(got && got.payload?.label === `c-${i}`, `concurrency insert/get failed at task ${i}`);
|
||||
const hits = db.search(base, 5, 1, -1);
|
||||
assert(Array.isArray(hits), "concurrency search should return array");
|
||||
}),
|
||||
);
|
||||
|
||||
await Promise.all(tasks);
|
||||
|
||||
const openedByChild = await spawnNode(["--child-open-only", dbPath, String(DIM)], {
|
||||
timeoutMs: 5000,
|
||||
}).then(
|
||||
(res) => res,
|
||||
(error) => ({ error }),
|
||||
);
|
||||
|
||||
if (openedByChild?.error) {
|
||||
const msg = toErrorMessage(openedByChild.error).toLowerCase();
|
||||
const lockLikely = msg.includes("lock") || msg.includes("busy") || msg.includes("in use") || msg.includes("already");
|
||||
assert(lockLikely, `expected lock error for second process open, got: ${toErrorMessage(openedByChild.error)}`);
|
||||
} else {
|
||||
assert(openedByChild.code !== 0, "second process open should fail while parent instance is active");
|
||||
}
|
||||
|
||||
keyStatus.concurrentSafety = true;
|
||||
});
|
||||
|
||||
const required = [
|
||||
keyStatus.loadModule,
|
||||
keyStatus.crud,
|
||||
keyStatus.search,
|
||||
keyStatus.flushReopen,
|
||||
keyStatus.walRecovery,
|
||||
keyStatus.dimensionMismatch,
|
||||
keyStatus.benchmark,
|
||||
keyStatus.concurrentSafety,
|
||||
];
|
||||
|
||||
const allRequiredPassed = required.every(Boolean);
|
||||
|
||||
if (allRequiredPassed) {
|
||||
report.goNoGo = "GO";
|
||||
report.installability = "pass";
|
||||
report.addonLoadability = "pass";
|
||||
report.walRecovery = report.walRecovery === "unknown" ? "pass" : report.walRecovery;
|
||||
report.dimensionBehavior = report.dimensionBehavior === "unknown" ? "pass" : report.dimensionBehavior;
|
||||
report.recommendedSyncMode = "normal";
|
||||
report.notes.push("Recommended syncMode=normal for balanced durability/performance in ST-BME plugin workload.");
|
||||
} else {
|
||||
report.goNoGo = "NO_GO";
|
||||
if (report.installability === "unknown") report.installability = "fail";
|
||||
if (report.addonLoadability === "unknown") report.addonLoadability = "fail";
|
||||
if (report.walRecovery === "unknown") report.walRecovery = keyStatus.walRecovery ? "pass" : "fail";
|
||||
if (report.dimensionBehavior === "unknown") report.dimensionBehavior = keyStatus.dimensionMismatch ? "pass" : "fail";
|
||||
report.notes.push("One or more critical Phase-0 checks failed. Consider Plan B sidecar mode.");
|
||||
}
|
||||
|
||||
printFinalReport(report, keyStatus);
|
||||
|
||||
process.exit(allRequiredPassed ? 0 : 1);
|
||||
}
|
||||
|
||||
function printFinalReport(report, keyStatus) {
|
||||
const passed = report.cases.filter((item) => item.status === "passed").length;
|
||||
const failed = report.cases.filter((item) => item.status === "failed").length;
|
||||
|
||||
log("================ FINAL SUMMARY ================");
|
||||
log(`cases: total=${report.cases.length}, passed=${passed}, failed=${failed}`);
|
||||
log(`installability=${report.installability}, addonLoadability=${report.addonLoadability}`);
|
||||
log(`walRecovery=${report.walRecovery}, dimensionBehavior=${report.dimensionBehavior}`);
|
||||
log(`averageSearchLatencyMs=${report.averageSearchLatencyMs}`);
|
||||
log(`recommendedSyncMode=${report.recommendedSyncMode}`);
|
||||
log(`goNoGo=${report.goNoGo}`);
|
||||
log(`keyStatus=${JSON.stringify(keyStatus)}`);
|
||||
|
||||
console.log(`${PREFIX} report-json ${JSON.stringify(report, null, 2)}`);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(`${PREFIX} fatal:`, toErrorMessage(error));
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user