diff --git a/src/plugin/ultrawork-db-model-override.bun-sqlite-unavailable.test.ts b/src/plugin/ultrawork-db-model-override.bun-sqlite-unavailable.test.ts new file mode 100644 index 000000000..ef3d3c8bd --- /dev/null +++ b/src/plugin/ultrawork-db-model-override.bun-sqlite-unavailable.test.ts @@ -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 { + return new Promise((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?]> = [] + + beforeEach(() => { + // Simulate non-Bun runtime (Node/Electron): bun:sqlite import returns null + __setBunSqliteImporterForTesting(async () => null) + + spyOn(sharedModule, "log").mockImplementation((message: string, metadata?: Record) => { + 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", + ) + }) +}) diff --git a/src/plugin/ultrawork-db-model-override.ts b/src/plugin/ultrawork-db-model-override.ts index 8a36609d8..59e4d58c3 100644 --- a/src/plugin/ultrawork-db-model-override.ts +++ b/src/plugin/ultrawork-db-model-override.ts @@ -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 = () => + import("bun:sqlite").catch(() => null) as Promise + +/** @internal test-only */ +export function __setBunSqliteImporterForTesting( + impl: () => Promise, +): void { + _bunSqliteImporter = impl +} + +/** @internal test-only */ +export function __resetBunSqliteImporterForTesting(): void { + _bunSqliteImporter = () => import("bun:sqlite").catch(() => null) as Promise +} + 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, + db: BunDatabase, messageId: string, targetModel: { providerID: string; modelID: string }, variant?: string, @@ -30,7 +48,7 @@ function tryUpdateMessageModel( } function retryViaMicrotask( - db: InstanceType, + 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 + let db: BunDatabase try { db = new Database(dbPath) } catch (error) {