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.
This commit is contained in:
Yiğit ERDOĞAN
2026-08-21 17:22:57 +03:00
committed by GitHub
parent f94f945314
commit ff83e3efe8
8 changed files with 49 additions and 27 deletions
@@ -105,6 +105,7 @@ describe("acpx doctor state migration", () => {
});
afterEach(async () => {
resetPluginStateStoreForTests();
await fs.rm(stateDir, { recursive: true, force: true });
});
@@ -65,6 +65,7 @@ describe("active-memory doctor state migration", () => {
afterEach(async () => {
vi.useRealTimers();
resetPluginStateStoreForTests();
await fs.rm(stateDir, { recursive: true, force: true });
});
@@ -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 });
}
});
+34 -27
View File
@@ -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<void> {
// 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<string, unknown>;
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", () => {
@@ -43,6 +43,7 @@ describe("device-pair doctor notify migration", () => {
});
afterEach(async () => {
resetPluginStateStoreForTests();
await fs.rm(stateDir, { recursive: true, force: true });
});
@@ -488,6 +488,7 @@ describe("memory-core doctor dreaming migration", () => {
afterEach(async () => {
resetMemoryCoreDreamingStateForTests();
resetPluginStateStoreForTests();
await fs.rm(rootDir, { recursive: true, force: true });
});
@@ -83,6 +83,7 @@ describe("msteams doctor state migration", () => {
});
afterEach(async () => {
resetPluginStateStoreForTests();
await fs.rm(stateDir, { recursive: true, force: true });
});
@@ -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 });
}
});