diff --git a/packages/opencode/src/bun/index.ts b/packages/opencode/src/bun/index.ts index bdb7cff78e2..d327d1ded52 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,77 +68,100 @@ 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) + } + + 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(() => {}) + } + + 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 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 state = await readPackageJson() + const cached = state.dependencies?.[pkg] + const oldPkg = provider ? state.opencode?.providers?.[provider] : undefined + + // Check if we can skip installation const modExists = await Filesystem.exists(mod) - const cachedVersion = dependencies[pkg] - - if (!modExists || !cachedVersion) { - // continue to install - } else if (version !== "latest" && cachedVersion === version) { - return mod - } else if (version === "latest") { - const isOutdated = await PackageRegistry.isOutdated(pkg, cachedVersion, Global.Path.cache) - if (!isOutdated) return mod - log.info("Cached version is outdated, proceeding with install", { pkg, cachedVersion }) + 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 }) } - // Build command arguments - 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, - ] - - // 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, - }, - ) + 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 }) }) - // 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 - } + // 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) - parsed.dependencies[pkg] = resolvedVersion - await Bun.write(pkgjson.name!, JSON.stringify(parsed, null, 2)) + 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/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..5d8432b1284 100644 --- a/packages/opencode/test/bun.test.ts +++ b/packages/opencode/test/bun.test.ts @@ -1,53 +1,362 @@ import { describe, expect, test } from "bun:test" -import fs from "fs/promises" +import * as fs from "fs/promises" import path from "path" +import { tmpdir } from "./fixture/fixture" -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") +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-]+)*))?$/ - // Verify that no hardcoded registry is present +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() + + 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") - expect(content).not.toContain("NpmRc") }) - test("should use Bun's default registry resolution", async () => { - // Read the bun/index.ts file - const bunIndexPath = path.join(__dirname, "../src/bun/index.ts") - const content = await fs.readFile(bunIndexPath, "utf-8") + 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 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 + }) +}) + +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 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("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 + }) + + 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 }) + + 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("reinstalls when outdated", async () => { + await using tmp = await tmpdir() + process.env.OPENCODE_TEST_CACHE = tmp.path + const { BunProc } = await import("../src/bun") + + 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" } })) + + await BunProc.install("zod", "latest", "anthropic") + + 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("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 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") + + 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 + }) +}) + +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 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 + }) + + 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", "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") + + expect(await Bun.file(path.join(tmp.path, "node_modules", "zod", "package.json")).exists()).toBe(true) + + delete process.env.OPENCODE_TEST_CACHE + }) + + 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 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({})) - // 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") + 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("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") + 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", () => { + 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) - // Extract the install function - const installFunctionMatch = content.match(/export async function install[\s\S]*?^ }/m) - expect(installFunctionMatch).toBeTruthy() + 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") + + await BunProc.install("zod", "latest", "anthropic") + await BunProc.install("superstruct", "latest", "openai") + + 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("handles partial package.json", 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: {} })) + 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") + + 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 + }) +}) + +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", "latest", "anthropic") + await BunProc.install("superstruct", "latest", "anthropic") + + 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("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", "latest", "provider-a") + await BunProc.install("superstruct", "latest", "provider-b") + await BunProc.install("superstruct", "latest", "provider-a") + + expect(await Bun.file(path.join(tmp.path, "node_modules", "zod", "package.json")).exists()).toBe(false) + + 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") + + 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 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) + }) - if (installFunctionMatch) { - const installFunction = installFunctionMatch[0] + test("returns false when cached matches latest", async () => { + const { PackageRegistry } = await import("../src/bun/registry") - // 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') + const latest = await PackageRegistry.info("zod", "version") + expect(latest).toBeTruthy() - // Verify no registry argument is added - expect(installFunction).not.toContain('"--registry"') - expect(installFunction).not.toContain('args.push("--registry') - } + const result = await PackageRegistry.isOutdated("zod", latest!) + expect(result).toBe(false) }) })