From ff83e3efe811d561aee0e8d7cd38047294b1cb58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yi=C4=9Fit=20ERDO=C4=9EAN?= Date: Fri, 21 Aug 2026 17:22:57 +0300 Subject: [PATCH] test(extensions): close cached agent databases before removing fixture state dirs (#126352) Doctor migrations and auth-profile writes open per-agent and shared state databases under the fixture's temporary directory. Clearing the plugin state store or the runtime auth snapshots does not release those handles, so Windows fails the directory removal with EBUSY while Linux unlinks the open files and stays green. Close the cached databases before each removal, matching the ordering the zalouser and zalo fixtures already use. --- extensions/acpx/doctor-contract-api.test.ts | 1 + .../active-memory/doctor-contract-api.test.ts | 1 + .../alibaba/video-generation-provider.test.ts | 5 ++ extensions/codex/doctor-contract-api.test.ts | 61 +++++++++++-------- .../device-pair/doctor-contract-api.test.ts | 1 + .../memory-core/doctor-contract-api.test.ts | 1 + .../msteams/doctor-contract-api.test.ts | 1 + .../openai/video-generation-provider.test.ts | 5 ++ 8 files changed, 49 insertions(+), 27 deletions(-) diff --git a/extensions/acpx/doctor-contract-api.test.ts b/extensions/acpx/doctor-contract-api.test.ts index bfa404bddad5..ad9263fe7617 100644 --- a/extensions/acpx/doctor-contract-api.test.ts +++ b/extensions/acpx/doctor-contract-api.test.ts @@ -105,6 +105,7 @@ describe("acpx doctor state migration", () => { }); afterEach(async () => { + resetPluginStateStoreForTests(); await fs.rm(stateDir, { recursive: true, force: true }); }); diff --git a/extensions/active-memory/doctor-contract-api.test.ts b/extensions/active-memory/doctor-contract-api.test.ts index b0716b7ef474..0fc3e640bde1 100644 --- a/extensions/active-memory/doctor-contract-api.test.ts +++ b/extensions/active-memory/doctor-contract-api.test.ts @@ -65,6 +65,7 @@ describe("active-memory doctor state migration", () => { afterEach(async () => { vi.useRealTimers(); + resetPluginStateStoreForTests(); await fs.rm(stateDir, { recursive: true, force: true }); }); diff --git a/extensions/alibaba/video-generation-provider.test.ts b/extensions/alibaba/video-generation-provider.test.ts index 3a0dfbbba0b0..56e44a4717d4 100644 --- a/extensions/alibaba/video-generation-provider.test.ts +++ b/extensions/alibaba/video-generation-provider.test.ts @@ -22,6 +22,7 @@ import { mockSuccessfulDashscopeVideoTask, } from "openclaw/plugin-sdk/provider-test-contracts"; // Alibaba tests cover video generation provider plugin behavior. +import { closeOpenClawAgentDatabasesForTest } from "openclaw/plugin-sdk/sqlite-runtime-testing"; import { createRequireRecord } from "openclaw/plugin-sdk/test-fixtures"; import { DASHSCOPE_WAN_VIDEO_MODELS, @@ -276,6 +277,10 @@ describe("alibaba video generation provider", () => { expect(alibabaVideoGenerationProvider.isConfigured?.({ cfg: {}, agentDir })).toBe(expected); } finally { clearRuntimeAuthProfileStoreSnapshots(); + // Saving the profile store opens the per-agent database under the temporary agent + // dir, and clearing the snapshots does not release it, so Windows fails the removal + // with EBUSY unless the cached handles are closed first. + closeOpenClawAgentDatabasesForTest(); await fs.rm(agentDir, { force: true, recursive: true }); } }); diff --git a/extensions/codex/doctor-contract-api.test.ts b/extensions/codex/doctor-contract-api.test.ts index f022791762ba..e1411564c755 100644 --- a/extensions/codex/doctor-contract-api.test.ts +++ b/extensions/codex/doctor-contract-api.test.ts @@ -11,6 +11,7 @@ import type { PluginDoctorStateMigrationContext, } from "openclaw/plugin-sdk/runtime-doctor-migrations"; import { getSessionEntry, upsertSessionEntry } from "openclaw/plugin-sdk/session-store-runtime"; +import { closeOpenClawAgentDatabasesForTest } from "openclaw/plugin-sdk/sqlite-runtime-testing"; import { afterEach, describe, expect, it } from "vitest"; import { legacyConfigRules, @@ -59,6 +60,16 @@ function openBindingStore(env: NodeJS.ProcessEnv) { }); } +async function removeCodexDoctorFixture(stateDir: string): Promise { + // Doctor migrations open per-agent databases and leave the shared state database open under + // the temporary state dir; both must be released before removal or Windows keeps the files + // locked and the removal fails with EBUSY. Agent close first: it releases leases through + // shared state, so the reverse order can reopen it. + closeOpenClawAgentDatabasesForTest(); + resetPluginStateStoreForTests(); + await fs.rm(stateDir, { recursive: true, force: true }); +} + async function createBindingMigrationFixture(options: { binding?: Record; legacySharedRoot?: boolean; @@ -272,7 +283,7 @@ describe("codex doctor contract", () => { }), ).toMatchObject({ agentHarnessId: "codex" }); } finally { - await fs.rm(fixture.stateDir, { recursive: true, force: true }); + await removeCodexDoctorFixture(fixture.stateDir); } }); @@ -357,7 +368,7 @@ describe("codex doctor contract", () => { fs.readFile(fixture.storePath, "utf8").then(JSON.parse), ).resolves.not.toHaveProperty("agent:main:session-1.agentHarnessId"); - await fs.rm(fixture.stateDir, { recursive: true, force: true }); + await removeCodexDoctorFixture(fixture.stateDir); }); it.each([ @@ -396,7 +407,7 @@ describe("codex doctor contract", () => { await expect(fs.access(`${fixture.sidecarPath}.migrated`)).rejects.toThrow(); await expect(openBindingStore(fixture.env).entries()).resolves.toEqual([]); - await fs.rm(fixture.stateDir, { recursive: true, force: true }); + await removeCodexDoctorFixture(fixture.stateDir); }); it("migrates a shared-root binding to the configured system agent", async () => { @@ -448,7 +459,7 @@ describe("codex doctor contract", () => { ).toMatchObject({ agentHarnessId: "codex" }); await expect(fs.access(`${fixture.sidecarPath}.migrated`)).resolves.toBeUndefined(); - await fs.rm(fixture.stateDir, { recursive: true, force: true }); + await removeCodexDoctorFixture(fixture.stateDir); }); it("keeps an agent-scoped shared-root binding with its explicit owner", async () => { @@ -492,7 +503,7 @@ describe("codex doctor contract", () => { ).resolves.toMatchObject({ sessionId: "explicit-ops-owner" }); await expect(fs.access(`${fixture.sidecarPath}.migrated`)).resolves.toBeUndefined(); - await fs.rm(fixture.stateDir, { recursive: true, force: true }); + await removeCodexDoctorFixture(fixture.stateDir); }); it("bounds oversized legacy fingerprints before plugin-state import", async () => { @@ -560,7 +571,7 @@ describe("codex doctor contract", () => { warnings: [], }); - await fs.rm(fixture.stateDir, { recursive: true, force: true }); + await removeCodexDoctorFixture(fixture.stateDir); }); it("normalizes a partial raw conversation import before copying the session row", async () => { @@ -638,7 +649,7 @@ describe("codex doctor contract", () => { warnings: [], }); - await fs.rm(fixture.stateDir, { recursive: true, force: true }); + await removeCodexDoctorFixture(fixture.stateDir); }); it("normalizes retained raw conversation and session rows before comparison", async () => { @@ -710,7 +721,7 @@ describe("codex doctor contract", () => { warnings: [], }); - await fs.rm(fixture.stateDir, { recursive: true, force: true }); + await removeCodexDoctorFixture(fixture.stateDir); }); it("rejects an explicit session file locator outside the session directory", async () => { @@ -743,7 +754,7 @@ describe("codex doctor contract", () => { ).toBeUndefined(); await expect(openBindingStore(fixture.env).entries()).resolves.toEqual([]); - await fs.rm(fixture.stateDir, { recursive: true, force: true }); + await removeCodexDoctorFixture(fixture.stateDir); }); it("deduplicates session-store aliases before classifying binding ownership", async () => { @@ -795,7 +806,7 @@ describe("codex doctor contract", () => { expect(configuredIndex["agent:main:aliased-store"]).not.toHaveProperty("agentHarnessId"); expect(targetIndex["agent:main:aliased-store"]).not.toHaveProperty("agentHarnessId"); - await fs.rm(fixture.stateDir, { recursive: true, force: true }); + await removeCodexDoctorFixture(fixture.stateDir); }); it("resolves relative session files from a symlinked store path", async () => { @@ -847,7 +858,7 @@ describe("codex doctor contract", () => { `${sessionKey}.agentHarnessId`, ); - await fs.rm(fixture.stateDir, { recursive: true, force: true }); + await removeCodexDoctorFixture(fixture.stateDir); }); it.each([ @@ -929,7 +940,7 @@ describe("codex doctor contract", () => { retired: true, }); - await fs.rm(fixture.stateDir, { recursive: true, force: true }); + await removeCodexDoctorFixture(fixture.stateDir); }, ); @@ -986,7 +997,7 @@ describe("codex doctor contract", () => { retired: true, }); - await fs.rm(fixture.stateDir, { recursive: true, force: true }); + await removeCodexDoctorFixture(fixture.stateDir); }); it("does not resurrect a retired session generation from its legacy sidecar", async () => { @@ -1041,7 +1052,7 @@ describe("codex doctor contract", () => { fs.readFile(path.join(fixture.sessionsDir, "sessions.json"), "utf8").then(JSON.parse), ).resolves.not.toHaveProperty(`${sessionKey}.agentHarnessId`); - await fs.rm(fixture.stateDir, { recursive: true, force: true }); + await removeCodexDoctorFixture(fixture.stateDir); }); it.each(["active", "cleared"] as const)( @@ -1081,7 +1092,7 @@ describe("codex doctor contract", () => { warnings: [], }); - await fs.rm(fixture.stateDir, { recursive: true, force: true }); + await removeCodexDoctorFixture(fixture.stateDir); }, ); @@ -1106,7 +1117,7 @@ describe("codex doctor contract", () => { await expect(fs.access(fixture.sidecarPath)).rejects.toThrow(); await expect(fs.access(`${fixture.sidecarPath}.migrated`)).resolves.toBeUndefined(); - await fs.rm(fixture.stateDir, { recursive: true, force: true }); + await removeCodexDoctorFixture(fixture.stateDir); }); it("retains a zero-owner sidecar when canonical plugin state is malformed", async () => { @@ -1135,7 +1146,7 @@ describe("codex doctor contract", () => { await expect(fs.access(fixture.sidecarPath)).resolves.toBeUndefined(); await expect(store.lookup(bindingKey)).resolves.toEqual(malformed); - await fs.rm(fixture.stateDir, { recursive: true, force: true }); + await removeCodexDoctorFixture(fixture.stateDir); }); it("retains mixed Codex and foreign ambiguous binding owners", async () => { @@ -1164,7 +1175,7 @@ describe("codex doctor contract", () => { await expect(fs.access(fixture.sidecarPath)).resolves.toBeUndefined(); await expect(openBindingStore(fixture.env).entries()).resolves.toEqual([]); - await fs.rm(fixture.stateDir, { recursive: true, force: true }); + await removeCodexDoctorFixture(fixture.stateDir); }); it("retains a sidecar owned by a foreign harness without importing plugin state", async () => { @@ -1188,7 +1199,7 @@ describe("codex doctor contract", () => { await expect(fs.access(fixture.sidecarPath)).resolves.toBeUndefined(); await expect(openBindingStore(fixture.env).entries()).resolves.toEqual([]); - await fs.rm(fixture.stateDir, { recursive: true, force: true }); + await removeCodexDoctorFixture(fixture.stateDir); }); it.each([ @@ -1230,10 +1241,8 @@ describe("codex doctor contract", () => { await expect(fs.access(`${fixture.sidecarPath}.migrated`)).rejects.toThrow(); await expect(openBindingStore(fixture.env).entries()).resolves.toEqual([]); - await Promise.all([ - fs.rm(fixture.stateDir, { recursive: true, force: true }), - fs.rm(externalDir, { recursive: true, force: true }), - ]); + await removeCodexDoctorFixture(fixture.stateDir); + await fs.rm(externalDir, { recursive: true, force: true }); }); it("does not scan above stateDir or follow escaped external store locators", async () => { @@ -1284,10 +1293,8 @@ describe("codex doctor contract", () => { await expect(migration.detectLegacyState(params)).resolves.toBeNull(); - await Promise.all([ - fs.rm(outerDir, { recursive: true, force: true }), - fs.rm(outsideDir, { recursive: true, force: true }), - ]); + await removeCodexDoctorFixture(outerDir); + await fs.rm(outsideDir, { recursive: true, force: true }); }); it("renames old approval-routed destructive plugin policy values", () => { diff --git a/extensions/device-pair/doctor-contract-api.test.ts b/extensions/device-pair/doctor-contract-api.test.ts index 2b3a92dccfe0..919f5c9b0e5f 100644 --- a/extensions/device-pair/doctor-contract-api.test.ts +++ b/extensions/device-pair/doctor-contract-api.test.ts @@ -43,6 +43,7 @@ describe("device-pair doctor notify migration", () => { }); afterEach(async () => { + resetPluginStateStoreForTests(); await fs.rm(stateDir, { recursive: true, force: true }); }); diff --git a/extensions/memory-core/doctor-contract-api.test.ts b/extensions/memory-core/doctor-contract-api.test.ts index 90d08a55a737..38e45c064bf6 100644 --- a/extensions/memory-core/doctor-contract-api.test.ts +++ b/extensions/memory-core/doctor-contract-api.test.ts @@ -488,6 +488,7 @@ describe("memory-core doctor dreaming migration", () => { afterEach(async () => { resetMemoryCoreDreamingStateForTests(); + resetPluginStateStoreForTests(); await fs.rm(rootDir, { recursive: true, force: true }); }); diff --git a/extensions/msteams/doctor-contract-api.test.ts b/extensions/msteams/doctor-contract-api.test.ts index 0c66fde14c69..9d19d44418eb 100644 --- a/extensions/msteams/doctor-contract-api.test.ts +++ b/extensions/msteams/doctor-contract-api.test.ts @@ -83,6 +83,7 @@ describe("msteams doctor state migration", () => { }); afterEach(async () => { + resetPluginStateStoreForTests(); await fs.rm(stateDir, { recursive: true, force: true }); }); diff --git a/extensions/openai/video-generation-provider.test.ts b/extensions/openai/video-generation-provider.test.ts index d50989b5cbcc..a09f37cb051b 100644 --- a/extensions/openai/video-generation-provider.test.ts +++ b/extensions/openai/video-generation-provider.test.ts @@ -13,6 +13,7 @@ import { installProviderHttpMockCleanup, } from "openclaw/plugin-sdk/provider-http-test-mocks"; import { expectExplicitVideoGenerationCapabilities } from "openclaw/plugin-sdk/provider-test-contracts"; +import { closeOpenClawAgentDatabasesForTest } from "openclaw/plugin-sdk/sqlite-runtime-testing"; import { beforeAll, describe, expect, it, vi } from "vitest"; const { @@ -171,6 +172,10 @@ describe("openai video generation provider", () => { } else { process.env.OPENAI_API_KEY = previousOpenAIKey; } + // Saving the profile store opens the per-agent database under the temporary agent + // dir, and clearing the snapshots does not release it, so Windows fails the removal + // with EBUSY unless the cached handles are closed first. + closeOpenClawAgentDatabasesForTest(); fs.rmSync(agentDir, { recursive: true, force: true }); } });