diff --git a/package.json b/package.json index f61357a4..65a70141 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "opencode-mem", - "version": "2.34.0", + "version": "2.35.0", "description": "OpenCode plugin that gives coding agents persistent memory using local Turso/libSQL vector search", "type": "module", "main": "dist/plugin.js", diff --git a/tests/add-memory-phase-logging.test.ts b/tests/add-memory-phase-logging.test.ts index 2d0f66d5..52d84695 100644 --- a/tests/add-memory-phase-logging.test.ts +++ b/tests/add-memory-phase-logging.test.ts @@ -38,6 +38,7 @@ mock.module(${JSON.stringify(configUrl)}, () => ({ mock.module(${JSON.stringify(loggerUrl)}, () => ({ log: (message, data) => logs.push({ message, data }), + safeLog: (message, data) => logs.push({ message, data }), serializeError: (error) => ({ error: error instanceof Error ? error.message : String(error), name: error instanceof Error ? error.name : undefined, diff --git a/tests/ai-cleanup.test.ts b/tests/ai-cleanup.test.ts index 0750e3e9..a998c675 100644 --- a/tests/ai-cleanup.test.ts +++ b/tests/ai-cleanup.test.ts @@ -120,7 +120,7 @@ mock.module(${JSON.stringify(configUrl)}, () => ({ }, })); -mock.module(${JSON.stringify(loggerUrl)}, () => ({ log: () => {}, serializeError: (error) => ({ error: error instanceof Error ? error.message : String(error) }) })); +mock.module(${JSON.stringify(loggerUrl)}, () => ({ log: () => {}, safeLog: () => {}, serializeError: (error) => ({ error: error instanceof Error ? error.message : String(error) }) })); mock.module(${JSON.stringify(opencodeProviderLoaderUrl)}, () => ({ loadOpencodeProvider: async () => ({ diff --git a/tests/api-handlers-container-tag-traversal.test.ts b/tests/api-handlers-container-tag-traversal.test.ts index 79a7ebf7..47eaafb4 100644 --- a/tests/api-handlers-container-tag-traversal.test.ts +++ b/tests/api-handlers-container-tag-traversal.test.ts @@ -74,6 +74,7 @@ mock.module(${JSON.stringify(userPromptManagerUrl)}, () => ({ mock.module(${JSON.stringify(loggerUrl)}, () => ({ log: () => {}, + safeLog: () => {}, serializeError: (error) => ({ error: error instanceof Error ? error.message : String(error) }), })); diff --git a/tests/api-handlers-profile-strip.test.ts b/tests/api-handlers-profile-strip.test.ts index a895105e..8787515d 100644 --- a/tests/api-handlers-profile-strip.test.ts +++ b/tests/api-handlers-profile-strip.test.ts @@ -58,6 +58,7 @@ const profileWithEmbeddings = ${JSON.stringify(profileWithEmbeddings)}; mock.module(${JSON.stringify(loggerUrl)}, () => ({ log: () => {}, + safeLog: () => {}, serializeError: (error) => ({ error: error instanceof Error ? error.message : String(error) }), })); diff --git a/tests/auto-capture-concurrency.test.ts b/tests/auto-capture-concurrency.test.ts index 24dc152a..3019a2ef 100644 --- a/tests/auto-capture-concurrency.test.ts +++ b/tests/auto-capture-concurrency.test.ts @@ -89,7 +89,7 @@ mock.module(${JSON.stringify(u("src/services/context.js"))}, () => ({ formatCont mock.module(${JSON.stringify(u("src/services/privacy.js"))}, () => ({ stripPrivateContent: (v) => v, isFullyPrivate: () => false, })); -mock.module(${JSON.stringify(u("src/services/logger.js"))}, () => ({ log: () => {}, serializeError: (error) => ({ error: error instanceof Error ? error.message : String(error) }) })); +mock.module(${JSON.stringify(u("src/services/logger.js"))}, () => ({ log: () => {}, safeLog: () => {}, serializeError: (error) => ({ error: error instanceof Error ? error.message : String(error) }) })); mock.module(${JSON.stringify(u("src/services/language-detector.js"))}, () => ({ detectLanguage: () => "en", getLanguageName: () => "English", })); diff --git a/tests/auto-capture-config-toast.test.ts b/tests/auto-capture-config-toast.test.ts index 50f17bac..7c9076eb 100644 --- a/tests/auto-capture-config-toast.test.ts +++ b/tests/auto-capture-config-toast.test.ts @@ -60,6 +60,7 @@ mock.module(${JSON.stringify(configUrl)}, () => ({ mock.module(${JSON.stringify(loggerUrl)}, () => ({ log: (message) => logs.push(message), + safeLog: () => {}, })); const { notifyAutoCaptureProviderStatus } = await import(${JSON.stringify(statusUrl)}); diff --git a/tests/auto-capture-provider-order.test.ts b/tests/auto-capture-provider-order.test.ts index 9dc83f4f..90ae10be 100644 --- a/tests/auto-capture-provider-order.test.ts +++ b/tests/auto-capture-provider-order.test.ts @@ -112,6 +112,7 @@ mock.module(${JSON.stringify(promptManagerUrl)}, () => ({ mock.module(${JSON.stringify(loggerUrl)}, () => ({ log: (message, data) => logs.push({ message, data }), + safeLog: (message, data) => logs.push({ message, data }), serializeError: (error) => ({ error: error instanceof Error ? error.message : String(error), name: error instanceof Error ? error.name : undefined, diff --git a/tests/auto-capture.test.ts b/tests/auto-capture.test.ts index 210cea39..6b8bed64 100644 --- a/tests/auto-capture.test.ts +++ b/tests/auto-capture.test.ts @@ -150,7 +150,7 @@ mock.module(${JSON.stringify(promptManagerUrl)}, () => ({ }, })); -mock.module(${JSON.stringify(loggerUrl)}, () => ({ log: () => {}, serializeError: (error) => ({ error: error instanceof Error ? error.message : String(error) }) })); +mock.module(${JSON.stringify(loggerUrl)}, () => ({ log: () => {}, safeLog: () => {}, serializeError: (error) => ({ error: error instanceof Error ? error.message : String(error) }) })); mock.module(${JSON.stringify(languageUrl)}, () => ({ detectLanguage: () => "en", getLanguageName: () => "English", @@ -279,7 +279,7 @@ mock.module(${JSON.stringify(promptManagerUrl)}, () => ({ }, })); -mock.module(${JSON.stringify(loggerUrl)}, () => ({ log: () => {}, serializeError: (error) => ({ error: error instanceof Error ? error.message : String(error) }) })); +mock.module(${JSON.stringify(loggerUrl)}, () => ({ log: () => {}, safeLog: () => {}, serializeError: (error) => ({ error: error instanceof Error ? error.message : String(error) }) })); mock.module(${JSON.stringify(languageUrl)}, () => ({ detectLanguage: () => "en", getLanguageName: () => "English", @@ -430,7 +430,7 @@ mock.module(${JSON.stringify(promptManagerUrl)}, () => ({ }, })); -mock.module(${JSON.stringify(loggerUrl)}, () => ({ log: () => {}, serializeError: (error) => ({ error: error instanceof Error ? error.message : String(error) }) })); +mock.module(${JSON.stringify(loggerUrl)}, () => ({ log: () => {}, safeLog: () => {}, serializeError: (error) => ({ error: error instanceof Error ? error.message : String(error) }) })); mock.module(${JSON.stringify(languageUrl)}, () => ({ detectLanguage: () => "en", getLanguageName: () => "English", diff --git a/tests/cleanup-ai-sessions.test.ts b/tests/cleanup-ai-sessions.test.ts index e4f3943f..2e6718da 100644 --- a/tests/cleanup-ai-sessions.test.ts +++ b/tests/cleanup-ai-sessions.test.ts @@ -46,6 +46,7 @@ mock.module(${JSON.stringify(configUrl)}, () => ({ mock.module(${JSON.stringify(loggerUrl)}, () => ({ log: () => {}, + safeLog: () => {}, serializeError: (error) => ({ error: String(error) }), })); diff --git a/tests/client-scope-traversal.test.ts b/tests/client-scope-traversal.test.ts index 506db1bf..e6af2a8c 100644 --- a/tests/client-scope-traversal.test.ts +++ b/tests/client-scope-traversal.test.ts @@ -73,6 +73,7 @@ mock.module(${JSON.stringify(configUrl)}, () => ({ mock.module(${JSON.stringify(loggerUrl)}, () => ({ log: () => {}, + safeLog: () => {}, serializeError: (error) => ({ error: error instanceof Error ? error.message : String(error) }), })); diff --git a/tests/compaction-agent-preservation.test.ts b/tests/compaction-agent-preservation.test.ts index 0a01be69..e29b49fa 100644 --- a/tests/compaction-agent-preservation.test.ts +++ b/tests/compaction-agent-preservation.test.ts @@ -198,7 +198,7 @@ mock.module(${JSON.stringify(webServerUrl)}, () => ({ startWebServer: async () => null, WebServer: class {}, })); -mock.module(${JSON.stringify(loggerUrl)}, () => ({ log: () => {}, serializeError: (error) => ({ error: error instanceof Error ? error.message : String(error) }) })); +mock.module(${JSON.stringify(loggerUrl)}, () => ({ log: () => {}, safeLog: () => {}, serializeError: (error) => ({ error: error instanceof Error ? error.message : String(error) }) })); mock.module(${JSON.stringify(languageUrl)}, () => ({ getLanguageName: () => "English" })); const mockClient = { diff --git a/tests/fixtures/profile-learning-lock-worker.mts b/tests/fixtures/profile-learning-lock-worker.mts index 4680641d..d971591e 100644 --- a/tests/fixtures/profile-learning-lock-worker.mts +++ b/tests/fixtures/profile-learning-lock-worker.mts @@ -81,7 +81,7 @@ mock.module(configUrl, () => ({ initConfig: () => {}, isConfigured: () => true, })); -mock.module(loggerUrl, () => ({ log: () => {} })); +mock.module(loggerUrl, () => ({ log: () => {}, safeLog: () => {} })); // current-boot-invalid: the LOCAL machine's boot identity reads as garbage. // The implementation must treat the local boot id as "unknown" and never use diff --git a/tests/fixtures/profile-learning-memory-writer.mjs b/tests/fixtures/profile-learning-memory-writer.mjs index 4da6971b..e41d63cd 100644 --- a/tests/fixtures/profile-learning-memory-writer.mjs +++ b/tests/fixtures/profile-learning-memory-writer.mjs @@ -74,6 +74,7 @@ mock.module(configUrl, () => ({ const loggerUrl = new URL("../../src/services/logger.js", import.meta.url).href; mock.module(loggerUrl, () => ({ log: () => {}, + safeLog: () => {}, serializeError: (error) => ({ error: error instanceof Error ? error.message : String(error) }), })); diff --git a/tests/fixtures/profile-learning-service-worker.mjs b/tests/fixtures/profile-learning-service-worker.mjs index 190fc4e2..0519ff60 100644 --- a/tests/fixtures/profile-learning-service-worker.mjs +++ b/tests/fixtures/profile-learning-service-worker.mjs @@ -100,6 +100,7 @@ mock.module(tagsUrl, () => ({ // off the lock rather than silently doing nothing. mock.module(loggerUrl, () => ({ log: (label, data) => event("log", { label, data }), + safeLog: (label, data) => event("log", { label, data }), serializeError: (error) => ({ error: error instanceof Error ? error.message : String(error) }), })); diff --git a/tests/fixtures/profile-learning-storage-error-worker.mjs b/tests/fixtures/profile-learning-storage-error-worker.mjs index 8a7983c0..aff579d8 100644 --- a/tests/fixtures/profile-learning-storage-error-worker.mjs +++ b/tests/fixtures/profile-learning-storage-error-worker.mjs @@ -127,6 +127,7 @@ mock.module(tagsUrl, () => ({ mock.module(loggerUrl, () => ({ log: (label, data) => event("log", { label, data }), + safeLog: (label, data) => event("log", { label, data }), serializeError: (error) => ({ error: error instanceof Error ? error.message : String(error) }), })); diff --git a/tests/fixtures/v2-provider-refresh.mjs b/tests/fixtures/v2-provider-refresh.mjs index 84a5dc92..b586b57b 100644 --- a/tests/fixtures/v2-provider-refresh.mjs +++ b/tests/fixtures/v2-provider-refresh.mjs @@ -142,6 +142,8 @@ mock.module(moduleUrl("services/user-prompt/user-prompt-manager"), () => ({ mock.module(moduleUrl("services/logger"), () => ({ log: (...args) => logs.push(args.map((a) => (typeof a === "string" ? a : JSON.stringify(a))).join(" ")), + safeLog: (...args) => + logs.push(args.map((a) => (typeof a === "string" ? a : JSON.stringify(a))).join(" ")), serializeError: (error) => ({ error: error instanceof Error ? error.message : String(error) }), })); diff --git a/tests/memory-portability-tool.test.ts b/tests/memory-portability-tool.test.ts index 81d2bbb6..4585f750 100644 --- a/tests/memory-portability-tool.test.ts +++ b/tests/memory-portability-tool.test.ts @@ -93,7 +93,7 @@ mock.module(${JSON.stringify(webServerUrl)}, () => ({ startWebServer: async () => null, WebServer: class {}, })); -mock.module(${JSON.stringify(loggerUrl)}, () => ({ log: () => {}, serializeError: (error) => ({ error: error instanceof Error ? error.message : String(error) }) })); +mock.module(${JSON.stringify(loggerUrl)}, () => ({ log: () => {}, safeLog: () => {}, serializeError: (error) => ({ error: error instanceof Error ? error.message : String(error) }) })); mock.module(${JSON.stringify(languageUrl)}, () => ({ getLanguageName: () => "English" })); const { OpenCodeMemPlugin } = await import(${JSON.stringify(indexUrl)}); diff --git a/tests/profile-learning-idle.test.ts b/tests/profile-learning-idle.test.ts index da510bc8..45742117 100644 --- a/tests/profile-learning-idle.test.ts +++ b/tests/profile-learning-idle.test.ts @@ -88,7 +88,7 @@ mock.module(${JSON.stringify(privacyUrl)}, () => ({ isFullyPrivate: () => false, })); mock.module(${JSON.stringify(promptManagerUrl)}, () => ({ userPromptManager: { savePrompt() {} } })); -mock.module(${JSON.stringify(loggerUrl)}, () => ({ log: () => {}, serializeError: (error) => ({ error: error instanceof Error ? error.message : String(error) }) })); +mock.module(${JSON.stringify(loggerUrl)}, () => ({ log: () => {}, safeLog: () => {}, serializeError: (error) => ({ error: error instanceof Error ? error.message : String(error) }) })); mock.module(${JSON.stringify(languageUrl)}, () => ({ getLanguageName: () => "English" })); // The web server only starts once the Turso readiness gate passes, and that // gate is what decides whether an owner exists at all. diff --git a/tests/tag-migration-opencode-provider.test.ts b/tests/tag-migration-opencode-provider.test.ts index fdba2ba0..6f96638e 100644 --- a/tests/tag-migration-opencode-provider.test.ts +++ b/tests/tag-migration-opencode-provider.test.ts @@ -116,6 +116,7 @@ mock.module(${JSON.stringify(userPromptManagerUrl)}, () => ({ mock.module(${JSON.stringify(loggerUrl)}, () => ({ log: () => {}, + safeLog: () => {}, serializeError: (error) => ({ error: error instanceof Error ? error.message : String(error) }), })); diff --git a/tests/tag-migration-skip-tagged.test.ts b/tests/tag-migration-skip-tagged.test.ts index 07e01499..86694b3d 100644 --- a/tests/tag-migration-skip-tagged.test.ts +++ b/tests/tag-migration-skip-tagged.test.ts @@ -110,6 +110,7 @@ mock.module(${JSON.stringify(userPromptManagerUrl)}, () => ({ mock.module(${JSON.stringify(loggerUrl)}, () => ({ log: () => {}, + safeLog: () => {}, serializeError: (error) => ({ error: error instanceof Error ? error.message : String(error) }), })); diff --git a/tests/tag-migration-soft-failure.test.ts b/tests/tag-migration-soft-failure.test.ts index 4610b8cc..144e4fc1 100644 --- a/tests/tag-migration-soft-failure.test.ts +++ b/tests/tag-migration-soft-failure.test.ts @@ -104,6 +104,7 @@ mock.module(${JSON.stringify(userPromptManagerUrl)}, () => ({ mock.module(${JSON.stringify(loggerUrl)}, () => ({ log: () => {}, + safeLog: () => {}, serializeError: (error) => ({ error: error instanceof Error ? error.message : String(error) }), })); diff --git a/tests/tool-scope.test.ts b/tests/tool-scope.test.ts index 9b822cfa..7f6ff7bb 100644 --- a/tests/tool-scope.test.ts +++ b/tests/tool-scope.test.ts @@ -96,7 +96,7 @@ mock.module(${JSON.stringify(webServerUrl)}, () => ({ startWebServer: async () => null, WebServer: class {}, })); -mock.module(${JSON.stringify(loggerUrl)}, () => ({ log: () => {}, serializeError: (error) => ({ error: error instanceof Error ? error.message : String(error) }) })); +mock.module(${JSON.stringify(loggerUrl)}, () => ({ log: () => {}, safeLog: () => {}, serializeError: (error) => ({ error: error instanceof Error ? error.message : String(error) }) })); mock.module(${JSON.stringify(languageUrl)}, () => ({ getLanguageName: () => "English" })); const { OpenCodeMemPlugin } = await import(${JSON.stringify(indexUrl)}); diff --git a/tests/turso-wal-index-recovery.test.ts b/tests/turso-wal-index-recovery.test.ts index 6a360210..5174a4dd 100644 --- a/tests/turso-wal-index-recovery.test.ts +++ b/tests/turso-wal-index-recovery.test.ts @@ -162,27 +162,30 @@ describe("turso WAL index recovery", () => { try { const db = await manager.getConnection(dbPath); values = (await db.all<{ v: string }>("SELECT v FROM t ORDER BY id")).map((row) => row.v); + + // Assert while the connection is still open: closeAll runs + // PRAGMA wal_checkpoint(TRUNCATE) and empties the -wal afterward. + expect(values).toEqual(["checkpointed", "wal-only"]); + expect(existsSync(walPath)).toBe(true); + expect(statSync(walPath).size).toBeGreaterThan(0); + // Same inode: the file was kept, not moved and recreated. + expect(statSync(walPath).ino).toBe(walInode); + + const [quarantined] = quarantineDirs(baseDir); + const quarantinedFiles = readdirSync( + join(baseDir, "recovery", quarantined as string) + ).sort(); + expect(quarantinedFiles).toContain("memories.db-tshm"); + expect(quarantinedFiles.every((name) => /-(shm|tshm)$/.test(name))).toBe(true); + // Engine evidence, not an assumption: the old -tshm is in quarantine *and* + // a fresh, non-empty index exists beside the WAL again, rebuilt on reopen. + // The seeded WAL has no torn trailing frame; a torn WAL is the + // "retry fails, original error surfaces, WAL untouched" case. + expect(existsSync(`${dbPath}-tshm`)).toBe(true); + expect(statSync(`${dbPath}-tshm`).size).toBeGreaterThan(0); } finally { await manager.closeAll(); } - - // The WAL was not discarded, and its committed row survived the recovery. - expect(values).toEqual(["checkpointed", "wal-only"]); - expect(existsSync(walPath)).toBe(true); - expect(statSync(walPath).size).toBeGreaterThan(0); - // Same inode: the file was kept, not moved and recreated. - expect(statSync(walPath).ino).toBe(walInode); - - const [quarantined] = quarantineDirs(baseDir); - const quarantinedFiles = readdirSync(join(baseDir, "recovery", quarantined as string)).sort(); - expect(quarantinedFiles).toContain("memories.db-tshm"); - expect(quarantinedFiles.every((name) => /-(shm|tshm)$/.test(name))).toBe(true); - // Engine evidence, not an assumption: the old -tshm is in quarantine *and* - // a fresh, non-empty index exists beside the WAL again, rebuilt on reopen. - // The seeded WAL has no torn trailing frame; a torn WAL is the - // "retry fails, original error surfaces, WAL untouched" case. - expect(existsSync(`${dbPath}-tshm`)).toBe(true); - expect(statSync(`${dbPath}-tshm`).size).toBeGreaterThan(0); } ); diff --git a/tests/user-profile-learning-error.test.ts b/tests/user-profile-learning-error.test.ts index 0fd01b66..bc3ab900 100644 --- a/tests/user-profile-learning-error.test.ts +++ b/tests/user-profile-learning-error.test.ts @@ -89,7 +89,7 @@ mock.module(${JSON.stringify(profileManagerUrl)}, () => ({ }, })); -mock.module(${JSON.stringify(loggerUrl)}, () => ({ log: () => {}, serializeError: (error) => ({ error: error instanceof Error ? error.message : String(error) }) })); +mock.module(${JSON.stringify(loggerUrl)}, () => ({ log: () => {}, safeLog: () => {}, serializeError: (error) => ({ error: error instanceof Error ? error.message : String(error) }) })); mock.module(${JSON.stringify(opencodeProviderLoaderUrl)}, () => ({ loadOpencodeProvider: async () => ({ diff --git a/tests/user-profile-learning-language.test.ts b/tests/user-profile-learning-language.test.ts index 1110c7f2..c080ae80 100644 --- a/tests/user-profile-learning-language.test.ts +++ b/tests/user-profile-learning-language.test.ts @@ -105,7 +105,7 @@ mock.module(${JSON.stringify(profileManagerUrl)}, () => ({ }, })); -mock.module(${JSON.stringify(loggerUrl)}, () => ({ log: () => {}, serializeError: (error) => ({ error: error instanceof Error ? error.message : String(error) }) })); +mock.module(${JSON.stringify(loggerUrl)}, () => ({ log: () => {}, safeLog: () => {}, serializeError: (error) => ({ error: error instanceof Error ? error.message : String(error) }) })); let capturedSystemPrompt = null; let capturedUserPrompt = null; diff --git a/tests/user-profile-learning-prompt-mode.test.ts b/tests/user-profile-learning-prompt-mode.test.ts index 3255c538..2402bcf3 100644 --- a/tests/user-profile-learning-prompt-mode.test.ts +++ b/tests/user-profile-learning-prompt-mode.test.ts @@ -113,7 +113,7 @@ mock.module(${JSON.stringify(profileManagerUrl)}, () => ({ }, })); -mock.module(${JSON.stringify(loggerUrl)}, () => ({ log: () => {}, serializeError: (error) => ({ error: error instanceof Error ? error.message : String(error) }) })); +mock.module(${JSON.stringify(loggerUrl)}, () => ({ log: () => {}, safeLog: () => {}, serializeError: (error) => ({ error: error instanceof Error ? error.message : String(error) }) })); let structuredSystemPrompt = null; let externalSystemPrompt = null;