Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions packages/opencode/src/storage/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand All @@ -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<{
Expand Down
21 changes: 18 additions & 3 deletions packages/opencode/test/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> => {
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)
Comment on lines +14 to +28
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cleanup loop always forces a full GC and sleeps 100ms even on non-Windows platforms (where EBUSY is unlikely). This adds a fixed delay and extra GC pressure to every test run; consider gating the GC+retry logic behind process.platform === "win32" (or similar) and using a single fs.rm on other platforms.

Suggested change
const busy = (error: unknown) =>
typeof error === "object" && error !== null && "code" in error && error.code === "EBUSY"
const rm = async (left: number): Promise<void> => {
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)
if (process.platform === "win32") {
const busy = (error: unknown) =>
typeof error === "object" && error !== null && "code" in error && (error as { code?: unknown }).code === "EBUSY"
const rm = async (left: number): Promise<void> => {
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)
} else {
// On non-Windows platforms, EBUSY is unlikely; a single rm is sufficient.
await fs.rm(dir, { recursive: true, force: true })
}

Copilot uses AI. Check for mistakes.
})

process.env["XDG_DATA_HOME"] = path.join(dir, "share")
Expand Down
Loading