Skip to content

fix(test): harden preload cleanup against Windows EBUSY#14895

Merged
Hona merged 1 commit intoanomalyco:devfrom
Hona:fix/ebusy-on-cleanup
Feb 24, 2026
Merged

fix(test): harden preload cleanup against Windows EBUSY#14895
Hona merged 1 commit intoanomalyco:devfrom
Hona:fix/ebusy-on-cleanup

Conversation

@Hona
Copy link
Member

@Hona Hona commented Feb 24, 2026

Summary

  • close the test sqlite client explicitly during preload teardown to release open WAL/database handles.
  • replace one-shot temp directory removal with an EBUSY-aware cleanup loop that forces GC and retries removal on Windows.
  • add a comment in preload cleanup explaining why GC + retry teardown is required to prevent flaky test failures.

Copilot AI review requested due to automatic review settings February 24, 2026 11:52
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens test teardown on Windows by explicitly closing the SQLite client used during tests and replacing one-shot temp directory removal with a retrying cleanup that mitigates EBUSY from lingering WAL handles.

Changes:

  • Add Database.close() to explicitly close the underlying bun:sqlite handle and reset the lazy client.
  • Update test preload teardown to GC + retry fs.rm() cleanup to avoid flaky Windows EBUSY failures.
  • Add inline rationale explaining why GC + retry is required on Windows.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
packages/opencode/test/preload.ts Switch teardown to async cleanup with GC + retry loop and close DB before removing temp dir.
packages/opencode/src/storage/db.ts Track the underlying SQLite handle and expose Database.close() for explicit teardown/reset.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +14 to +28
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)
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.
Comment on lines 72 to 77
export const Client = lazy(() => {
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

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.

state.sqlite is assigned immediately after creating the BunDatabase, but Client initialization can still throw later (e.g., during migrations). If an error is thrown, lazy() will retry on the next call and state.sqlite may get overwritten, leaking the original open handle and making close() behavior ambiguous. Consider only setting state.sqlite after successful initialization, or ensuring the created handle is closed/cleared on initialization failure.

Copilot uses AI. Check for mistakes.
@Hona Hona merged commit a292edd into anomalyco:dev Feb 24, 2026
13 of 15 checks passed
lanej pushed a commit to lanej/opencode that referenced this pull request Feb 24, 2026
kcelia pushed a commit to concrete-security/private-opencode that referenced this pull request Feb 25, 2026
jonathanmiddleton pushed a commit to jonathanmiddleton/opencode that referenced this pull request Mar 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants