Handle Windows where lookup and prefer exe/cmd/bat; fall back to ps1 and run via PowerShell for version detection. Tests: bun test src/cli/doctor/checks/opencode.test.ts
228 lines
7.2 KiB
TypeScript
228 lines
7.2 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")
|
|
})
|
|
})
|
|
})
|