From be40cf4d196777fa7e16b67f693a76670894b268 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Fri, 23 Jan 2026 17:44:58 +0100 Subject: [PATCH 01/11] feat(bun): track provider packages for automatic cleanup When a provider switches SDK packages, the old package is now automatically removed to avoid accumulating unused dependencies in the cache. - Add provider tracking in package.json under opencode.providers section - Modify BunProc.install() to accept optional providerID parameter - Remove old package when provider switches to a different SDK - Add comprehensive integration tests for provider tracking --- packages/opencode/src/bun/index.ts | 79 +++++----- packages/opencode/src/global/index.ts | 5 +- packages/opencode/src/provider/provider.ts | 2 +- packages/opencode/test/bun.test.ts | 162 ++++++++++++++++++--- 4 files changed, 180 insertions(+), 68 deletions(-) diff --git a/packages/opencode/src/bun/index.ts b/packages/opencode/src/bun/index.ts index bdb7cff78e2..5520d762d23 100644 --- a/packages/opencode/src/bun/index.ts +++ b/packages/opencode/src/bun/index.ts @@ -12,6 +12,13 @@ import { proxied } from "@/util/proxied" export namespace BunProc { const log = Log.create({ service: "bun" }) + interface PackageJson { + dependencies?: Record + opencode?: { + providers?: Record + } + } + export async function run(cmd: string[], options?: Bun.SpawnOptions.OptionsObject) { log.info("running", { cmd: [which(), ...cmd], @@ -61,21 +68,31 @@ export namespace BunProc { }), ) - export async function install(pkg: string, version = "latest") { - // Use lock to ensure only one install at a time + async function readPackageJson(): Promise { + const file = Bun.file(path.join(Global.Path.cache, "package.json")) + return file.json().catch(() => ({})) + } + + async function writePackageJson(parsed: PackageJson) { + const file = Bun.file(path.join(Global.Path.cache, "package.json")) + await Bun.write(file.name!, JSON.stringify(parsed, null, 2)) + } + + async function track(provider: string, pkg: string) { + const parsed = await readPackageJson() + if (!parsed.opencode) parsed.opencode = {} + if (!parsed.opencode.providers) parsed.opencode.providers = {} + parsed.opencode.providers[provider] = pkg + await writePackageJson(parsed) + } + + export async function install(pkg: string, version = "latest", provider?: string) { using _ = await Lock.write("bun-install") const mod = path.join(Global.Path.cache, "node_modules", pkg) - const pkgjson = Bun.file(path.join(Global.Path.cache, "package.json")) - const parsed = await pkgjson.json().catch(async () => { - const result = { dependencies: {} } - await Bun.write(pkgjson.name!, JSON.stringify(result, null, 2)) - return result - }) - const dependencies = parsed.dependencies ?? {} - if (!parsed.dependencies) parsed.dependencies = dependencies - const modExists = await Filesystem.exists(mod) - const cachedVersion = dependencies[pkg] + const parsed = await readPackageJson() + const oldPkg = provider ? parsed.opencode?.providers?.[provider] : undefined + const pkgSwitched = oldPkg && oldPkg !== pkg if (!modExists || !cachedVersion) { // continue to install @@ -87,7 +104,6 @@ export namespace BunProc { log.info("Cached version is outdated, proceeding with install", { pkg, cachedVersion }) } - // Build command arguments const args = [ "add", "--force", @@ -99,39 +115,16 @@ export namespace BunProc { pkg + "@" + version, ] - // Let Bun handle registry resolution: - // - If .npmrc files exist, Bun will use them automatically - // - If no .npmrc files exist, Bun will default to https://registry.npmjs.org - // - No need to pass --registry flag - log.info("installing package using Bun's default registry resolution", { - pkg, - version, - }) + log.info("installing package", { pkg, version }) - await BunProc.run(args, { - cwd: Global.Path.cache, - }).catch((e) => { - throw new InstallFailedError( - { pkg, version }, - { - cause: e, - }, - ) - }) - - // Resolve actual version from installed package when using "latest" - // This ensures subsequent starts use the cached version until explicitly updated - let resolvedVersion = version - if (version === "latest") { - const installedPkgJson = Bun.file(path.join(mod, "package.json")) - const installedPkg = await installedPkgJson.json().catch(() => null) - if (installedPkg?.version) { - resolvedVersion = installedPkg.version + await BunProc.run(args, { cwd: Global.Path.cache }).catch((e) => { + if (pkgSwitched && provider) { + log.info("install failed, keeping old provider package tracking", { provider, old: oldPkg }) } - } + throw new InstallFailedError({ pkg, version }, { cause: e }) + }) - parsed.dependencies[pkg] = resolvedVersion - await Bun.write(pkgjson.name!, JSON.stringify(parsed, null, 2)) + if (provider) await track(provider, pkg) return mod } } diff --git a/packages/opencode/src/global/index.ts b/packages/opencode/src/global/index.ts index 10b6125a6a9..ab522506284 100644 --- a/packages/opencode/src/global/index.ts +++ b/packages/opencode/src/global/index.ts @@ -19,7 +19,10 @@ export namespace Global { data, bin: path.join(data, "bin"), log: path.join(data, "log"), - cache, + // Allow override via OPENCODE_TEST_CACHE for test isolation + get cache() { + return process.env.OPENCODE_TEST_CACHE || cache + }, config, state, } diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 614d4ec1443..240e3b0bae5 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -1058,7 +1058,7 @@ export namespace Provider { let installedPath: string if (!model.api.npm.startsWith("file://")) { - installedPath = await BunProc.install(model.api.npm, "latest") + installedPath = await BunProc.install(model.api.npm, "latest", model.providerID) } else { log.info("loading local provider", { pkg: model.api.npm }) installedPath = model.api.npm diff --git a/packages/opencode/test/bun.test.ts b/packages/opencode/test/bun.test.ts index d607ae47820..1bcdd160d01 100644 --- a/packages/opencode/test/bun.test.ts +++ b/packages/opencode/test/bun.test.ts @@ -1,53 +1,169 @@ -import { describe, expect, test } from "bun:test" +import { describe, expect, test, beforeEach, afterEach } from "bun:test" import fs from "fs/promises" import path from "path" +import os from "os" describe("BunProc registry configuration", () => { test("should not contain hardcoded registry parameters", async () => { - // Read the bun/index.ts file const bunIndexPath = path.join(__dirname, "../src/bun/index.ts") const content = await fs.readFile(bunIndexPath, "utf-8") - // Verify that no hardcoded registry is present expect(content).not.toContain("--registry=") expect(content).not.toContain("hasNpmRcConfig") expect(content).not.toContain("NpmRc") }) - test("should use Bun's default registry resolution", async () => { - // Read the bun/index.ts file + test("should have correct bun add command structure", async () => { const bunIndexPath = path.join(__dirname, "../src/bun/index.ts") const content = await fs.readFile(bunIndexPath, "utf-8") - // Verify that it uses Bun's default resolution - expect(content).toContain("Bun's default registry resolution") - expect(content).toContain("Bun will use them automatically") - expect(content).toContain("No need to pass --registry flag") - }) - - test("should have correct command structure without registry", async () => { - // Read the bun/index.ts file - const bunIndexPath = path.join(__dirname, "../src/bun/index.ts") - const content = await fs.readFile(bunIndexPath, "utf-8") - - // Extract the install function const installFunctionMatch = content.match(/export async function install[\s\S]*?^ }/m) expect(installFunctionMatch).toBeTruthy() if (installFunctionMatch) { const installFunction = installFunctionMatch[0] - // Verify expected arguments are present expect(installFunction).toContain('"add"') expect(installFunction).toContain('"--force"') expect(installFunction).toContain('"--exact"') expect(installFunction).toContain('"--cwd"') - expect(installFunction).toContain("Global.Path.cache") - expect(installFunction).toContain('pkg + "@" + version') - - // Verify no registry argument is added expect(installFunction).not.toContain('"--registry"') - expect(installFunction).not.toContain('args.push("--registry') } }) }) + +describe("BunProc.install provider tracking", () => { + let tempDir: string + let originalCache: string | undefined + + beforeEach(async () => { + tempDir = path.join(os.tmpdir(), "opencode-bun-test-" + Math.random().toString(36).slice(2)) + await fs.mkdir(tempDir, { recursive: true }) + originalCache = process.env.OPENCODE_TEST_CACHE + process.env.OPENCODE_TEST_CACHE = tempDir + }) + + afterEach(async () => { + if (originalCache === undefined) { + delete process.env.OPENCODE_TEST_CACHE + } else { + process.env.OPENCODE_TEST_CACHE = originalCache + } + await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {}) + }) + + async function readPkgJson() { + return JSON.parse(await fs.readFile(path.join(tempDir, "package.json"), "utf-8")) + } + + async function pkgExists(pkg: string) { + return fs + .stat(path.join(tempDir, "node_modules", pkg)) + .then(() => true) + .catch(() => false) + } + + test("should track provider in opencode.providers section", async () => { + const { BunProc } = await import("../src/bun") + + await BunProc.install("zod", "latest", "anthropic") + + const pkgJson = await readPkgJson() + expect(pkgJson.opencode?.providers?.anthropic).toBe("zod") + }) + + test("should install package in node_modules", async () => { + const { BunProc } = await import("../src/bun") + + await BunProc.install("zod", "latest", "anthropic") + + expect(await pkgExists("zod")).toBe(true) + }) + + test("should update tracking when provider switches packages", async () => { + const { BunProc } = await import("../src/bun") + + await BunProc.install("zod", "latest", "anthropic") + let pkgJson = await readPkgJson() + expect(pkgJson.opencode?.providers?.anthropic).toBe("zod") + + await BunProc.install("superstruct", "latest", "anthropic") + pkgJson = await readPkgJson() + expect(pkgJson.opencode?.providers?.anthropic).toBe("superstruct") + }) + + test("should remove old package when provider switches", async () => { + const { BunProc } = await import("../src/bun") + + await BunProc.install("zod", "latest", "anthropic") + expect(await pkgExists("zod")).toBe(true) + + await BunProc.install("superstruct", "latest", "anthropic") + + // Old package should be removed + expect(await pkgExists("zod")).toBe(false) + // New package should be installed + expect(await pkgExists("superstruct")).toBe(true) + }) + + test("should remove old package even when new package is already cached", async () => { + const { BunProc } = await import("../src/bun") + + // Install zod for provider-a + await BunProc.install("zod", "latest", "provider-a") + expect(await pkgExists("zod")).toBe(true) + + // Install superstruct for provider-b (now superstruct is cached) + await BunProc.install("superstruct", "latest", "provider-b") + expect(await pkgExists("superstruct")).toBe(true) + + // Switch provider-a from zod to superstruct (superstruct already cached) + await BunProc.install("superstruct", "latest", "provider-a") + + // zod should be removed since provider-a no longer uses it + expect(await pkgExists("zod")).toBe(false) + + // Tracking should be updated + const pkgJson = await readPkgJson() + expect(pkgJson.opencode?.providers?.["provider-a"]).toBe("superstruct") + expect(pkgJson.opencode?.providers?.["provider-b"]).toBe("superstruct") + }) + + test("should not remove package if provider is not switching", async () => { + const { BunProc } = await import("../src/bun") + + // Install zod for anthropic + await BunProc.install("zod", "latest", "anthropic") + + // Install same package again (e.g., version check) + await BunProc.install("zod", "latest", "anthropic") + + // Package should still exist + expect(await pkgExists("zod")).toBe(true) + }) + + test("should work without providerID (backward compatible)", async () => { + const { BunProc } = await import("../src/bun") + + await BunProc.install("zod", "latest") + + expect(await pkgExists("zod")).toBe(true) + const pkgJson = await readPkgJson() + // No provider tracking when providerID not provided + expect(pkgJson.opencode?.providers).toBeUndefined() + }) + + test("should track multiple providers independently", async () => { + const { BunProc } = await import("../src/bun") + + await BunProc.install("zod", "latest", "anthropic") + await BunProc.install("superstruct", "latest", "openai") + + const pkgJson = await readPkgJson() + expect(pkgJson.opencode?.providers?.anthropic).toBe("zod") + expect(pkgJson.opencode?.providers?.openai).toBe("superstruct") + + expect(await pkgExists("zod")).toBe(true) + expect(await pkgExists("superstruct")).toBe(true) + }) +}) From 244a9e03fe3fe83c01547f6c0a768aea6832acd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Fri, 23 Jan 2026 18:01:24 +0100 Subject: [PATCH 02/11] fix(bun): address review feedback for provider tracking - Don't remove package if other providers still use it - Fix version comparison to handle "latest" correctly - Add test for shared package scenario --- packages/opencode/src/bun/index.ts | 19 ++++++++++++++++--- packages/opencode/test/bun.test.ts | 19 +++++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/bun/index.ts b/packages/opencode/src/bun/index.ts index 5520d762d23..88dc9640b77 100644 --- a/packages/opencode/src/bun/index.ts +++ b/packages/opencode/src/bun/index.ts @@ -94,9 +94,22 @@ export namespace BunProc { const oldPkg = provider ? parsed.opencode?.providers?.[provider] : undefined const pkgSwitched = oldPkg && oldPkg !== pkg - if (!modExists || !cachedVersion) { - // continue to install - } else if (version !== "latest" && cachedVersion === version) { + if (pkgSwitched) { + const providers = parsed.opencode?.providers ?? {} + const used = Object.entries(providers).some(([p, name]) => p !== provider && name === oldPkg) + if (used) { + log.info("provider package changed but still used", { provider, old: oldPkg, new: pkg }) + } else { + log.info("provider package changed", { provider, old: oldPkg, new: pkg }) + await BunProc.run(["remove", "--cwd", Global.Path.cache, oldPkg]).catch(() => {}) + } + } + + // Re-read after potential remove, check if already installed + const current = pkgSwitched ? await readPackageJson() : parsed + const installed = current.dependencies?.[pkg] + if (installed && (version === "latest" || installed === version) && (await Filesystem.exists(mod))) { + if (provider) await track(provider, pkg) return mod } else if (version === "latest") { const isOutdated = await PackageRegistry.isOutdated(pkg, cachedVersion, Global.Path.cache) diff --git a/packages/opencode/test/bun.test.ts b/packages/opencode/test/bun.test.ts index 1bcdd160d01..137a8d10fb9 100644 --- a/packages/opencode/test/bun.test.ts +++ b/packages/opencode/test/bun.test.ts @@ -166,4 +166,23 @@ describe("BunProc.install provider tracking", () => { expect(await pkgExists("zod")).toBe(true) expect(await pkgExists("superstruct")).toBe(true) }) + + test("should not remove package if another provider still uses it", async () => { + const { BunProc } = await import("../src/bun") + + // Both providers use zod + await BunProc.install("zod", "latest", "anthropic") + await BunProc.install("zod", "latest", "openai") + + // anthropic switches to superstruct + await BunProc.install("superstruct", "latest", "anthropic") + + // zod should still exist because openai uses it + expect(await pkgExists("zod")).toBe(true) + expect(await pkgExists("superstruct")).toBe(true) + + const pkgJson = await readPkgJson() + expect(pkgJson.opencode?.providers?.anthropic).toBe("superstruct") + expect(pkgJson.opencode?.providers?.openai).toBe("zod") + }) }) From b194a1f8f7db5722b15b6dee849ba3885684a812 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Fri, 23 Jan 2026 18:11:42 +0100 Subject: [PATCH 03/11] fix: install new package before removing old to prevent broken state --- packages/opencode/src/bun/index.ts | 40 ++++++++++++++++-------------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/packages/opencode/src/bun/index.ts b/packages/opencode/src/bun/index.ts index 88dc9640b77..206d034a8cc 100644 --- a/packages/opencode/src/bun/index.ts +++ b/packages/opencode/src/bun/index.ts @@ -92,24 +92,21 @@ export namespace BunProc { const mod = path.join(Global.Path.cache, "node_modules", pkg) const parsed = await readPackageJson() const oldPkg = provider ? parsed.opencode?.providers?.[provider] : undefined - const pkgSwitched = oldPkg && oldPkg !== pkg + const switched = oldPkg && oldPkg !== pkg - if (pkgSwitched) { - const providers = parsed.opencode?.providers ?? {} - const used = Object.entries(providers).some(([p, name]) => p !== provider && name === oldPkg) - if (used) { - log.info("provider package changed but still used", { provider, old: oldPkg, new: pkg }) - } else { - log.info("provider package changed", { provider, old: oldPkg, new: pkg }) - await BunProc.run(["remove", "--cwd", Global.Path.cache, oldPkg]).catch(() => {}) - } - } - - // Re-read after potential remove, check if already installed - const current = pkgSwitched ? await readPackageJson() : parsed - const installed = current.dependencies?.[pkg] + // Check if already installed + const installed = parsed.dependencies?.[pkg] if (installed && (version === "latest" || installed === version) && (await Filesystem.exists(mod))) { if (provider) await track(provider, pkg) + // Remove old package after tracking update, only if not used by others + if (switched) { + const providers = parsed.opencode?.providers ?? {} + const used = Object.entries(providers).some(([p, name]) => p !== provider && name === oldPkg) + if (!used) { + log.info("removing unused package", { pkg: oldPkg }) + await BunProc.run(["remove", "--cwd", Global.Path.cache, oldPkg]).catch(() => {}) + } + } return mod } else if (version === "latest") { const isOutdated = await PackageRegistry.isOutdated(pkg, cachedVersion, Global.Path.cache) @@ -131,13 +128,20 @@ export namespace BunProc { log.info("installing package", { pkg, version }) await BunProc.run(args, { cwd: Global.Path.cache }).catch((e) => { - if (pkgSwitched && provider) { - log.info("install failed, keeping old provider package tracking", { provider, old: oldPkg }) - } throw new InstallFailedError({ pkg, version }, { cause: e }) }) + // Install succeeded - update tracking and remove old package if (provider) await track(provider, pkg) + if (switched) { + const current = await readPackageJson() + const providers = current.opencode?.providers ?? {} + const used = Object.entries(providers).some(([p, name]) => p !== provider && name === oldPkg) + if (!used) { + log.info("removing unused package", { pkg: oldPkg }) + await BunProc.run(["remove", "--cwd", Global.Path.cache, oldPkg!]).catch(() => {}) + } + } return mod } } From 7a750aaa02a2699dd3704bbf1372392888377d37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Fri, 23 Jan 2026 18:34:27 +0100 Subject: [PATCH 04/11] fix: always reinstall when version is latest --- packages/opencode/src/bun/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/bun/index.ts b/packages/opencode/src/bun/index.ts index 206d034a8cc..8bbb235a2f1 100644 --- a/packages/opencode/src/bun/index.ts +++ b/packages/opencode/src/bun/index.ts @@ -94,9 +94,9 @@ export namespace BunProc { const oldPkg = provider ? parsed.opencode?.providers?.[provider] : undefined const switched = oldPkg && oldPkg !== pkg - // Check if already installed + // Check if already installed with exact version const installed = parsed.dependencies?.[pkg] - if (installed && (version === "latest" || installed === version) && (await Filesystem.exists(mod))) { + if (installed && version !== "latest" && installed === version && (await Filesystem.exists(mod))) { if (provider) await track(provider, pkg) // Remove old package after tracking update, only if not used by others if (switched) { From 6f92a5b2abcf6c7e2fc8b42eab3097fea5c6455b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Fri, 23 Jan 2026 18:45:55 +0100 Subject: [PATCH 05/11] test: add edge cases for package.json states --- packages/opencode/test/bun.test.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/packages/opencode/test/bun.test.ts b/packages/opencode/test/bun.test.ts index 137a8d10fb9..b2827652d3a 100644 --- a/packages/opencode/test/bun.test.ts +++ b/packages/opencode/test/bun.test.ts @@ -185,4 +185,30 @@ describe("BunProc.install provider tracking", () => { expect(pkgJson.opencode?.providers?.anthropic).toBe("superstruct") expect(pkgJson.opencode?.providers?.openai).toBe("zod") }) + + test("should work when package.json exists without opencode section", async () => { + const { BunProc } = await import("../src/bun") + + // Create package.json without opencode section + await fs.writeFile(path.join(tempDir, "package.json"), JSON.stringify({ dependencies: {} }, null, 2)) + + await BunProc.install("zod", "latest", "anthropic") + + const pkgJson = await readPkgJson() + expect(pkgJson.opencode?.providers?.anthropic).toBe("zod") + expect(await pkgExists("zod")).toBe(true) + }) + + test("should work when opencode section exists without providers", async () => { + const { BunProc } = await import("../src/bun") + + // Create package.json with opencode but no providers + await fs.writeFile(path.join(tempDir, "package.json"), JSON.stringify({ dependencies: {}, opencode: {} }, null, 2)) + + await BunProc.install("zod", "latest", "anthropic") + + const pkgJson = await readPkgJson() + expect(pkgJson.opencode?.providers?.anthropic).toBe("zod") + expect(await pkgExists("zod")).toBe(true) + }) }) From 1afc4043840d06672a30d0681fa1ea751168900c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Fri, 23 Jan 2026 18:51:46 +0100 Subject: [PATCH 06/11] chore: clean up comments --- packages/opencode/src/bun/index.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/opencode/src/bun/index.ts b/packages/opencode/src/bun/index.ts index 8bbb235a2f1..eee4a00700e 100644 --- a/packages/opencode/src/bun/index.ts +++ b/packages/opencode/src/bun/index.ts @@ -94,11 +94,10 @@ export namespace BunProc { const oldPkg = provider ? parsed.opencode?.providers?.[provider] : undefined const switched = oldPkg && oldPkg !== pkg - // Check if already installed with exact version + // Skip install if exact version already cached (always reinstall with "latest") const installed = parsed.dependencies?.[pkg] if (installed && version !== "latest" && installed === version && (await Filesystem.exists(mod))) { if (provider) await track(provider, pkg) - // Remove old package after tracking update, only if not used by others if (switched) { const providers = parsed.opencode?.providers ?? {} const used = Object.entries(providers).some(([p, name]) => p !== provider && name === oldPkg) @@ -131,7 +130,6 @@ export namespace BunProc { throw new InstallFailedError({ pkg, version }, { cause: e }) }) - // Install succeeded - update tracking and remove old package if (provider) await track(provider, pkg) if (switched) { const current = await readPackageJson() From ecc94d661c3f81375691401eda3c296e019f7051 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Fri, 23 Jan 2026 20:06:00 +0100 Subject: [PATCH 07/11] test(bun): verify exact semver version after install --- packages/opencode/test/bun.test.ts | 53 +++++++++++++++++------------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/packages/opencode/test/bun.test.ts b/packages/opencode/test/bun.test.ts index b2827652d3a..4de235475d0 100644 --- a/packages/opencode/test/bun.test.ts +++ b/packages/opencode/test/bun.test.ts @@ -63,6 +63,13 @@ describe("BunProc.install provider tracking", () => { .catch(() => false) } + const SEMVER_REGEX = + /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/ + + function isExactVersion(v: string) { + return typeof v === "string" && SEMVER_REGEX.test(v) + } + test("should track provider in opencode.providers section", async () => { const { BunProc } = await import("../src/bun") @@ -70,6 +77,7 @@ describe("BunProc.install provider tracking", () => { const pkgJson = await readPkgJson() expect(pkgJson.opencode?.providers?.anthropic).toBe("zod") + expect(isExactVersion(pkgJson.dependencies?.zod)).toBe(true) }) test("should install package in node_modules", async () => { @@ -78,18 +86,22 @@ describe("BunProc.install provider tracking", () => { await BunProc.install("zod", "latest", "anthropic") expect(await pkgExists("zod")).toBe(true) + const pkgJson = await readPkgJson() + expect(isExactVersion(pkgJson.dependencies?.zod)).toBe(true) }) test("should update tracking when provider switches packages", async () => { const { BunProc } = await import("../src/bun") await BunProc.install("zod", "latest", "anthropic") - let pkgJson = await readPkgJson() - expect(pkgJson.opencode?.providers?.anthropic).toBe("zod") + const pkgJson1 = await readPkgJson() + expect(pkgJson1.opencode?.providers?.anthropic).toBe("zod") + expect(isExactVersion(pkgJson1.dependencies?.zod)).toBe(true) await BunProc.install("superstruct", "latest", "anthropic") - pkgJson = await readPkgJson() - expect(pkgJson.opencode?.providers?.anthropic).toBe("superstruct") + const pkgJson2 = await readPkgJson() + expect(pkgJson2.opencode?.providers?.anthropic).toBe("superstruct") + expect(isExactVersion(pkgJson2.dependencies?.superstruct)).toBe(true) }) test("should remove old package when provider switches", async () => { @@ -100,30 +112,23 @@ describe("BunProc.install provider tracking", () => { await BunProc.install("superstruct", "latest", "anthropic") - // Old package should be removed expect(await pkgExists("zod")).toBe(false) - // New package should be installed expect(await pkgExists("superstruct")).toBe(true) }) test("should remove old package even when new package is already cached", async () => { const { BunProc } = await import("../src/bun") - // Install zod for provider-a await BunProc.install("zod", "latest", "provider-a") expect(await pkgExists("zod")).toBe(true) - // Install superstruct for provider-b (now superstruct is cached) await BunProc.install("superstruct", "latest", "provider-b") expect(await pkgExists("superstruct")).toBe(true) - // Switch provider-a from zod to superstruct (superstruct already cached) await BunProc.install("superstruct", "latest", "provider-a") - // zod should be removed since provider-a no longer uses it expect(await pkgExists("zod")).toBe(false) - // Tracking should be updated const pkgJson = await readPkgJson() expect(pkgJson.opencode?.providers?.["provider-a"]).toBe("superstruct") expect(pkgJson.opencode?.providers?.["provider-b"]).toBe("superstruct") @@ -132,13 +137,9 @@ describe("BunProc.install provider tracking", () => { test("should not remove package if provider is not switching", async () => { const { BunProc } = await import("../src/bun") - // Install zod for anthropic await BunProc.install("zod", "latest", "anthropic") - - // Install same package again (e.g., version check) await BunProc.install("zod", "latest", "anthropic") - // Package should still exist expect(await pkgExists("zod")).toBe(true) }) @@ -149,8 +150,8 @@ describe("BunProc.install provider tracking", () => { expect(await pkgExists("zod")).toBe(true) const pkgJson = await readPkgJson() - // No provider tracking when providerID not provided expect(pkgJson.opencode?.providers).toBeUndefined() + expect(isExactVersion(pkgJson.dependencies?.zod)).toBe(true) }) test("should track multiple providers independently", async () => { @@ -165,19 +166,17 @@ describe("BunProc.install provider tracking", () => { expect(await pkgExists("zod")).toBe(true) expect(await pkgExists("superstruct")).toBe(true) + expect(isExactVersion(pkgJson.dependencies?.zod)).toBe(true) + expect(isExactVersion(pkgJson.dependencies?.superstruct)).toBe(true) }) test("should not remove package if another provider still uses it", async () => { const { BunProc } = await import("../src/bun") - // Both providers use zod await BunProc.install("zod", "latest", "anthropic") await BunProc.install("zod", "latest", "openai") - - // anthropic switches to superstruct await BunProc.install("superstruct", "latest", "anthropic") - // zod should still exist because openai uses it expect(await pkgExists("zod")).toBe(true) expect(await pkgExists("superstruct")).toBe(true) @@ -189,7 +188,6 @@ describe("BunProc.install provider tracking", () => { test("should work when package.json exists without opencode section", async () => { const { BunProc } = await import("../src/bun") - // Create package.json without opencode section await fs.writeFile(path.join(tempDir, "package.json"), JSON.stringify({ dependencies: {} }, null, 2)) await BunProc.install("zod", "latest", "anthropic") @@ -197,12 +195,12 @@ describe("BunProc.install provider tracking", () => { const pkgJson = await readPkgJson() expect(pkgJson.opencode?.providers?.anthropic).toBe("zod") expect(await pkgExists("zod")).toBe(true) + expect(isExactVersion(pkgJson.dependencies?.zod)).toBe(true) }) test("should work when opencode section exists without providers", async () => { const { BunProc } = await import("../src/bun") - // Create package.json with opencode but no providers await fs.writeFile(path.join(tempDir, "package.json"), JSON.stringify({ dependencies: {}, opencode: {} }, null, 2)) await BunProc.install("zod", "latest", "anthropic") @@ -210,5 +208,16 @@ describe("BunProc.install provider tracking", () => { const pkgJson = await readPkgJson() expect(pkgJson.opencode?.providers?.anthropic).toBe("zod") expect(await pkgExists("zod")).toBe(true) + expect(isExactVersion(pkgJson.dependencies?.zod)).toBe(true) + }) + + test("should install exact version when specific version provided", async () => { + const { BunProc } = await import("../src/bun") + + await BunProc.install("zod", "3.23.0", "anthropic") + + const pkgJson = await readPkgJson() + expect(pkgJson.dependencies?.zod).toBe("3.23.0") + expect(await pkgExists("zod")).toBe(true) }) }) From 757865eaaf226e6254d01b1f649ad155ddb5d5ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Fri, 23 Jan 2026 20:33:32 +0100 Subject: [PATCH 08/11] test(bun): harmonize version checks across all tests --- packages/opencode/test/bun.test.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/opencode/test/bun.test.ts b/packages/opencode/test/bun.test.ts index 4de235475d0..116636403b6 100644 --- a/packages/opencode/test/bun.test.ts +++ b/packages/opencode/test/bun.test.ts @@ -80,11 +80,12 @@ describe("BunProc.install provider tracking", () => { expect(isExactVersion(pkgJson.dependencies?.zod)).toBe(true) }) - test("should install package in node_modules", async () => { + test("should install package and return module path", async () => { const { BunProc } = await import("../src/bun") - await BunProc.install("zod", "latest", "anthropic") + const mod = await BunProc.install("zod", "latest", "anthropic") + expect(mod).toContain("node_modules/zod") expect(await pkgExists("zod")).toBe(true) const pkgJson = await readPkgJson() expect(isExactVersion(pkgJson.dependencies?.zod)).toBe(true) @@ -114,6 +115,10 @@ describe("BunProc.install provider tracking", () => { expect(await pkgExists("zod")).toBe(false) expect(await pkgExists("superstruct")).toBe(true) + + const pkgJson = await readPkgJson() + expect(pkgJson.dependencies?.zod).toBeUndefined() + expect(isExactVersion(pkgJson.dependencies?.superstruct)).toBe(true) }) test("should remove old package even when new package is already cached", async () => { @@ -132,6 +137,8 @@ describe("BunProc.install provider tracking", () => { const pkgJson = await readPkgJson() expect(pkgJson.opencode?.providers?.["provider-a"]).toBe("superstruct") expect(pkgJson.opencode?.providers?.["provider-b"]).toBe("superstruct") + expect(pkgJson.dependencies?.zod).toBeUndefined() + expect(isExactVersion(pkgJson.dependencies?.superstruct)).toBe(true) }) test("should not remove package if provider is not switching", async () => { @@ -183,6 +190,8 @@ describe("BunProc.install provider tracking", () => { const pkgJson = await readPkgJson() expect(pkgJson.opencode?.providers?.anthropic).toBe("superstruct") expect(pkgJson.opencode?.providers?.openai).toBe("zod") + expect(isExactVersion(pkgJson.dependencies?.zod)).toBe(true) + expect(isExactVersion(pkgJson.dependencies?.superstruct)).toBe(true) }) test("should work when package.json exists without opencode section", async () => { From 29ccd76cca2d1a5cc4def370b782879eb4330955 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Tue, 3 Feb 2026 23:38:43 +0100 Subject: [PATCH 09/11] refactor: dedupe and harmonize bun tests --- packages/opencode/src/bun/index.ts | 41 ++--- packages/opencode/test/bun.test.ts | 271 ++++++++++++++--------------- 2 files changed, 155 insertions(+), 157 deletions(-) diff --git a/packages/opencode/src/bun/index.ts b/packages/opencode/src/bun/index.ts index eee4a00700e..52df88ccc02 100644 --- a/packages/opencode/src/bun/index.ts +++ b/packages/opencode/src/bun/index.ts @@ -86,6 +86,15 @@ export namespace BunProc { await writePackageJson(parsed) } + async function cleanup(provider: string, oldPkg: string) { + const parsed = await readPackageJson() + const providers = parsed.opencode?.providers ?? {} + const used = Object.entries(providers).some(([p, name]) => p !== provider && name === oldPkg) + if (used) return + log.info("removing unused package", { pkg: oldPkg }) + await BunProc.run(["remove", "--cwd", Global.Path.cache, oldPkg]).catch(() => {}) + } + export async function install(pkg: string, version = "latest", provider?: string) { using _ = await Lock.write("bun-install") @@ -93,23 +102,23 @@ export namespace BunProc { const parsed = await readPackageJson() const oldPkg = provider ? parsed.opencode?.providers?.[provider] : undefined const switched = oldPkg && oldPkg !== pkg + const dependencies = parsed.dependencies ?? {} + const modExists = await Filesystem.exists(mod) + const cachedVersion = dependencies[pkg] + + const earlyReturn = async () => { + if (provider) await track(provider, pkg) + if (switched) await cleanup(provider!, oldPkg!) + return mod + } // Skip install if exact version already cached (always reinstall with "latest") const installed = parsed.dependencies?.[pkg] if (installed && version !== "latest" && installed === version && (await Filesystem.exists(mod))) { - if (provider) await track(provider, pkg) - if (switched) { - const providers = parsed.opencode?.providers ?? {} - const used = Object.entries(providers).some(([p, name]) => p !== provider && name === oldPkg) - if (!used) { - log.info("removing unused package", { pkg: oldPkg }) - await BunProc.run(["remove", "--cwd", Global.Path.cache, oldPkg]).catch(() => {}) - } - } - return mod + return earlyReturn() } else if (version === "latest") { const isOutdated = await PackageRegistry.isOutdated(pkg, cachedVersion, Global.Path.cache) - if (!isOutdated) return mod + if (!isOutdated) return earlyReturn() log.info("Cached version is outdated, proceeding with install", { pkg, cachedVersion }) } @@ -131,15 +140,7 @@ export namespace BunProc { }) if (provider) await track(provider, pkg) - if (switched) { - const current = await readPackageJson() - const providers = current.opencode?.providers ?? {} - const used = Object.entries(providers).some(([p, name]) => p !== provider && name === oldPkg) - if (!used) { - log.info("removing unused package", { pkg: oldPkg }) - await BunProc.run(["remove", "--cwd", Global.Path.cache, oldPkg!]).catch(() => {}) - } - } + if (switched) await cleanup(provider!, oldPkg!) return mod } } diff --git a/packages/opencode/test/bun.test.ts b/packages/opencode/test/bun.test.ts index 116636403b6..3138eb9b014 100644 --- a/packages/opencode/test/bun.test.ts +++ b/packages/opencode/test/bun.test.ts @@ -1,232 +1,229 @@ -import { describe, expect, test, beforeEach, afterEach } from "bun:test" -import fs from "fs/promises" +import { describe, expect, test } from "bun:test" import path from "path" -import os from "os" +import { tmpdir } from "./fixture/fixture" -describe("BunProc registry configuration", () => { - test("should not contain hardcoded registry parameters", async () => { - const bunIndexPath = path.join(__dirname, "../src/bun/index.ts") - const content = await fs.readFile(bunIndexPath, "utf-8") +const SEMVER_REGEX = + /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/ - expect(content).not.toContain("--registry=") - expect(content).not.toContain("hasNpmRcConfig") - expect(content).not.toContain("NpmRc") - }) - - test("should have correct bun add command structure", async () => { - const bunIndexPath = path.join(__dirname, "../src/bun/index.ts") - const content = await fs.readFile(bunIndexPath, "utf-8") - - const installFunctionMatch = content.match(/export async function install[\s\S]*?^ }/m) - expect(installFunctionMatch).toBeTruthy() +function isExactVersion(v: string) { + return typeof v === "string" && SEMVER_REGEX.test(v) +} - if (installFunctionMatch) { - const installFunction = installFunctionMatch[0] +describe("BunProc", () => { + test("install function has correct bun add command structure", async () => { + const content = await Bun.file(path.join(__dirname, "../src/bun/index.ts")).text() - expect(installFunction).toContain('"add"') - expect(installFunction).toContain('"--force"') - expect(installFunction).toContain('"--exact"') - expect(installFunction).toContain('"--cwd"') - expect(installFunction).not.toContain('"--registry"') - } - }) -}) - -describe("BunProc.install provider tracking", () => { - let tempDir: string - let originalCache: string | undefined - - beforeEach(async () => { - tempDir = path.join(os.tmpdir(), "opencode-bun-test-" + Math.random().toString(36).slice(2)) - await fs.mkdir(tempDir, { recursive: true }) - originalCache = process.env.OPENCODE_TEST_CACHE - process.env.OPENCODE_TEST_CACHE = tempDir - }) + const match = content.match(/export async function install[\s\S]*?^ }/m) + expect(match).toBeTruthy() - afterEach(async () => { - if (originalCache === undefined) { - delete process.env.OPENCODE_TEST_CACHE - } else { - process.env.OPENCODE_TEST_CACHE = originalCache - } - await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {}) + const fn = match![0] + expect(fn).toContain('"add"') + expect(fn).toContain('"--force"') + expect(fn).toContain('"--exact"') + expect(fn).toContain('"--cwd"') + expect(fn).not.toContain('"--registry"') + expect(content).not.toContain("--registry=") + expect(content).not.toContain("hasNpmRcConfig") }) - async function readPkgJson() { - return JSON.parse(await fs.readFile(path.join(tempDir, "package.json"), "utf-8")) - } - - async function pkgExists(pkg: string) { - return fs - .stat(path.join(tempDir, "node_modules", pkg)) - .then(() => true) - .catch(() => false) - } - - const SEMVER_REGEX = - /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/ - - function isExactVersion(v: string) { - return typeof v === "string" && SEMVER_REGEX.test(v) - } + test("installs package, tracks provider, and returns module path", async () => { + await using tmp = await tmpdir() + process.env.OPENCODE_TEST_CACHE = tmp.path - test("should track provider in opencode.providers section", async () => { const { BunProc } = await import("../src/bun") + const mod = await BunProc.install("zod", "latest", "anthropic") - await BunProc.install("zod", "latest", "anthropic") + expect(mod).toContain("node_modules/zod") + expect(await Bun.file(path.join(tmp.path, "node_modules", "zod", "package.json")).exists()).toBe(true) - const pkgJson = await readPkgJson() + const pkgJson = await Bun.file(path.join(tmp.path, "package.json")).json() expect(pkgJson.opencode?.providers?.anthropic).toBe("zod") expect(isExactVersion(pkgJson.dependencies?.zod)).toBe(true) - }) - test("should install package and return module path", async () => { - const { BunProc } = await import("../src/bun") - - const mod = await BunProc.install("zod", "latest", "anthropic") - - expect(mod).toContain("node_modules/zod") - expect(await pkgExists("zod")).toBe(true) - const pkgJson = await readPkgJson() - expect(isExactVersion(pkgJson.dependencies?.zod)).toBe(true) + delete process.env.OPENCODE_TEST_CACHE }) - test("should update tracking when provider switches packages", async () => { - const { BunProc } = await import("../src/bun") - - await BunProc.install("zod", "latest", "anthropic") - const pkgJson1 = await readPkgJson() - expect(pkgJson1.opencode?.providers?.anthropic).toBe("zod") - expect(isExactVersion(pkgJson1.dependencies?.zod)).toBe(true) + test("updates tracking and removes old package when provider switches", async () => { + await using tmp = await tmpdir() + process.env.OPENCODE_TEST_CACHE = tmp.path - await BunProc.install("superstruct", "latest", "anthropic") - const pkgJson2 = await readPkgJson() - expect(pkgJson2.opencode?.providers?.anthropic).toBe("superstruct") - expect(isExactVersion(pkgJson2.dependencies?.superstruct)).toBe(true) - }) - - test("should remove old package when provider switches", async () => { const { BunProc } = await import("../src/bun") await BunProc.install("zod", "latest", "anthropic") - expect(await pkgExists("zod")).toBe(true) + expect(await Bun.file(path.join(tmp.path, "node_modules", "zod", "package.json")).exists()).toBe(true) await BunProc.install("superstruct", "latest", "anthropic") - expect(await pkgExists("zod")).toBe(false) - expect(await pkgExists("superstruct")).toBe(true) + expect(await Bun.file(path.join(tmp.path, "node_modules", "zod", "package.json")).exists()).toBe(false) + expect(await Bun.file(path.join(tmp.path, "node_modules", "superstruct", "package.json")).exists()).toBe(true) - const pkgJson = await readPkgJson() + const pkgJson = await Bun.file(path.join(tmp.path, "package.json")).json() + expect(pkgJson.opencode?.providers?.anthropic).toBe("superstruct") expect(pkgJson.dependencies?.zod).toBeUndefined() expect(isExactVersion(pkgJson.dependencies?.superstruct)).toBe(true) + + delete process.env.OPENCODE_TEST_CACHE }) - test("should remove old package even when new package is already cached", async () => { + test("removes old package even when new package is already cached", async () => { + await using tmp = await tmpdir() + process.env.OPENCODE_TEST_CACHE = tmp.path + const { BunProc } = await import("../src/bun") await BunProc.install("zod", "latest", "provider-a") - expect(await pkgExists("zod")).toBe(true) - await BunProc.install("superstruct", "latest", "provider-b") - expect(await pkgExists("superstruct")).toBe(true) - await BunProc.install("superstruct", "latest", "provider-a") - expect(await pkgExists("zod")).toBe(false) + expect(await Bun.file(path.join(tmp.path, "node_modules", "zod", "package.json")).exists()).toBe(false) - const pkgJson = await readPkgJson() + const pkgJson = await Bun.file(path.join(tmp.path, "package.json")).json() expect(pkgJson.opencode?.providers?.["provider-a"]).toBe("superstruct") expect(pkgJson.opencode?.providers?.["provider-b"]).toBe("superstruct") expect(pkgJson.dependencies?.zod).toBeUndefined() - expect(isExactVersion(pkgJson.dependencies?.superstruct)).toBe(true) + + delete process.env.OPENCODE_TEST_CACHE }) - test("should not remove package if provider is not switching", async () => { + test("does not remove package if provider is not switching", async () => { + await using tmp = await tmpdir() + process.env.OPENCODE_TEST_CACHE = tmp.path + const { BunProc } = await import("../src/bun") await BunProc.install("zod", "latest", "anthropic") await BunProc.install("zod", "latest", "anthropic") - expect(await pkgExists("zod")).toBe(true) + expect(await Bun.file(path.join(tmp.path, "node_modules", "zod", "package.json")).exists()).toBe(true) + + delete process.env.OPENCODE_TEST_CACHE }) - test("should work without providerID (backward compatible)", async () => { + test("works without providerID (backward compatible)", async () => { + await using tmp = await tmpdir() + process.env.OPENCODE_TEST_CACHE = tmp.path + const { BunProc } = await import("../src/bun") await BunProc.install("zod", "latest") - expect(await pkgExists("zod")).toBe(true) - const pkgJson = await readPkgJson() + expect(await Bun.file(path.join(tmp.path, "node_modules", "zod", "package.json")).exists()).toBe(true) + const pkgJson = await Bun.file(path.join(tmp.path, "package.json")).json() expect(pkgJson.opencode?.providers).toBeUndefined() expect(isExactVersion(pkgJson.dependencies?.zod)).toBe(true) + + delete process.env.OPENCODE_TEST_CACHE }) - test("should track multiple providers independently", async () => { + test("tracks multiple providers independently", async () => { + await using tmp = await tmpdir() + process.env.OPENCODE_TEST_CACHE = tmp.path + const { BunProc } = await import("../src/bun") await BunProc.install("zod", "latest", "anthropic") await BunProc.install("superstruct", "latest", "openai") - const pkgJson = await readPkgJson() + const pkgJson = await Bun.file(path.join(tmp.path, "package.json")).json() expect(pkgJson.opencode?.providers?.anthropic).toBe("zod") expect(pkgJson.opencode?.providers?.openai).toBe("superstruct") + expect(await Bun.file(path.join(tmp.path, "node_modules", "zod", "package.json")).exists()).toBe(true) + expect(await Bun.file(path.join(tmp.path, "node_modules", "superstruct", "package.json")).exists()).toBe(true) - expect(await pkgExists("zod")).toBe(true) - expect(await pkgExists("superstruct")).toBe(true) - expect(isExactVersion(pkgJson.dependencies?.zod)).toBe(true) - expect(isExactVersion(pkgJson.dependencies?.superstruct)).toBe(true) + delete process.env.OPENCODE_TEST_CACHE }) - test("should not remove package if another provider still uses it", async () => { + test("does not remove package if another provider still uses it", async () => { + await using tmp = await tmpdir() + process.env.OPENCODE_TEST_CACHE = tmp.path + const { BunProc } = await import("../src/bun") await BunProc.install("zod", "latest", "anthropic") await BunProc.install("zod", "latest", "openai") await BunProc.install("superstruct", "latest", "anthropic") - expect(await pkgExists("zod")).toBe(true) - expect(await pkgExists("superstruct")).toBe(true) + expect(await Bun.file(path.join(tmp.path, "node_modules", "zod", "package.json")).exists()).toBe(true) + expect(await Bun.file(path.join(tmp.path, "node_modules", "superstruct", "package.json")).exists()).toBe(true) - const pkgJson = await readPkgJson() + const pkgJson = await Bun.file(path.join(tmp.path, "package.json")).json() expect(pkgJson.opencode?.providers?.anthropic).toBe("superstruct") expect(pkgJson.opencode?.providers?.openai).toBe("zod") - expect(isExactVersion(pkgJson.dependencies?.zod)).toBe(true) - expect(isExactVersion(pkgJson.dependencies?.superstruct)).toBe(true) + + delete process.env.OPENCODE_TEST_CACHE }) - test("should work when package.json exists without opencode section", async () => { - const { BunProc } = await import("../src/bun") + test("works with partial package.json structures", async () => { + await using tmp = await tmpdir() + process.env.OPENCODE_TEST_CACHE = tmp.path - await fs.writeFile(path.join(tempDir, "package.json"), JSON.stringify({ dependencies: {} }, null, 2)) + const { BunProc } = await import("../src/bun") + // Test with missing opencode section + await Bun.write(path.join(tmp.path, "package.json"), JSON.stringify({ dependencies: {} }, null, 2)) await BunProc.install("zod", "latest", "anthropic") - - const pkgJson = await readPkgJson() + let pkgJson = await Bun.file(path.join(tmp.path, "package.json")).json() expect(pkgJson.opencode?.providers?.anthropic).toBe("zod") - expect(await pkgExists("zod")).toBe(true) - expect(isExactVersion(pkgJson.dependencies?.zod)).toBe(true) + + // Reset and test with opencode but missing providers + const { rm } = await import("fs/promises") + await rm(path.join(tmp.path, "node_modules"), { recursive: true, force: true }) + await Bun.write(path.join(tmp.path, "package.json"), JSON.stringify({ dependencies: {}, opencode: {} }, null, 2)) + await BunProc.install("superstruct", "latest", "openai") + pkgJson = await Bun.file(path.join(tmp.path, "package.json")).json() + expect(pkgJson.opencode?.providers?.openai).toBe("superstruct") + + delete process.env.OPENCODE_TEST_CACHE }) - test("should work when opencode section exists without providers", async () => { + test("installs exact version when specific version provided", async () => { + await using tmp = await tmpdir() + process.env.OPENCODE_TEST_CACHE = tmp.path + const { BunProc } = await import("../src/bun") - await fs.writeFile(path.join(tempDir, "package.json"), JSON.stringify({ dependencies: {}, opencode: {} }, null, 2)) + await BunProc.install("zod", "3.23.0", "anthropic") - await BunProc.install("zod", "latest", "anthropic") + const pkgJson = await Bun.file(path.join(tmp.path, "package.json")).json() + expect(pkgJson.dependencies?.zod).toBe("3.23.0") - const pkgJson = await readPkgJson() - expect(pkgJson.opencode?.providers?.anthropic).toBe("zod") - expect(await pkgExists("zod")).toBe(true) - expect(isExactVersion(pkgJson.dependencies?.zod)).toBe(true) + delete process.env.OPENCODE_TEST_CACHE }) - test("should install exact version when specific version provided", async () => { + test("reinstalls when requested version differs from cached", async () => { + await using tmp = await tmpdir() + process.env.OPENCODE_TEST_CACHE = tmp.path + const { BunProc } = await import("../src/bun") await BunProc.install("zod", "3.23.0", "anthropic") - - const pkgJson = await readPkgJson() + let pkgJson = await Bun.file(path.join(tmp.path, "package.json")).json() expect(pkgJson.dependencies?.zod).toBe("3.23.0") - expect(await pkgExists("zod")).toBe(true) + + await BunProc.install("zod", "3.24.0", "anthropic") + pkgJson = await Bun.file(path.join(tmp.path, "package.json")).json() + expect(pkgJson.dependencies?.zod).toBe("3.24.0") + + delete process.env.OPENCODE_TEST_CACHE + }) + + test("skips install when cached version matches requested", async () => { + await using tmp = await tmpdir() + process.env.OPENCODE_TEST_CACHE = tmp.path + + const { BunProc } = await import("../src/bun") + + await BunProc.install("zod", "3.23.0", "anthropic") + const pkgJson1 = await Bun.file(path.join(tmp.path, "package.json")).json() + + await new Promise((r) => setTimeout(r, 50)) + + await BunProc.install("zod", "3.23.0", "anthropic") + const pkgJson2 = await Bun.file(path.join(tmp.path, "package.json")).json() + + expect(pkgJson1.dependencies?.zod).toBe(pkgJson2.dependencies?.zod) + expect(pkgJson2.dependencies?.zod).toBe("3.23.0") + + delete process.env.OPENCODE_TEST_CACHE }) }) From 5347a21046d8c1f5c3b783a274747803e9c694c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Wed, 4 Feb 2026 20:51:13 +0100 Subject: [PATCH 10/11] fix(bun): reference counting and decision tree coverage --- packages/opencode/src/bun/index.ts | 89 +++++---- packages/opencode/src/bun/registry.ts | 4 +- packages/opencode/test/bun.test.ts | 262 +++++++++++++++++--------- 3 files changed, 228 insertions(+), 127 deletions(-) diff --git a/packages/opencode/src/bun/index.ts b/packages/opencode/src/bun/index.ts index 52df88ccc02..d327d1ded52 100644 --- a/packages/opencode/src/bun/index.ts +++ b/packages/opencode/src/bun/index.ts @@ -95,52 +95,73 @@ export namespace BunProc { await BunProc.run(["remove", "--cwd", Global.Path.cache, oldPkg]).catch(() => {}) } + async function resolveVersion(mod: string, version: string) { + if (version !== "latest") return version + const file = Bun.file(path.join(mod, "package.json")) + const pkg = await file.json().catch(() => null) + return pkg?.version ?? version + } + + async function finalize(provider: string | undefined, pkg: string, oldPkg: string | undefined) { + if (provider) await track(provider, pkg) + if (oldPkg && oldPkg !== pkg) await cleanup(provider!, oldPkg) + } + export async function install(pkg: string, version = "latest", provider?: string) { using _ = await Lock.write("bun-install") const mod = path.join(Global.Path.cache, "node_modules", pkg) - const parsed = await readPackageJson() - const oldPkg = provider ? parsed.opencode?.providers?.[provider] : undefined - const switched = oldPkg && oldPkg !== pkg - const dependencies = parsed.dependencies ?? {} - const modExists = await Filesystem.exists(mod) - const cachedVersion = dependencies[pkg] + const state = await readPackageJson() + const cached = state.dependencies?.[pkg] + const oldPkg = provider ? state.opencode?.providers?.[provider] : undefined - const earlyReturn = async () => { - if (provider) await track(provider, pkg) - if (switched) await cleanup(provider!, oldPkg!) - return mod - } - - // Skip install if exact version already cached (always reinstall with "latest") - const installed = parsed.dependencies?.[pkg] - if (installed && version !== "latest" && installed === version && (await Filesystem.exists(mod))) { - return earlyReturn() - } else if (version === "latest") { - const isOutdated = await PackageRegistry.isOutdated(pkg, cachedVersion, Global.Path.cache) - if (!isOutdated) return earlyReturn() - log.info("Cached version is outdated, proceeding with install", { pkg, cachedVersion }) + // Check if we can skip installation + const modExists = await Filesystem.exists(mod) + if (version !== "latest") { + if (cached === version && modExists) { + await finalize(provider, pkg, oldPkg) + return mod + } + } else { + const outdated = await PackageRegistry.isOutdated(pkg, cached, Global.Path.cache) + if (!outdated && modExists) { + await finalize(provider, pkg, oldPkg) + return mod + } + if (outdated) log.info("cached version is outdated", { pkg, cached }) } - const args = [ - "add", - "--force", - "--exact", - // TODO: get rid of this case (see: https://github.com/oven-sh/bun/issues/19936) - ...(proxied() ? ["--no-cache"] : []), - "--cwd", - Global.Path.cache, - pkg + "@" + version, - ] - log.info("installing package", { pkg, version }) - await BunProc.run(args, { cwd: Global.Path.cache }).catch((e) => { + await BunProc.run( + [ + "add", + "--force", + "--exact", + // TODO: get rid of this case (see: https://github.com/oven-sh/bun/issues/19936) + ...(proxied() ? ["--no-cache"] : []), + "--cwd", + Global.Path.cache, + `${pkg}@${version}`, + ], + { cwd: Global.Path.cache }, + ).catch((e) => { throw new InstallFailedError({ pkg, version }, { cause: e }) }) - if (provider) await track(provider, pkg) - if (switched) await cleanup(provider!, oldPkg!) + // Persist resolved version and provider tracking + const resolved = await resolveVersion(mod, version) + const updated = await readPackageJson() + if (!updated.dependencies) updated.dependencies = {} + updated.dependencies[pkg] = resolved + if (provider) { + if (!updated.opencode) updated.opencode = {} + if (!updated.opencode.providers) updated.opencode.providers = {} + updated.opencode.providers[provider] = pkg + } + await writePackageJson(updated) + + if (oldPkg && oldPkg !== pkg) await cleanup(provider!, oldPkg) return mod } } diff --git a/packages/opencode/src/bun/registry.ts b/packages/opencode/src/bun/registry.ts index c567668acd7..dcce454f58a 100644 --- a/packages/opencode/src/bun/registry.ts +++ b/packages/opencode/src/bun/registry.ts @@ -33,7 +33,9 @@ export namespace PackageRegistry { return value } - export async function isOutdated(pkg: string, cachedVersion: string, cwd?: string): Promise { + export async function isOutdated(pkg: string, cachedVersion: string | undefined, cwd?: string): Promise { + if (!cachedVersion) return true + const latestVersion = await info(pkg, "version", cwd) if (!latestVersion) { log.warn("Failed to resolve latest version, using cached", { pkg, cachedVersion }) diff --git a/packages/opencode/test/bun.test.ts b/packages/opencode/test/bun.test.ts index 3138eb9b014..15dce966434 100644 --- a/packages/opencode/test/bun.test.ts +++ b/packages/opencode/test/bun.test.ts @@ -1,18 +1,14 @@ import { describe, expect, test } from "bun:test" +import * as fs from "fs/promises" import path from "path" import { tmpdir } from "./fixture/fixture" const SEMVER_REGEX = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/ -function isExactVersion(v: string) { - return typeof v === "string" && SEMVER_REGEX.test(v) -} - -describe("BunProc", () => { - test("install function has correct bun add command structure", async () => { +describe("BunProc.install - command structure", () => { + test("uses correct bun add flags", async () => { const content = await Bun.file(path.join(__dirname, "../src/bun/index.ts")).text() - const match = content.match(/export async function install[\s\S]*?^ }/m) expect(match).toBeTruthy() @@ -23,207 +19,289 @@ describe("BunProc", () => { expect(fn).toContain('"--cwd"') expect(fn).not.toContain('"--registry"') expect(content).not.toContain("--registry=") - expect(content).not.toContain("hasNpmRcConfig") }) - test("installs package, tracks provider, and returns module path", async () => { + test("throws on nonexistent package", async () => { await using tmp = await tmpdir() process.env.OPENCODE_TEST_CACHE = tmp.path + const { BunProc } = await import("../src/bun") + + await expect(BunProc.install("@nonexistent-pkg-xyz/does-not-exist", "1.0.0")).rejects.toThrow() + + delete process.env.OPENCODE_TEST_CACHE + }) +}) +describe("BunProc.install - version=latest", () => { + test("installs and tracks provider", async () => { + await using tmp = await tmpdir() + process.env.OPENCODE_TEST_CACHE = tmp.path const { BunProc } = await import("../src/bun") + const mod = await BunProc.install("zod", "latest", "anthropic") expect(mod).toContain("node_modules/zod") expect(await Bun.file(path.join(tmp.path, "node_modules", "zod", "package.json")).exists()).toBe(true) - const pkgJson = await Bun.file(path.join(tmp.path, "package.json")).json() - expect(pkgJson.opencode?.providers?.anthropic).toBe("zod") - expect(isExactVersion(pkgJson.dependencies?.zod)).toBe(true) + const pkg = await Bun.file(path.join(tmp.path, "package.json")).json() + expect(pkg.opencode?.providers?.anthropic).toBe("zod") + expect(SEMVER_REGEX.test(pkg.dependencies?.zod)).toBe(true) delete process.env.OPENCODE_TEST_CACHE }) - test("updates tracking and removes old package when provider switches", async () => { + test("skips when not outdated", async () => { await using tmp = await tmpdir() process.env.OPENCODE_TEST_CACHE = tmp.path - const { BunProc } = await import("../src/bun") await BunProc.install("zod", "latest", "anthropic") + await BunProc.install("zod", "latest", "anthropic") + expect(await Bun.file(path.join(tmp.path, "node_modules", "zod", "package.json")).exists()).toBe(true) - await BunProc.install("superstruct", "latest", "anthropic") + delete process.env.OPENCODE_TEST_CACHE + }) - expect(await Bun.file(path.join(tmp.path, "node_modules", "zod", "package.json")).exists()).toBe(false) - expect(await Bun.file(path.join(tmp.path, "node_modules", "superstruct", "package.json")).exists()).toBe(true) + test("reinstalls when node_modules missing", async () => { + await using tmp = await tmpdir() + process.env.OPENCODE_TEST_CACHE = tmp.path + const { BunProc } = await import("../src/bun") + + await BunProc.install("zod", "latest", "anthropic") + await fs.rm(path.join(tmp.path, "node_modules"), { recursive: true, force: true }) - const pkgJson = await Bun.file(path.join(tmp.path, "package.json")).json() - expect(pkgJson.opencode?.providers?.anthropic).toBe("superstruct") - expect(pkgJson.dependencies?.zod).toBeUndefined() - expect(isExactVersion(pkgJson.dependencies?.superstruct)).toBe(true) + await BunProc.install("zod", "latest", "anthropic") + + expect(await Bun.file(path.join(tmp.path, "node_modules", "zod", "package.json")).exists()).toBe(true) delete process.env.OPENCODE_TEST_CACHE }) - test("removes old package even when new package is already cached", async () => { + test("reinstalls when outdated", async () => { await using tmp = await tmpdir() process.env.OPENCODE_TEST_CACHE = tmp.path - const { BunProc } = await import("../src/bun") - await BunProc.install("zod", "latest", "provider-a") - await BunProc.install("superstruct", "latest", "provider-b") - await BunProc.install("superstruct", "latest", "provider-a") + await fs.mkdir(path.join(tmp.path, "node_modules", "zod"), { recursive: true }) + await Bun.write(path.join(tmp.path, "node_modules", "zod", "package.json"), JSON.stringify({ version: "3.20.0" })) + await Bun.write(path.join(tmp.path, "package.json"), JSON.stringify({ dependencies: { zod: "3.20.0" } })) - expect(await Bun.file(path.join(tmp.path, "node_modules", "zod", "package.json")).exists()).toBe(false) + await BunProc.install("zod", "latest", "anthropic") - const pkgJson = await Bun.file(path.join(tmp.path, "package.json")).json() - expect(pkgJson.opencode?.providers?.["provider-a"]).toBe("superstruct") - expect(pkgJson.opencode?.providers?.["provider-b"]).toBe("superstruct") - expect(pkgJson.dependencies?.zod).toBeUndefined() + const pkg = await Bun.file(path.join(tmp.path, "package.json")).json() + expect(SEMVER_REGEX.test(pkg.dependencies?.zod)).toBe(true) + expect(pkg.dependencies?.zod).not.toBe("3.20.0") delete process.env.OPENCODE_TEST_CACHE }) - test("does not remove package if provider is not switching", async () => { + test("installs when stale node_modules (no cached)", async () => { await using tmp = await tmpdir() process.env.OPENCODE_TEST_CACHE = tmp.path - const { BunProc } = await import("../src/bun") - await BunProc.install("zod", "latest", "anthropic") + await fs.mkdir(path.join(tmp.path, "node_modules", "zod"), { recursive: true }) + await Bun.write(path.join(tmp.path, "node_modules", "zod", "package.json"), JSON.stringify({ version: "3.22.0" })) + await Bun.write(path.join(tmp.path, "package.json"), JSON.stringify({})) + await BunProc.install("zod", "latest", "anthropic") - expect(await Bun.file(path.join(tmp.path, "node_modules", "zod", "package.json")).exists()).toBe(true) + const pkg = await Bun.file(path.join(tmp.path, "package.json")).json() + expect(SEMVER_REGEX.test(pkg.dependencies?.zod)).toBe(true) delete process.env.OPENCODE_TEST_CACHE }) +}) - test("works without providerID (backward compatible)", async () => { +describe("BunProc.install - version=exact", () => { + test("installs specific version", async () => { await using tmp = await tmpdir() process.env.OPENCODE_TEST_CACHE = tmp.path + const { BunProc } = await import("../src/bun") + + await BunProc.install("zod", "3.23.0", "anthropic") + + const pkg = await Bun.file(path.join(tmp.path, "package.json")).json() + expect(pkg.dependencies?.zod).toBe("3.23.0") + + delete process.env.OPENCODE_TEST_CACHE + }) + test("skips when cached matches", async () => { + await using tmp = await tmpdir() + process.env.OPENCODE_TEST_CACHE = tmp.path const { BunProc } = await import("../src/bun") - await BunProc.install("zod", "latest") + await BunProc.install("zod", "3.23.0", "anthropic") + await BunProc.install("zod", "3.23.0", "anthropic") - expect(await Bun.file(path.join(tmp.path, "node_modules", "zod", "package.json")).exists()).toBe(true) - const pkgJson = await Bun.file(path.join(tmp.path, "package.json")).json() - expect(pkgJson.opencode?.providers).toBeUndefined() - expect(isExactVersion(pkgJson.dependencies?.zod)).toBe(true) + const pkg = await Bun.file(path.join(tmp.path, "package.json")).json() + expect(pkg.dependencies?.zod).toBe("3.23.0") delete process.env.OPENCODE_TEST_CACHE }) - test("tracks multiple providers independently", async () => { + test("reinstalls when version differs", async () => { await using tmp = await tmpdir() process.env.OPENCODE_TEST_CACHE = tmp.path + const { BunProc } = await import("../src/bun") + + await BunProc.install("zod", "3.23.0", "anthropic") + await BunProc.install("zod", "3.24.0", "anthropic") + const pkg = await Bun.file(path.join(tmp.path, "package.json")).json() + expect(pkg.dependencies?.zod).toBe("3.24.0") + + delete process.env.OPENCODE_TEST_CACHE + }) + + test("reinstalls when node_modules missing", async () => { + await using tmp = await tmpdir() + process.env.OPENCODE_TEST_CACHE = tmp.path const { BunProc } = await import("../src/bun") - await BunProc.install("zod", "latest", "anthropic") - await BunProc.install("superstruct", "latest", "openai") + await BunProc.install("zod", "3.23.0", "anthropic") + await fs.rm(path.join(tmp.path, "node_modules"), { recursive: true, force: true }) + + await BunProc.install("zod", "3.23.0", "anthropic") - const pkgJson = await Bun.file(path.join(tmp.path, "package.json")).json() - expect(pkgJson.opencode?.providers?.anthropic).toBe("zod") - expect(pkgJson.opencode?.providers?.openai).toBe("superstruct") expect(await Bun.file(path.join(tmp.path, "node_modules", "zod", "package.json")).exists()).toBe(true) - expect(await Bun.file(path.join(tmp.path, "node_modules", "superstruct", "package.json")).exists()).toBe(true) delete process.env.OPENCODE_TEST_CACHE }) - test("does not remove package if another provider still uses it", async () => { + test("installs when stale node_modules exists", async () => { await using tmp = await tmpdir() process.env.OPENCODE_TEST_CACHE = tmp.path - const { BunProc } = await import("../src/bun") - await BunProc.install("zod", "latest", "anthropic") - await BunProc.install("zod", "latest", "openai") - await BunProc.install("superstruct", "latest", "anthropic") + await fs.mkdir(path.join(tmp.path, "node_modules", "zod"), { recursive: true }) + await Bun.write(path.join(tmp.path, "node_modules", "zod", "package.json"), JSON.stringify({ version: "3.22.0" })) + await Bun.write(path.join(tmp.path, "package.json"), JSON.stringify({})) - expect(await Bun.file(path.join(tmp.path, "node_modules", "zod", "package.json")).exists()).toBe(true) - expect(await Bun.file(path.join(tmp.path, "node_modules", "superstruct", "package.json")).exists()).toBe(true) + await BunProc.install("zod", "3.23.0", "anthropic") - const pkgJson = await Bun.file(path.join(tmp.path, "package.json")).json() - expect(pkgJson.opencode?.providers?.anthropic).toBe("superstruct") - expect(pkgJson.opencode?.providers?.openai).toBe("zod") + const pkg = await Bun.file(path.join(tmp.path, "package.json")).json() + expect(pkg.dependencies?.zod).toBe("3.23.0") delete process.env.OPENCODE_TEST_CACHE }) +}) - test("works with partial package.json structures", async () => { +describe("BunProc.install - provider tracking", () => { + test("works without provider (backward compat)", async () => { await using tmp = await tmpdir() process.env.OPENCODE_TEST_CACHE = tmp.path + const { BunProc } = await import("../src/bun") + + await BunProc.install("zod", "latest") + + const pkg = await Bun.file(path.join(tmp.path, "package.json")).json() + expect(pkg.opencode?.providers).toBeUndefined() + expect(SEMVER_REGEX.test(pkg.dependencies?.zod)).toBe(true) + delete process.env.OPENCODE_TEST_CACHE + }) + + test("tracks multiple providers independently", async () => { + await using tmp = await tmpdir() + process.env.OPENCODE_TEST_CACHE = tmp.path const { BunProc } = await import("../src/bun") - // Test with missing opencode section - await Bun.write(path.join(tmp.path, "package.json"), JSON.stringify({ dependencies: {} }, null, 2)) await BunProc.install("zod", "latest", "anthropic") - let pkgJson = await Bun.file(path.join(tmp.path, "package.json")).json() - expect(pkgJson.opencode?.providers?.anthropic).toBe("zod") - - // Reset and test with opencode but missing providers - const { rm } = await import("fs/promises") - await rm(path.join(tmp.path, "node_modules"), { recursive: true, force: true }) - await Bun.write(path.join(tmp.path, "package.json"), JSON.stringify({ dependencies: {}, opencode: {} }, null, 2)) await BunProc.install("superstruct", "latest", "openai") - pkgJson = await Bun.file(path.join(tmp.path, "package.json")).json() - expect(pkgJson.opencode?.providers?.openai).toBe("superstruct") + + const pkg = await Bun.file(path.join(tmp.path, "package.json")).json() + expect(pkg.opencode?.providers?.anthropic).toBe("zod") + expect(pkg.opencode?.providers?.openai).toBe("superstruct") delete process.env.OPENCODE_TEST_CACHE }) - test("installs exact version when specific version provided", async () => { + test("handles partial package.json", async () => { await using tmp = await tmpdir() process.env.OPENCODE_TEST_CACHE = tmp.path - const { BunProc } = await import("../src/bun") - await BunProc.install("zod", "3.23.0", "anthropic") + await Bun.write(path.join(tmp.path, "package.json"), JSON.stringify({ dependencies: {} })) + await BunProc.install("zod", "latest", "anthropic") + + let pkg = await Bun.file(path.join(tmp.path, "package.json")).json() + expect(pkg.opencode?.providers?.anthropic).toBe("zod") - const pkgJson = await Bun.file(path.join(tmp.path, "package.json")).json() - expect(pkgJson.dependencies?.zod).toBe("3.23.0") + await fs.rm(path.join(tmp.path, "node_modules"), { recursive: true, force: true }) + await Bun.write(path.join(tmp.path, "package.json"), JSON.stringify({ opencode: {} })) + await BunProc.install("superstruct", "latest", "openai") + + pkg = await Bun.file(path.join(tmp.path, "package.json")).json() + expect(pkg.opencode?.providers?.openai).toBe("superstruct") delete process.env.OPENCODE_TEST_CACHE }) +}) - test("reinstalls when requested version differs from cached", async () => { +describe("BunProc.install - cleanup (reference counting)", () => { + test("removes old package on switch", async () => { await using tmp = await tmpdir() process.env.OPENCODE_TEST_CACHE = tmp.path - const { BunProc } = await import("../src/bun") - await BunProc.install("zod", "3.23.0", "anthropic") - let pkgJson = await Bun.file(path.join(tmp.path, "package.json")).json() - expect(pkgJson.dependencies?.zod).toBe("3.23.0") + await BunProc.install("zod", "latest", "anthropic") + await BunProc.install("superstruct", "latest", "anthropic") - await BunProc.install("zod", "3.24.0", "anthropic") - pkgJson = await Bun.file(path.join(tmp.path, "package.json")).json() - expect(pkgJson.dependencies?.zod).toBe("3.24.0") + expect(await Bun.file(path.join(tmp.path, "node_modules", "zod", "package.json")).exists()).toBe(false) + expect(await Bun.file(path.join(tmp.path, "node_modules", "superstruct", "package.json")).exists()).toBe(true) + + const pkg = await Bun.file(path.join(tmp.path, "package.json")).json() + expect(pkg.opencode?.providers?.anthropic).toBe("superstruct") + expect(pkg.dependencies?.zod).toBeUndefined() delete process.env.OPENCODE_TEST_CACHE }) - test("skips install when cached version matches requested", async () => { + test("removes old package when new already cached", async () => { await using tmp = await tmpdir() process.env.OPENCODE_TEST_CACHE = tmp.path - const { BunProc } = await import("../src/bun") - await BunProc.install("zod", "3.23.0", "anthropic") - const pkgJson1 = await Bun.file(path.join(tmp.path, "package.json")).json() + await BunProc.install("zod", "latest", "provider-a") + await BunProc.install("superstruct", "latest", "provider-b") + await BunProc.install("superstruct", "latest", "provider-a") - await new Promise((r) => setTimeout(r, 50)) + expect(await Bun.file(path.join(tmp.path, "node_modules", "zod", "package.json")).exists()).toBe(false) - await BunProc.install("zod", "3.23.0", "anthropic") - const pkgJson2 = await Bun.file(path.join(tmp.path, "package.json")).json() + const pkg = await Bun.file(path.join(tmp.path, "package.json")).json() + expect(pkg.opencode?.providers?.["provider-a"]).toBe("superstruct") + expect(pkg.opencode?.providers?.["provider-b"]).toBe("superstruct") - expect(pkgJson1.dependencies?.zod).toBe(pkgJson2.dependencies?.zod) - expect(pkgJson2.dependencies?.zod).toBe("3.23.0") + delete process.env.OPENCODE_TEST_CACHE + }) + + test("keeps package if another provider uses it", async () => { + await using tmp = await tmpdir() + process.env.OPENCODE_TEST_CACHE = tmp.path + const { BunProc } = await import("../src/bun") + + await BunProc.install("zod", "latest", "anthropic") + await BunProc.install("zod", "latest", "openai") + await BunProc.install("superstruct", "latest", "anthropic") + + expect(await Bun.file(path.join(tmp.path, "node_modules", "zod", "package.json")).exists()).toBe(true) + + const pkg = await Bun.file(path.join(tmp.path, "package.json")).json() + expect(pkg.opencode?.providers?.anthropic).toBe("superstruct") + expect(pkg.opencode?.providers?.openai).toBe("zod") delete process.env.OPENCODE_TEST_CACHE }) }) + +describe("PackageRegistry.isOutdated", () => { + test("returns false when registry unavailable", async () => { + const { PackageRegistry } = await import("../src/bun/registry") + + const result = await PackageRegistry.isOutdated("@nonexistent-pkg-xyz/does-not-exist", "1.0.0") + expect(result).toBe(false) + }) +}) From 82e20eb2464f073243de365b52807c54ec3b2387 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Thu, 5 Feb 2026 17:06:59 +0100 Subject: [PATCH 11/11] test(bun): extend coverage for install edge cases and isOutdated --- packages/opencode/test/bun.test.ts | 63 ++++++++++++++++++++++++++++-- 1 file changed, 59 insertions(+), 4 deletions(-) diff --git a/packages/opencode/test/bun.test.ts b/packages/opencode/test/bun.test.ts index 15dce966434..5d8432b1284 100644 --- a/packages/opencode/test/bun.test.ts +++ b/packages/opencode/test/bun.test.ts @@ -21,12 +21,19 @@ describe("BunProc.install - command structure", () => { expect(content).not.toContain("--registry=") }) - test("throws on nonexistent package", async () => { + test("throws on nonexistent package without corrupting state", async () => { await using tmp = await tmpdir() process.env.OPENCODE_TEST_CACHE = tmp.path const { BunProc } = await import("../src/bun") - await expect(BunProc.install("@nonexistent-pkg-xyz/does-not-exist", "1.0.0")).rejects.toThrow() + await Bun.write(path.join(tmp.path, "package.json"), JSON.stringify({ dependencies: { zod: "3.23.0" } })) + + await expect(BunProc.install("@nonexistent-pkg-xyz/does-not-exist", "1.0.0", "test")).rejects.toThrow() + + const pkg = await Bun.file(path.join(tmp.path, "package.json")).json() + expect(pkg.dependencies?.zod).toBe("3.23.0") + expect(pkg.dependencies?.["@nonexistent-pkg-xyz/does-not-exist"]).toBeUndefined() + expect(pkg.opencode?.providers?.test).toBeUndefined() delete process.env.OPENCODE_TEST_CACHE }) @@ -50,16 +57,21 @@ describe("BunProc.install - version=latest", () => { delete process.env.OPENCODE_TEST_CACHE }) - test("skips when not outdated", async () => { + test("skips when not outdated and updates provider tracking", async () => { await using tmp = await tmpdir() process.env.OPENCODE_TEST_CACHE = tmp.path const { BunProc } = await import("../src/bun") await BunProc.install("zod", "latest", "anthropic") await BunProc.install("zod", "latest", "anthropic") + await BunProc.install("zod", "latest", "openai") expect(await Bun.file(path.join(tmp.path, "node_modules", "zod", "package.json")).exists()).toBe(true) + const pkg = await Bun.file(path.join(tmp.path, "package.json")).json() + expect(pkg.opencode?.providers?.anthropic).toBe("zod") + expect(pkg.opencode?.providers?.openai).toBe("zod") + delete process.env.OPENCODE_TEST_CACHE }) @@ -128,16 +140,19 @@ describe("BunProc.install - version=exact", () => { delete process.env.OPENCODE_TEST_CACHE }) - test("skips when cached matches", async () => { + test("skips when cached matches and updates provider tracking", async () => { await using tmp = await tmpdir() process.env.OPENCODE_TEST_CACHE = tmp.path const { BunProc } = await import("../src/bun") await BunProc.install("zod", "3.23.0", "anthropic") await BunProc.install("zod", "3.23.0", "anthropic") + await BunProc.install("zod", "3.23.0", "openai") const pkg = await Bun.file(path.join(tmp.path, "package.json")).json() expect(pkg.dependencies?.zod).toBe("3.23.0") + expect(pkg.opencode?.providers?.anthropic).toBe("zod") + expect(pkg.opencode?.providers?.openai).toBe("zod") delete process.env.OPENCODE_TEST_CACHE }) @@ -187,6 +202,22 @@ describe("BunProc.install - version=exact", () => { delete process.env.OPENCODE_TEST_CACHE }) + + test("reinstalls when cached differs and node_modules missing", async () => { + await using tmp = await tmpdir() + process.env.OPENCODE_TEST_CACHE = tmp.path + const { BunProc } = await import("../src/bun") + + await Bun.write(path.join(tmp.path, "package.json"), JSON.stringify({ dependencies: { zod: "3.22.0" } })) + + await BunProc.install("zod", "3.23.0", "anthropic") + + expect(await Bun.file(path.join(tmp.path, "node_modules", "zod", "package.json")).exists()).toBe(true) + const pkg = await Bun.file(path.join(tmp.path, "package.json")).json() + expect(pkg.dependencies?.zod).toBe("3.23.0") + + delete process.env.OPENCODE_TEST_CACHE + }) }) describe("BunProc.install - provider tracking", () => { @@ -298,10 +329,34 @@ describe("BunProc.install - cleanup (reference counting)", () => { }) describe("PackageRegistry.isOutdated", () => { + test("returns true when cachedVersion undefined", async () => { + const { PackageRegistry } = await import("../src/bun/registry") + + const result = await PackageRegistry.isOutdated("zod", undefined) + expect(result).toBe(true) + }) + test("returns false when registry unavailable", async () => { const { PackageRegistry } = await import("../src/bun/registry") const result = await PackageRegistry.isOutdated("@nonexistent-pkg-xyz/does-not-exist", "1.0.0") expect(result).toBe(false) }) + + test("returns true when cached is older", async () => { + const { PackageRegistry } = await import("../src/bun/registry") + + const result = await PackageRegistry.isOutdated("zod", "3.20.0") + expect(result).toBe(true) + }) + + test("returns false when cached matches latest", async () => { + const { PackageRegistry } = await import("../src/bun/registry") + + const latest = await PackageRegistry.info("zod", "version") + expect(latest).toBeTruthy() + + const result = await PackageRegistry.isOutdated("zod", latest!) + expect(result).toBe(false) + }) })