From e9250be4bbaa4ec2ada46309df994adbc2f07caf Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 9 Aug 2026 07:29:03 -0700 Subject: [PATCH] test(sessions): close SQLite fixtures before cleanup (#113074) --- .../ambient-transcript-watermark.test.ts | 12 ++++-- src/config/sessions/entry-freshness.test.ts | 4 ++ src/config/sessions/session-accessor.test.ts | 38 +++++++++++++++++-- .../session-registry-maintenance.test.ts | 4 ++ src/test-utils/fixture-suite.ts | 10 ++--- 5 files changed, 56 insertions(+), 12 deletions(-) diff --git a/src/config/sessions/ambient-transcript-watermark.test.ts b/src/config/sessions/ambient-transcript-watermark.test.ts index 85d0ee0c0612..d88d95493fbe 100644 --- a/src/config/sessions/ambient-transcript-watermark.test.ts +++ b/src/config/sessions/ambient-transcript-watermark.test.ts @@ -1,7 +1,9 @@ import fs from "node:fs"; -import os from "node:os"; import path from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { cleanupTempDirs, makeTempDir } from "../../../test/helpers/temp-dir.js"; +import { closeOpenClawAgentDatabasesForTest } from "../../state/openclaw-agent-db.js"; +import { closeOpenClawStateDatabaseForTest } from "../../state/openclaw-state-db.js"; import { readAmbientTranscriptWatermark, resolveAmbientTranscriptWatermarkKey, @@ -9,6 +11,8 @@ import { } from "./ambient-transcript-watermark.js"; import { loadSessionEntry, replaceSessionEntry } from "./session-accessor.js"; +const tempDirs: string[] = []; + describe("ambient transcript watermark", () => { let tempDir: string; let storePath: string; @@ -20,12 +24,14 @@ describe("ambient transcript watermark", () => { }); beforeEach(() => { - tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-ambient-watermark-")); + tempDir = makeTempDir(tempDirs, "openclaw-ambient-watermark-"); storePath = path.join(tempDir, "sessions.json"); }); afterEach(() => { - fs.rmSync(tempDir, { recursive: true, force: true }); + closeOpenClawAgentDatabasesForTest(); + closeOpenClawStateDatabaseForTest(); + cleanupTempDirs(tempDirs); }); it("stamps and resolves the watermark for the current session id only", async () => { diff --git a/src/config/sessions/entry-freshness.test.ts b/src/config/sessions/entry-freshness.test.ts index 2ac9694b7c17..b16bd89a0343 100644 --- a/src/config/sessions/entry-freshness.test.ts +++ b/src/config/sessions/entry-freshness.test.ts @@ -1,6 +1,8 @@ import path from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { cleanupTempDirs, makeTempDir } from "../../../test/helpers/temp-dir.js"; +import { closeOpenClawAgentDatabasesForTest } from "../../state/openclaw-agent-db.js"; +import { closeOpenClawStateDatabaseForTest } from "../../state/openclaw-state-db.js"; import { resolveSessionEntryResetFreshness } from "./entry-freshness.js"; import { appendTranscriptEvent, upsertSessionEntry } from "./session-accessor.js"; @@ -18,6 +20,8 @@ describe("resolveSessionEntryResetFreshness", () => { }); afterEach(() => { + closeOpenClawAgentDatabasesForTest(); + closeOpenClawStateDatabaseForTest(); cleanupTempDirs(tempDirs); }); diff --git a/src/config/sessions/session-accessor.test.ts b/src/config/sessions/session-accessor.test.ts index bd0a9211d2d0..73b93ac11d5f 100644 --- a/src/config/sessions/session-accessor.test.ts +++ b/src/config/sessions/session-accessor.test.ts @@ -1,10 +1,10 @@ import fs from "node:fs"; -import os from "node:os"; import path from "node:path"; import { DatabaseSync } from "node:sqlite"; import { expectDefined } from "@openclaw/normalization-core"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { withTestTimeout } from "../../../test/helpers/promise.js"; +import { cleanupTempDirs, makeTempDir } from "../../../test/helpers/temp-dir.js"; import type { MsgContext } from "../../auto-reply/templating.js"; import { onInternalSessionTranscriptUpdate, @@ -17,6 +17,10 @@ import { openOpenClawAgentDatabase, resolveOpenClawAgentSqlitePath, } from "../../state/openclaw-agent-db.js"; +import { + closeOpenClawStateDatabaseForTest, + isOpenClawStateDatabaseOpen, +} from "../../state/openclaw-state-db.js"; import { appendSqliteTrajectoryRuntimeEvents } from "../../trajectory/runtime-store.sqlite.js"; import type { TrajectoryEvent } from "../../trajectory/types.js"; import { @@ -88,6 +92,7 @@ import { withOwnedSessionTranscriptWrites } from "./transcript-write-context.js" import type { InternalSessionEntry, SessionEntry } from "./types.js"; const cleanupArchivedSessionTranscriptsMock = vi.hoisted(() => vi.fn(async () => {})); +const tempDirs: string[] = []; vi.mock("../../gateway/session-archive.runtime.js", async (importOriginal) => { const actual = await importOriginal(); @@ -127,6 +132,8 @@ describe("session accessor seam", () => { let tempDir: string; let storePath: string; let transcriptPath: string; + let cleanupProbeDatabasePath = ""; + let cleanupProbeRoot = ""; function loadMainInitializationSnapshot(sessionKey: string) { return loadReplySessionInitializationSnapshot({ agentId: "main", sessionKey, storePath }); @@ -134,13 +141,38 @@ describe("session accessor seam", () => { beforeEach(() => { cleanupArchivedSessionTranscriptsMock.mockReset(); - tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-session-accessor-")); + tempDir = makeTempDir(tempDirs, "openclaw-session-accessor-"); storePath = path.join(tempDir, "sessions.json"); transcriptPath = path.join(tempDir, "session.jsonl"); }); afterEach(() => { - fs.rmSync(tempDir, { recursive: true, force: true }); + closeOpenClawAgentDatabasesForTest(); + closeOpenClawStateDatabaseForTest(); + cleanupTempDirs(tempDirs); + }); + + describe.sequential("session database teardown boundary", () => { + it("opens cached agent and shared-state handles", async () => { + await replaceSessionEntry( + { agentId: "main", sessionKey: "agent:main:cleanup-probe", storePath }, + { sessionId: "cleanup-probe", updatedAt: 1 }, + ); + cleanupProbeDatabasePath = expectDefined( + resolveSqliteTargetFromSessionStorePath(storePath, { agentId: "main" }).path, + "cleanup probe database path", + ); + cleanupProbeRoot = tempDir; + + expect(isOpenClawAgentDatabaseOpen(cleanupProbeDatabasePath)).toBe(true); + expect(isOpenClawStateDatabaseOpen()).toBe(true); + }); + + it("releases both cache owners before the next test", () => { + expect(isOpenClawAgentDatabaseOpen(cleanupProbeDatabasePath)).toBe(false); + expect(isOpenClawStateDatabaseOpen()).toBe(false); + expect(fs.existsSync(cleanupProbeRoot)).toBe(false); + }); }); it("exposes the canonical SQLite session lifecycle owners", () => { diff --git a/src/config/sessions/session-registry-maintenance.test.ts b/src/config/sessions/session-registry-maintenance.test.ts index be9410851ec0..dfd29bde954a 100644 --- a/src/config/sessions/session-registry-maintenance.test.ts +++ b/src/config/sessions/session-registry-maintenance.test.ts @@ -3,6 +3,8 @@ import fs from "node:fs/promises"; import path from "node:path"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { beginSessionWorkAdmission } from "../../sessions/session-lifecycle-admission.js"; +import { closeOpenClawAgentDatabasesForTest } from "../../state/openclaw-agent-db.js"; +import { closeOpenClawStateDatabaseForTest } from "../../state/openclaw-state-db.js"; import { createFixtureSuite } from "../../test-utils/fixture-suite.js"; import { readSessionArchiveContentSync } from "./archive-compression.js"; import { isRetainedSessionTranscriptArchiveName } from "./artifacts.js"; @@ -24,6 +26,8 @@ beforeAll(async () => { }); afterAll(async () => { + closeOpenClawAgentDatabasesForTest(); + closeOpenClawStateDatabaseForTest(); await fixtureSuite.cleanup(); }); diff --git a/src/test-utils/fixture-suite.ts b/src/test-utils/fixture-suite.ts index 6cf5544ba2c4..f6b4cba7520b 100644 --- a/src/test-utils/fixture-suite.ts +++ b/src/test-utils/fixture-suite.ts @@ -1,25 +1,23 @@ // Loads fixture suites from disk for parametrized tests. import fs from "node:fs/promises"; -import os from "node:os"; import path from "node:path"; +import { cleanupTempDirs, makeTempDir } from "../../test/helpers/temp-dir.js"; /** Creates a temp fixture root with deterministic per-case subdirectories. */ export function createFixtureSuite(rootPrefix: string) { let fixtureRoot = ""; let fixtureCount = 0; + const fixtureRoots = new Set(); return { async setup(): Promise { - // Canonicalize: macOS tmpdir sits behind a symlink (/var -> /private/var) - // and production realpaths state/session paths, so symlinked roots break - // path-equality assertions. - fixtureRoot = await fs.realpath(await fs.mkdtemp(path.join(os.tmpdir(), rootPrefix))); + fixtureRoot = makeTempDir(fixtureRoots, rootPrefix); }, async cleanup(): Promise { if (!fixtureRoot) { return; } - await fs.rm(fixtureRoot, { recursive: true, force: true }); + cleanupTempDirs(fixtureRoots); fixtureRoot = ""; }, async createCaseDir(prefix: string): Promise {