diff --git a/packages/opencode/src/storage/db.ts b/packages/opencode/src/storage/db.ts index 6d7bfd72810..f29aac18d16 100644 --- a/packages/opencode/src/storage/db.ts +++ b/packages/opencode/src/storage/db.ts @@ -33,6 +33,10 @@ export namespace Database { type Journal = { sql: string; timestamp: number }[] + const state = { + sqlite: undefined as BunDatabase | undefined, + } + function time(tag: string) { const match = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/.exec(tag) if (!match) return 0 @@ -69,6 +73,7 @@ export namespace Database { log.info("opening database", { path: path.join(Global.Path.data, "opencode.db") }) const sqlite = new BunDatabase(path.join(Global.Path.data, "opencode.db"), { create: true }) + state.sqlite = sqlite sqlite.run("PRAGMA journal_mode = WAL") sqlite.run("PRAGMA synchronous = NORMAL") @@ -95,6 +100,14 @@ export namespace Database { return db }) + export function close() { + const sqlite = state.sqlite + if (!sqlite) return + sqlite.close() + state.sqlite = undefined + Client.reset() + } + export type TxOrDb = Transaction | Client const ctx = Context.create<{ diff --git a/packages/opencode/test/preload.ts b/packages/opencode/test/preload.ts index a6d96cf17bd..41028633e83 100644 --- a/packages/opencode/test/preload.ts +++ b/packages/opencode/test/preload.ts @@ -3,14 +3,29 @@ import os from "os" import path from "path" import fs from "fs/promises" -import fsSync from "fs" import { afterAll } from "bun:test" // Set XDG env vars FIRST, before any src/ imports const dir = path.join(os.tmpdir(), "opencode-test-data-" + process.pid) await fs.mkdir(dir, { recursive: true }) -afterAll(() => { - fsSync.rmSync(dir, { recursive: true, force: true, maxRetries: 3, retryDelay: 500 }) +afterAll(async () => { + const { Database } = await import("../src/storage/db") + Database.close() + const busy = (error: unknown) => + typeof error === "object" && error !== null && "code" in error && error.code === "EBUSY" + const rm = async (left: number): Promise => { + Bun.gc(true) + await Bun.sleep(100) + return fs.rm(dir, { recursive: true, force: true }).catch((error) => { + if (!busy(error)) throw error + if (left <= 1) throw error + return rm(left - 1) + }) + } + + // Windows can keep SQLite WAL handles alive until GC finalizers run, so we + // force GC and retry teardown to avoid flaky EBUSY in test cleanup. + await rm(30) }) process.env["XDG_DATA_HOME"] = path.join(dir, "share")