* feat(doctor): detect OpenCode desktop GUI installations on all platforms - Add getDesktopAppPaths() returning platform-specific desktop app paths - macOS: /Applications/OpenCode.app, ~/Applications/OpenCode.app - Windows: C:\Program Files\OpenCode, %LOCALAPPDATA%\Programs\OpenCode - Linux: /opt/opencode, /snap/bin, ~/.local/bin - Add findDesktopBinary() for testable desktop path detection - Modify findOpenCodeBinary() to check desktop paths as fallback Fixes #1310 * fix: use verified installation paths from OpenCode source Verified paths from sst/opencode Tauri config: macOS: - /Applications/OpenCode.app/Contents/MacOS/OpenCode (capital C) Windows: - C:\Program Files\OpenCode\OpenCode.exe - %LOCALAPPDATA%\OpenCode\OpenCode.exe - Removed hardcoded paths, use ProgramFiles env var - Filter empty paths when env vars undefined Linux: - /usr/bin/opencode (deb symlink) - /usr/lib/opencode/opencode (deb actual binary) - ~/Applications/*.AppImage (user AppImage) - Removed non-existent /opt/opencode and /snap/bin paths * chore: remove unused imports from tests
332 lines
11 KiB
TypeScript
332 lines
11 KiB
TypeScript
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<typeof spyOn>
|
|
|
|
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()
|
|
})
|
|
})
|
|
})
|