fix(ultrawork): lazy-load bun:sqlite to support Node/Electron runtime

Top-level `import { Database } from 'bun:sqlite'` caused Node/Electron's
ESM loader to reject the plugin at module-graph resolution time because
the `bun:` protocol is not in Node's allowed scheme list. This prevented
the OpenCode desktop app from loading the plugin at all.

Fix:
- Remove top-level static import of `bun:sqlite`
- Use a lazy importer (`_bunSqliteImporter`) that calls
  `import('bun:sqlite').catch(() => null)` at runtime
- If the import returns null (non-Bun environment), log a warning and
  return early — no DB override attempted, plugin loads normally
- Expose `__setBunSqliteImporterForTesting` / `__resetBunSqliteImporterForTesting`
  test seams to verify the Node/Electron fallback path

Closes #3795
This commit is contained in:
YeonGyu-Kim
2026-05-05 18:52:17 +09:00
parent 61ea99a72b
commit f4a225b6cd
2 changed files with 94 additions and 5 deletions
@@ -0,0 +1,60 @@
import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from "bun:test"
import * as sharedModule from "../shared"
import {
scheduleDeferredModelOverride,
__setBunSqliteImporterForTesting,
__resetBunSqliteImporterForTesting,
} from "./ultrawork-db-model-override"
function flushMicrotasks(depth: number): Promise<void> {
return new Promise<void>((resolve) => {
let remaining = depth
function step() {
if (remaining <= 0) {
resolve()
return
}
remaining--
queueMicrotask(step)
}
queueMicrotask(step)
})
}
describe("scheduleDeferredModelOverride bun:sqlite unavailable", () => {
let logCalls: Array<[string, Record<string, unknown>?]> = []
beforeEach(() => {
// Simulate non-Bun runtime (Node/Electron): bun:sqlite import returns null
__setBunSqliteImporterForTesting(async () => null)
spyOn(sharedModule, "log").mockImplementation((message: string, metadata?: Record<string, unknown>) => {
logCalls.push([message, metadata])
})
})
afterEach(() => {
__resetBunSqliteImporterForTesting()
mock.restore()
logCalls = []
})
test("#given non-Bun runtime #when scheduleDeferredModelOverride is called #then it returns without throwing", async () => {
//#given
//#when
expect(() => {
scheduleDeferredModelOverride("msg_unavailable", {
providerID: "anthropic",
modelID: "claude-opus-4-7",
})
}).not.toThrow()
await flushMicrotasks(5)
//#then
const logMessages = logCalls.map(([msg]) => msg)
expect(logMessages).toContain(
"[ultrawork-db-override] bun:sqlite unavailable (non-Bun runtime), skipping deferred override",
)
})
})
+34 -5
View File
@@ -1,9 +1,27 @@
import { Database } from "bun:sqlite"
import { join } from "node:path"
import { existsSync } from "node:fs"
import { getDataDir } from "../shared/data-path"
import { log } from "../shared"
type BunDatabase = import("bun:sqlite").Database
type SqliteModule = { Database: new (path: string) => BunDatabase }
/** @internal test-only seam: override to simulate non-Bun runtime */
let _bunSqliteImporter: () => Promise<SqliteModule | null> = () =>
import("bun:sqlite").catch(() => null) as Promise<SqliteModule | null>
/** @internal test-only */
export function __setBunSqliteImporterForTesting(
impl: () => Promise<SqliteModule | null>,
): void {
_bunSqliteImporter = impl
}
/** @internal test-only */
export function __resetBunSqliteImporterForTesting(): void {
_bunSqliteImporter = () => import("bun:sqlite").catch(() => null) as Promise<SqliteModule | null>
}
function getDbPath(): string {
return join(getDataDir(), "opencode", "opencode.db")
}
@@ -11,7 +29,7 @@ function getDbPath(): string {
const MAX_MICROTASK_RETRIES = 10
function tryUpdateMessageModel(
db: InstanceType<typeof Database>,
db: BunDatabase,
messageId: string,
targetModel: { providerID: string; modelID: string },
variant?: string,
@@ -30,7 +48,7 @@ function tryUpdateMessageModel(
}
function retryViaMicrotask(
db: InstanceType<typeof Database>,
db: BunDatabase,
messageId: string,
targetModel: { providerID: string; modelID: string },
variant: string | undefined,
@@ -106,20 +124,31 @@ function retryViaMicrotask(
* Session.updateMessage() to save the message first, then overwrites the model.
*
* Falls back to setTimeout(fn, 0) after 10 microtask attempts.
*
*/
export function scheduleDeferredModelOverride(
messageId: string,
targetModel: { providerID: string; modelID: string },
variant?: string,
): void {
queueMicrotask(() => {
queueMicrotask(async () => {
// Lazy-load bun:sqlite so this module can be imported under Node/Electron
// without crashing the ESM loader (bun: protocol is Bun-only).
const sqliteModule = await _bunSqliteImporter()
if (sqliteModule === null) {
log("[ultrawork-db-override] bun:sqlite unavailable (non-Bun runtime), skipping deferred override")
return
}
const { Database } = sqliteModule
const dbPath = getDbPath()
if (!existsSync(dbPath)) {
log("[ultrawork-db-override] DB not found, skipping deferred override")
return
}
let db: InstanceType<typeof Database>
let db: BunDatabase
try {
db = new Database(dbPath)
} catch (error) {