import { describe, it, expect, spyOn, beforeEach, afterEach } from "bun:test" import * as opencode from "./opencode" import { MIN_OPENCODE_VERSION } from "../constants" describe("opencode check", () => { describe("compareVersions", () => { it("returns true when current >= minimum", () => { // given versions where current is greater // when comparing // then should return true expect(opencode.compareVersions("1.0.200", "1.0.150")).toBe(true) expect(opencode.compareVersions("1.1.0", "1.0.150")).toBe(true) expect(opencode.compareVersions("2.0.0", "1.0.150")).toBe(true) }) it("returns true when versions are equal", () => { // given equal versions // when comparing // then should return true expect(opencode.compareVersions("1.0.150", "1.0.150")).toBe(true) }) it("returns false when current < minimum", () => { // given version below minimum // when comparing // then should return false expect(opencode.compareVersions("1.0.100", "1.0.150")).toBe(false) expect(opencode.compareVersions("0.9.0", "1.0.150")).toBe(false) }) it("handles version prefixes", () => { // given version with v prefix // when comparing // then should strip prefix and compare correctly expect(opencode.compareVersions("v1.0.200", "1.0.150")).toBe(true) }) it("handles prerelease versions", () => { // given prerelease version // when comparing // then should use base version expect(opencode.compareVersions("1.0.200-beta.1", "1.0.150")).toBe(true) }) }) describe("command helpers", () => { it("selects where on Windows", () => { // given win32 platform // when selecting lookup command // then should use where expect(opencode.getBinaryLookupCommand("win32")).toBe("where") }) it("selects which on non-Windows", () => { // given linux platform // when selecting lookup command // then should use which expect(opencode.getBinaryLookupCommand("linux")).toBe("which") expect(opencode.getBinaryLookupCommand("darwin")).toBe("which") }) it("parses command output into paths", () => { // given raw output with multiple lines and spaces const output = "C:\\\\bin\\\\opencode.ps1\r\nC:\\\\bin\\\\opencode.exe\n\n" // when parsing const paths = opencode.parseBinaryPaths(output) // then should return trimmed, non-empty paths expect(paths).toEqual(["C:\\\\bin\\\\opencode.ps1", "C:\\\\bin\\\\opencode.exe"]) }) it("prefers exe/cmd/bat over ps1 on Windows", () => { // given windows paths const paths = [ "C:\\\\bin\\\\opencode.ps1", "C:\\\\bin\\\\opencode.cmd", "C:\\\\bin\\\\opencode.exe", ] // when selecting binary const selected = opencode.selectBinaryPath(paths, "win32") // then should prefer exe expect(selected).toBe("C:\\\\bin\\\\opencode.exe") }) it("falls back to ps1 when it is the only Windows candidate", () => { // given only ps1 path const paths = ["C:\\\\bin\\\\opencode.ps1"] // when selecting binary const selected = opencode.selectBinaryPath(paths, "win32") // then should return ps1 path expect(selected).toBe("C:\\\\bin\\\\opencode.ps1") }) it("builds PowerShell command for ps1 on Windows", () => { // given a ps1 path on Windows const command = opencode.buildVersionCommand( "C:\\\\bin\\\\opencode.ps1", "win32" ) // when building command // then should use PowerShell expect(command).toEqual([ "powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "C:\\\\bin\\\\opencode.ps1", "--version", ]) }) it("builds direct command for non-ps1 binaries", () => { // given an exe on Windows and a binary on linux const winCommand = opencode.buildVersionCommand( "C:\\\\bin\\\\opencode.exe", "win32" ) const linuxCommand = opencode.buildVersionCommand("opencode", "linux") // when building commands // then should execute directly expect(winCommand).toEqual(["C:\\\\bin\\\\opencode.exe", "--version"]) expect(linuxCommand).toEqual(["opencode", "--version"]) }) }) describe("getOpenCodeInfo", () => { it("returns installed: false when binary not found", async () => { // given no opencode binary const spy = spyOn(opencode, "findOpenCodeBinary").mockResolvedValue(null) // when getting info const info = await opencode.getOpenCodeInfo() // then should indicate not installed expect(info.installed).toBe(false) expect(info.version).toBeNull() expect(info.path).toBeNull() expect(info.binary).toBeNull() spy.mockRestore() }) }) describe("checkOpenCodeInstallation", () => { let getInfoSpy: ReturnType afterEach(() => { getInfoSpy?.mockRestore() }) it("returns fail when not installed", async () => { // given opencode not installed getInfoSpy = spyOn(opencode, "getOpenCodeInfo").mockResolvedValue({ installed: false, version: null, path: null, binary: null, }) // when checking installation const result = await opencode.checkOpenCodeInstallation() // then should fail with installation hint expect(result.status).toBe("fail") expect(result.message).toContain("not installed") expect(result.details).toBeDefined() expect(result.details?.some((d) => d.includes("opencode.ai"))).toBe(true) }) it("returns warn when version below minimum", async () => { // given old version installed getInfoSpy = spyOn(opencode, "getOpenCodeInfo").mockResolvedValue({ installed: true, version: "1.0.100", path: "/usr/local/bin/opencode", binary: "opencode", }) // when checking installation const result = await opencode.checkOpenCodeInstallation() // then should warn about old version expect(result.status).toBe("warn") expect(result.message).toContain("below minimum") expect(result.details?.some((d) => d.includes(MIN_OPENCODE_VERSION))).toBe(true) }) it("returns pass when properly installed", async () => { // given current version installed getInfoSpy = spyOn(opencode, "getOpenCodeInfo").mockResolvedValue({ installed: true, version: "1.0.200", path: "/usr/local/bin/opencode", binary: "opencode", }) // when checking installation const result = await opencode.checkOpenCodeInstallation() // then should pass expect(result.status).toBe("pass") expect(result.message).toContain("1.0.200") }) }) describe("getOpenCodeCheckDefinition", () => { it("returns valid check definition", () => { // given // when getting definition const def = opencode.getOpenCodeCheckDefinition() // then should have required properties expect(def.id).toBe("opencode-installation") expect(def.category).toBe("installation") expect(def.critical).toBe(true) expect(typeof def.check).toBe("function") }) }) describe("getDesktopAppPaths", () => { it("returns macOS desktop app paths for darwin platform", () => { // given darwin platform const platform: NodeJS.Platform = "darwin" // when getting desktop paths const paths = opencode.getDesktopAppPaths(platform) // then should include macOS app bundle paths with correct binary name expect(paths).toContain("/Applications/OpenCode.app/Contents/MacOS/OpenCode") expect(paths.some((p) => p.includes("Applications/OpenCode.app"))).toBe(true) }) it("returns Windows desktop app paths for win32 platform when env vars set", () => { // given win32 platform with env vars set const platform: NodeJS.Platform = "win32" const originalProgramFiles = process.env.ProgramFiles const originalLocalAppData = process.env.LOCALAPPDATA process.env.ProgramFiles = "C:\\Program Files" process.env.LOCALAPPDATA = "C:\\Users\\Test\\AppData\\Local" // when getting desktop paths const paths = opencode.getDesktopAppPaths(platform) // then should include Windows program paths with correct binary name expect(paths.some((p) => p.includes("Program Files"))).toBe(true) expect(paths.some((p) => p.endsWith("OpenCode.exe"))).toBe(true) expect(paths.every((p) => p.startsWith("C:\\"))).toBe(true) // cleanup process.env.ProgramFiles = originalProgramFiles process.env.LOCALAPPDATA = originalLocalAppData }) it("returns empty array for win32 when all env vars undefined", () => { // given win32 platform with no env vars const platform: NodeJS.Platform = "win32" const originalProgramFiles = process.env.ProgramFiles const originalLocalAppData = process.env.LOCALAPPDATA delete process.env.ProgramFiles delete process.env.LOCALAPPDATA // when getting desktop paths const paths = opencode.getDesktopAppPaths(platform) // then should return empty array (no relative paths) expect(paths).toEqual([]) // cleanup process.env.ProgramFiles = originalProgramFiles process.env.LOCALAPPDATA = originalLocalAppData }) it("returns Linux desktop app paths for linux platform", () => { // given linux platform const platform: NodeJS.Platform = "linux" // when getting desktop paths const paths = opencode.getDesktopAppPaths(platform) // then should include verified Linux installation paths expect(paths).toContain("/usr/bin/opencode") expect(paths).toContain("/usr/lib/opencode/opencode") expect(paths.some((p) => p.includes("AppImage"))).toBe(true) }) it("returns empty array for unsupported platforms", () => { // given unsupported platform const platform = "freebsd" as NodeJS.Platform // when getting desktop paths const paths = opencode.getDesktopAppPaths(platform) // then should return empty array expect(paths).toEqual([]) }) }) describe("findOpenCodeBinary with desktop fallback", () => { it("falls back to desktop paths when PATH binary not found", async () => { // given no binary in PATH but desktop app exists const existsSyncMock = (p: string) => p === "/Applications/OpenCode.app/Contents/MacOS/OpenCode" // when finding binary with mocked filesystem const result = await opencode.findDesktopBinary("darwin", existsSyncMock) // then should find desktop app expect(result).not.toBeNull() expect(result?.path).toBe("/Applications/OpenCode.app/Contents/MacOS/OpenCode") }) it("returns null when no desktop binary found", async () => { // given no binary exists const existsSyncMock = () => false // when finding binary const result = await opencode.findDesktopBinary("darwin", existsSyncMock) // then should return null expect(result).toBeNull() }) }) })