From 3b1c8f229d9c8f2ec8507bb475a5f7e32e6916dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yi=C4=9Fit=20ERDO=C4=9EAN?= Date: Sun, 23 Aug 2026 23:55:24 +0300 Subject: [PATCH] test(memory-core): release cached databases before removing fixture state dirs (#128310) * test(memory-core): release cached state handles before removing fixture dirs Six memory-core fixtures remove their temporary state directory while the per-agent or shared SQLite handles opened underneath it are still cached, so Windows fails the removal with EBUSY while Linux unlinks the open files and stays green. Release both handles before each removal, in the ordering merged for this class: the agent close releases its leases through shared state and reopens it, so the shared store has to be released after it. * test(memory-core): release cached handles in the shared fixture harness and cli teardown --- extensions/memory-core/src/cli.test.ts | 5 +++++ .../memory-core/src/dreaming-startup-cleanup.test.ts | 2 ++ .../memory/manager-sync-ops.startup-catchup.test.ts | 5 +++++ .../src/memory/manager.fts-only-reindex.test.ts | 6 ++++++ .../src/memory/manager.reindex-recovery.test.ts | 6 ++++++ .../memory/manager.self-heal-missing-identity.test.ts | 6 ++++++ .../src/memory/manager.watcher-config.test.ts | 6 ++++++ extensions/memory-core/src/test-helpers.ts | 10 +++++++++- 8 files changed, 45 insertions(+), 1 deletion(-) diff --git a/extensions/memory-core/src/cli.test.ts b/extensions/memory-core/src/cli.test.ts index ed18abba760a..97351ae35f2a 100644 --- a/extensions/memory-core/src/cli.test.ts +++ b/extensions/memory-core/src/cli.test.ts @@ -5,6 +5,7 @@ import path from "node:path"; import { Command } from "commander"; import type { OpenClawConfig } from "openclaw/plugin-sdk/memory-core-host-engine-foundation"; import { resolveSessionTranscriptsDirForAgent as resolveTestSessionTranscriptsDirForAgent } from "openclaw/plugin-sdk/memory-core-host-runtime-core"; +import { resetPluginStateStoreForTests } from "openclaw/plugin-sdk/plugin-state-test-runtime"; import { upsertSessionEntry } from "openclaw/plugin-sdk/session-store-runtime"; import { appendSessionTranscriptMessageByIdentity } from "openclaw/plugin-sdk/session-transcript-runtime"; import { resolveOpenClawAgentSqlitePath } from "openclaw/plugin-sdk/sqlite-runtime"; @@ -161,6 +162,10 @@ afterAll(async () => { if (!fixtureRoot) { return; } + // The agent close releases its leases through shared state and reopens it, so the + // shared handle is released second; otherwise Windows fails the removal with EBUSY. + closeOpenClawAgentDatabasesForTest(); + resetPluginStateStoreForTests(); await fs.rm(fixtureRoot, { recursive: true, force: true }); resetMemoryCoreDreamingStateForTests(); }); diff --git a/extensions/memory-core/src/dreaming-startup-cleanup.test.ts b/extensions/memory-core/src/dreaming-startup-cleanup.test.ts index f62a6bc613ff..07e1332eef4d 100644 --- a/extensions/memory-core/src/dreaming-startup-cleanup.test.ts +++ b/extensions/memory-core/src/dreaming-startup-cleanup.test.ts @@ -3,6 +3,7 @@ import os from "node:os"; import path from "node:path"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry"; +import { resetPluginStateStoreForTests } from "openclaw/plugin-sdk/plugin-state-test-runtime"; import { getSessionEntry, upsertSessionEntry } from "openclaw/plugin-sdk/session-store-runtime"; import { appendSqliteSessionTranscriptEventForTest, @@ -30,6 +31,7 @@ afterEach(async () => { vi.useRealTimers(); vi.unstubAllEnvs(); vi.restoreAllMocks(); + resetPluginStateStoreForTests(); await fs.rm(stateDir, { recursive: true, force: true }); }); diff --git a/extensions/memory-core/src/memory/manager-sync-ops.startup-catchup.test.ts b/extensions/memory-core/src/memory/manager-sync-ops.startup-catchup.test.ts index 6fa4607b57a4..cb4c7bfdbeb4 100644 --- a/extensions/memory-core/src/memory/manager-sync-ops.startup-catchup.test.ts +++ b/extensions/memory-core/src/memory/manager-sync-ops.startup-catchup.test.ts @@ -19,6 +19,7 @@ import { type MemorySyncParams, type MemorySyncProgressUpdate, } from "openclaw/plugin-sdk/memory-core-host-engine-storage"; +import { resetPluginStateStoreForTests } from "openclaw/plugin-sdk/plugin-state-test-runtime"; import { clearConfigCache, clearRuntimeConfigSnapshot, @@ -413,6 +414,10 @@ describe("session startup catch-up", () => { } startupHarnessDatabases.clear(); closeOpenClawAgentDatabasesForTest(); + // Closing the agent databases releases their leases through shared state, which + // reopens it, so the shared handle has to be released after that and before the + // removal or Windows fails the unlink with EBUSY. + resetPluginStateStoreForTests(); await fs.rm(stateDir, { recursive: true, force: true }); }); diff --git a/extensions/memory-core/src/memory/manager.fts-only-reindex.test.ts b/extensions/memory-core/src/memory/manager.fts-only-reindex.test.ts index 4b0321cb7246..c1da516b39f2 100644 --- a/extensions/memory-core/src/memory/manager.fts-only-reindex.test.ts +++ b/extensions/memory-core/src/memory/manager.fts-only-reindex.test.ts @@ -4,7 +4,9 @@ import os from "node:os"; import path from "node:path"; import { DatabaseSync } from "node:sqlite"; import type { OpenClawConfig } from "openclaw/plugin-sdk/memory-core-host-engine-foundation"; +import { resetPluginStateStoreForTests } from "openclaw/plugin-sdk/plugin-state-test-runtime"; import { resolveOpenClawAgentSqlitePath } from "openclaw/plugin-sdk/sqlite-runtime"; +import { closeOpenClawAgentDatabasesForTest } from "openclaw/plugin-sdk/sqlite-runtime-testing"; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { closeAllMemorySearchManagers, getMemorySearchManager } from "./index.js"; import type { MemoryIndexMeta } from "./manager-reindex-state.js"; @@ -107,6 +109,10 @@ describe("memory manager FTS-only reindex", () => { afterAll(async () => { await closeAllMemorySearchManagers(); + // The agent close releases its leases through shared state and reopens it, so the + // shared handle is released second; otherwise Windows fails the removal with EBUSY. + closeOpenClawAgentDatabasesForTest(); + resetPluginStateStoreForTests(); if (fixtureRoot) { await fs.rm(fixtureRoot, { recursive: true, force: true }); } diff --git a/extensions/memory-core/src/memory/manager.reindex-recovery.test.ts b/extensions/memory-core/src/memory/manager.reindex-recovery.test.ts index 99472a5eda27..326c461a2fb6 100644 --- a/extensions/memory-core/src/memory/manager.reindex-recovery.test.ts +++ b/extensions/memory-core/src/memory/manager.reindex-recovery.test.ts @@ -4,7 +4,9 @@ import os from "node:os"; import path from "node:path"; import { DatabaseSync } from "node:sqlite"; import type { OpenClawConfig } from "openclaw/plugin-sdk/memory-core-host-engine-foundation"; +import { resetPluginStateStoreForTests } from "openclaw/plugin-sdk/plugin-state-test-runtime"; import { resolveOpenClawAgentSqlitePath } from "openclaw/plugin-sdk/sqlite-runtime"; +import { closeOpenClawAgentDatabasesForTest } from "openclaw/plugin-sdk/sqlite-runtime-testing"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { resetEmbeddingMocks } from "./embedding.test-mocks.js"; import { acquireMemoryReindexLock } from "./manager-reindex-lock.js"; @@ -52,6 +54,10 @@ describe("memory manager reindex recovery", () => { } const { closeAllMemorySearchManagers } = await import("./index.js"); await closeAllMemorySearchManagers(); + // The agent close releases its leases through shared state and reopens it, so the + // shared handle is released second; otherwise Windows fails the removal with EBUSY. + closeOpenClawAgentDatabasesForTest(); + resetPluginStateStoreForTests(); await fs.rm(fixtureRoot, { recursive: true, force: true }); }); diff --git a/extensions/memory-core/src/memory/manager.self-heal-missing-identity.test.ts b/extensions/memory-core/src/memory/manager.self-heal-missing-identity.test.ts index 7979ac373a66..ae84a0a2269d 100644 --- a/extensions/memory-core/src/memory/manager.self-heal-missing-identity.test.ts +++ b/extensions/memory-core/src/memory/manager.self-heal-missing-identity.test.ts @@ -3,7 +3,9 @@ import os from "node:os"; import path from "node:path"; import { DatabaseSync } from "node:sqlite"; import type { OpenClawConfig } from "openclaw/plugin-sdk/memory-core-host-engine-foundation"; +import { resetPluginStateStoreForTests } from "openclaw/plugin-sdk/plugin-state-test-runtime"; import { resolveOpenClawAgentSqlitePath } from "openclaw/plugin-sdk/sqlite-runtime"; +import { closeOpenClawAgentDatabasesForTest } from "openclaw/plugin-sdk/sqlite-runtime-testing"; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { closeAllMemorySearchManagers, getMemorySearchManager } from "./index.js"; import type { MemoryIndexManager } from "./manager.js"; @@ -78,6 +80,10 @@ describe("memory manager self-heal missing identity with FTS-only chunks", () => afterAll(async () => { await closeAllMemorySearchManagers(); + // The agent close releases its leases through shared state and reopens it, so the + // shared handle is released second; otherwise Windows fails the removal with EBUSY. + closeOpenClawAgentDatabasesForTest(); + resetPluginStateStoreForTests(); if (fixtureRoot) { await fs.rm(fixtureRoot, { recursive: true, force: true }); } diff --git a/extensions/memory-core/src/memory/manager.watcher-config.test.ts b/extensions/memory-core/src/memory/manager.watcher-config.test.ts index e3dc464f1caa..3f2355f368af 100644 --- a/extensions/memory-core/src/memory/manager.watcher-config.test.ts +++ b/extensions/memory-core/src/memory/manager.watcher-config.test.ts @@ -7,6 +7,8 @@ import type { MemorySearchConfig, OpenClawConfig, } from "openclaw/plugin-sdk/memory-core-host-engine-foundation"; +import { resetPluginStateStoreForTests } from "openclaw/plugin-sdk/plugin-state-test-runtime"; +import { closeOpenClawAgentDatabasesForTest } from "openclaw/plugin-sdk/sqlite-runtime-testing"; import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; type WatchIgnoredFn = (watchPath: string, stats?: { isDirectory?: () => boolean }) => boolean; @@ -208,6 +210,10 @@ describe("memory watcher config", () => { await closeAllMemorySearchManagers(); clearRegistry(); restoreWatcherStateDir(); + // The agent close releases its leases through shared state and reopens it, so the + // shared handle is released second; otherwise Windows fails the removal with EBUSY. + closeOpenClawAgentDatabasesForTest(); + resetPluginStateStoreForTests(); if (workspaceDir) { await fs.rm(workspaceDir, { recursive: true, force: true }); workspaceDir = ""; diff --git a/extensions/memory-core/src/test-helpers.ts b/extensions/memory-core/src/test-helpers.ts index 27ba97ac8b93..49b553466919 100644 --- a/extensions/memory-core/src/test-helpers.ts +++ b/extensions/memory-core/src/test-helpers.ts @@ -2,7 +2,11 @@ import fs from "node:fs/promises"; import path from "node:path"; import type { OpenKeyedStoreOptions } from "openclaw/plugin-sdk/plugin-state-runtime"; -import { createPluginStateKeyedStoreForTests } from "openclaw/plugin-sdk/plugin-state-test-runtime"; +import { + createPluginStateKeyedStoreForTests, + resetPluginStateStoreForTests, +} from "openclaw/plugin-sdk/plugin-state-test-runtime"; +import { closeOpenClawAgentDatabasesForTest } from "openclaw/plugin-sdk/sqlite-runtime-testing"; import { asOptionalRecord } from "openclaw/plugin-sdk/string-coerce-runtime"; import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path"; import { afterAll, beforeAll } from "vitest"; @@ -210,6 +214,10 @@ export function createMemoryCoreTestHarness() { if (!fixtureRoot) { return; } + // The agent close releases its leases through shared state and reopens it, so the + // shared handle is released second; otherwise Windows fails the removal with EBUSY. + closeOpenClawAgentDatabasesForTest(); + resetPluginStateStoreForTests(); await fs.rm(fixtureRoot, { recursive: true, force: true }); resetMemoryCoreDreamingStateForTests(); });