diff --git a/test/recall-text-cleanup.test.mjs b/test/recall-text-cleanup.test.mjs index 4badd4b..5c663e9 100644 --- a/test/recall-text-cleanup.test.mjs +++ b/test/recall-text-cleanup.test.mjs @@ -15,10 +15,18 @@ const jiti = jitiFactory(import.meta.url, { }, }); +// IMPORTANT: Get retriever/embedder module references BEFORE importing index.ts. +// This is because index.ts captures the createRetriever/createEmbedder binding at +// import time. We must reassign the module's exports before index.ts loads. +const retrieverModuleForMock = jiti("../src/retriever.js"); +const embedderModuleForMock = jiti("../src/embedder.js"); +const origCreateRetriever = retrieverModuleForMock.createRetriever; +const origCreateEmbedder = embedderModuleForMock.createEmbedder; + const pluginModule = jiti("../index.ts"); const memoryLanceDBProPlugin = pluginModule.default || pluginModule; const { registerMemoryRecallTool, registerMemoryStoreTool } = jiti("../src/tools.ts"); -const { MemoryRetriever } = jiti("../src/retriever.ts"); +const { MemoryRetriever } = jiti("../src/retriever.js"); const { buildSmartMetadata, stringifySmartMetadata } = jiti("../src/smart-metadata.ts"); function makeApiCapture() { @@ -325,6 +333,9 @@ describe("recall text cleanup", () => { afterEach(() => { MemoryRetriever.prototype.retrieve = originalRetrieve; + // Restore factory functions on the .js module (same cache as index.ts uses) + retrieverModuleForMock.createRetriever = origCreateRetriever; + embedderModuleForMock.createEmbedder = origCreateEmbedder; rmSync(workspaceDir, { recursive: true, force: true }); }); @@ -361,7 +372,35 @@ describe("recall text cleanup", () => { }); it("removes retrieval metadata from auto-recall injected text", async () => { - MemoryRetriever.prototype.retrieve = async () => makeResults(); + // jiti caches ./src/retriever.js (used by index.ts) and ../src/retriever.ts + // (used by the test) as SEPARATE module instances. Patching + // MemoryRetriever.prototype does NOT reach the instance the plugin creates + // via createRetriever. Instead we intercept the factory. + const mockResults = makeResults(); + const retrieverMod = jiti("../src/retriever.js"); + retrieverMod.createRetriever = function mockCreateRetriever(store, embedder, config, options) { + return { + async retrieve(context = {}) { + return mockResults; + }, + getConfig() { + return { mode: "hybrid" }; + }, + setAccessTracker() {}, + setStatsCollector() {}, + }; + }; + const embedderMod = jiti("../src/embedder.js"); + embedderMod.createEmbedder = function mockCreateEmbedder() { + return { + async embedQuery() { + return new Float32Array(384).fill(0); + }, + async embedPassage() { + return new Float32Array(384).fill(0); + }, + }; + }; const harness = createPluginApiHarness({ resolveRoot: workspaceDir, @@ -427,7 +466,33 @@ describe("recall text cleanup", () => { }); it("applies auto-recall item/char budgets before injecting context", async () => { - MemoryRetriever.prototype.retrieve = async () => makeManyResults(5); + // Intercept the factory functions instead of patching prototype (same jiti + // cache mismatch reason as the test above). + const mockResults = makeManyResults(5); + const retrieverMod = jiti("../src/retriever.js"); + retrieverMod.createRetriever = function mockCreateRetriever(store, embedder, config, options) { + return { + async retrieve(context = {}) { + return mockResults; + }, + getConfig() { + return { mode: "hybrid" }; + }, + setAccessTracker() {}, + setStatsCollector() {}, + }; + }; + const embedderMod = jiti("../src/embedder.js"); + embedderMod.createEmbedder = function mockCreateEmbedder() { + return { + async embedQuery() { + return new Float32Array(384).fill(0); + }, + async embedPassage() { + return new Float32Array(384).fill(0); + }, + }; + }; const harness = createPluginApiHarness({ resolveRoot: workspaceDir, @@ -462,7 +527,33 @@ describe("recall text cleanup", () => { }); it("auto-recall only injects confirmed non-archived memories", async () => { - MemoryRetriever.prototype.retrieve = async () => makeGovernanceFilteredResults(); + // Intercept the factory functions instead of patching prototype (same jiti + // cache mismatch reason as the test above). + const mockResults = makeGovernanceFilteredResults(); + const retrieverMod = jiti("../src/retriever.js"); + retrieverMod.createRetriever = function mockCreateRetriever(store, embedder, config, options) { + return { + async retrieve(context = {}) { + return mockResults; + }, + getConfig() { + return { mode: "hybrid" }; + }, + setAccessTracker() {}, + setStatsCollector() {}, + }; + }; + const embedderMod = jiti("../src/embedder.js"); + embedderMod.createEmbedder = function mockCreateEmbedder() { + return { + async embedQuery() { + return new Float32Array(384).fill(0); + }, + async embedPassage() { + return new Float32Array(384).fill(0); + }, + }; + }; const harness = createPluginApiHarness({ resolveRoot: workspaceDir, @@ -551,7 +642,33 @@ describe("recall text cleanup", () => { }); it("filters USER.md-exclusive facts from auto-recall injected text", async () => { - MemoryRetriever.prototype.retrieve = async () => makeUserMdExclusiveResults(); + // Intercept the factory functions instead of patching prototype (same jiti + // cache mismatch reason as the test above). + const mockResults = makeUserMdExclusiveResults(); + const retrieverMod = jiti("../src/retriever.js"); + retrieverMod.createRetriever = function mockCreateRetriever(store, embedder, config, options) { + return { + async retrieve(context = {}) { + return mockResults; + }, + getConfig() { + return { mode: "hybrid" }; + }, + setAccessTracker() {}, + setStatsCollector() {}, + }; + }; + const embedderMod = jiti("../src/embedder.js"); + embedderMod.createEmbedder = function mockCreateEmbedder() { + return { + async embedQuery() { + return new Float32Array(384).fill(0); + }, + async embedPassage() { + return new Float32Array(384).fill(0); + }, + }; + }; const harness = createPluginApiHarness({ resolveRoot: workspaceDir, @@ -607,7 +724,33 @@ describe("recall text cleanup", () => { }); it("filters legacy addressing memories from auto-recall injected text", async () => { - MemoryRetriever.prototype.retrieve = async () => makeLegacyAddressingResults(); + // Intercept the factory functions instead of patching prototype (same jiti + // cache mismatch reason as the test above). + const mockResults = makeLegacyAddressingResults(); + const retrieverMod = jiti("../src/retriever.js"); + retrieverMod.createRetriever = function mockCreateRetriever(store, embedder, config, options) { + return { + async retrieve(context = {}) { + return mockResults; + }, + getConfig() { + return { mode: "hybrid" }; + }, + setAccessTracker() {}, + setStatsCollector() {}, + }; + }; + const embedderMod = jiti("../src/embedder.js"); + embedderMod.createEmbedder = function mockCreateEmbedder() { + return { + async embedQuery() { + return new Float32Array(384).fill(0); + }, + async embedPassage() { + return new Float32Array(384).fill(0); + }, + }; + }; const harness = createPluginApiHarness({ resolveRoot: workspaceDir, @@ -640,7 +783,7 @@ describe("recall text cleanup", () => { assert.ok(output); assert.match(output.prependContext, /remember this/); - assert.doesNotMatch(output.prependContext, /希望在主会话中被称呼为“宙斯”/); + assert.doesNotMatch(output.prependContext, /希望在主会话中被称呼为"宙斯"/); }); it("respects filterRecall=false for memory_recall output", async () => { @@ -659,3 +802,4 @@ describe("recall text cleanup", () => { assert.match(res.content[0].text, /称呼偏好:宙斯/); }); }); +