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 }); } });