test(cli): add install command tests with snapshots

Add comprehensive tests for the install command with snapshot testing for generated configurations.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
justsisyphus 2026-01-22 22:47:43 +09:00
parent bdbc8d73cb
commit bb14537b14
3 changed files with 1570 additions and 29 deletions

File diff suppressed because it is too large Load Diff

151
src/cli/install.test.ts Normal file
View File

@ -0,0 +1,151 @@
import { describe, expect, test, mock, beforeEach, afterEach, spyOn } from "bun:test"
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { install } from "./install"
import * as configManager from "./config-manager"
import type { InstallArgs } from "./types"
// Mock console methods to capture output
const mockConsoleLog = mock(() => {})
const mockConsoleError = mock(() => {})
describe("install CLI - binary check behavior", () => {
let tempDir: string
let originalEnv: string | undefined
let isOpenCodeInstalledSpy: ReturnType<typeof spyOn>
let getOpenCodeVersionSpy: ReturnType<typeof spyOn>
beforeEach(() => {
// #given temporary config directory
tempDir = join(tmpdir(), `omo-test-${Date.now()}-${Math.random().toString(36).slice(2)}`)
mkdirSync(tempDir, { recursive: true })
originalEnv = process.env.OPENCODE_CONFIG_DIR
process.env.OPENCODE_CONFIG_DIR = tempDir
// Reset config context
configManager.resetConfigContext()
configManager.initConfigContext("opencode", null)
// Capture console output
console.log = mockConsoleLog
mockConsoleLog.mockClear()
})
afterEach(() => {
if (originalEnv !== undefined) {
process.env.OPENCODE_CONFIG_DIR = originalEnv
} else {
delete process.env.OPENCODE_CONFIG_DIR
}
if (existsSync(tempDir)) {
rmSync(tempDir, { recursive: true, force: true })
}
isOpenCodeInstalledSpy?.mockRestore()
getOpenCodeVersionSpy?.mockRestore()
})
test("non-TUI mode: should show warning but continue when OpenCode binary not found", async () => {
// #given OpenCode binary is NOT installed
isOpenCodeInstalledSpy = spyOn(configManager, "isOpenCodeInstalled").mockResolvedValue(false)
getOpenCodeVersionSpy = spyOn(configManager, "getOpenCodeVersion").mockResolvedValue(null)
const args: InstallArgs = {
tui: false,
claude: "yes",
openai: "no",
gemini: "no",
copilot: "no",
opencodeZen: "no",
zaiCodingPlan: "no",
}
// #when running install
const exitCode = await install(args)
// #then should return success (0), not failure (1)
expect(exitCode).toBe(0)
// #then should have printed a warning (not error)
const allCalls = mockConsoleLog.mock.calls.flat().join("\n")
expect(allCalls).toContain("[!]") // warning symbol
expect(allCalls).toContain("OpenCode")
})
test("non-TUI mode: should create opencode.json with plugin even when binary not found", async () => {
// #given OpenCode binary is NOT installed
isOpenCodeInstalledSpy = spyOn(configManager, "isOpenCodeInstalled").mockResolvedValue(false)
getOpenCodeVersionSpy = spyOn(configManager, "getOpenCodeVersion").mockResolvedValue(null)
// #given mock npm fetch
globalThis.fetch = mock(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({ latest: "3.0.0" }),
} as Response)
) as unknown as typeof fetch
const args: InstallArgs = {
tui: false,
claude: "yes",
openai: "no",
gemini: "no",
copilot: "no",
opencodeZen: "no",
zaiCodingPlan: "no",
}
// #when running install
const exitCode = await install(args)
// #then should create opencode.json
const configPath = join(tempDir, "opencode.json")
expect(existsSync(configPath)).toBe(true)
// #then opencode.json should have plugin entry
const config = JSON.parse(readFileSync(configPath, "utf-8"))
expect(config.plugin).toBeDefined()
expect(config.plugin.some((p: string) => p.includes("oh-my-opencode"))).toBe(true)
// #then exit code should be 0 (success)
expect(exitCode).toBe(0)
})
test("non-TUI mode: should still succeed and complete all steps when binary exists", async () => {
// #given OpenCode binary IS installed
isOpenCodeInstalledSpy = spyOn(configManager, "isOpenCodeInstalled").mockResolvedValue(true)
getOpenCodeVersionSpy = spyOn(configManager, "getOpenCodeVersion").mockResolvedValue("1.0.200")
// #given mock npm fetch
globalThis.fetch = mock(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve({ latest: "3.0.0" }),
} as Response)
) as unknown as typeof fetch
const args: InstallArgs = {
tui: false,
claude: "yes",
openai: "no",
gemini: "no",
copilot: "no",
opencodeZen: "no",
zaiCodingPlan: "no",
}
// #when running install
const exitCode = await install(args)
// #then should return success
expect(exitCode).toBe(0)
// #then should have printed success (OK symbol)
const allCalls = mockConsoleLog.mock.calls.flat().join("\n")
expect(allCalls).toContain("[OK]")
expect(allCalls).toContain("OpenCode 1.0.200")
})
})

View File

@ -16,13 +16,13 @@ import packageJson from "../../package.json" with { type: "json" }
const VERSION = packageJson.version const VERSION = packageJson.version
const SYMBOLS = { const SYMBOLS = {
check: color.green(""), check: color.green("[OK]"),
cross: color.red(""), cross: color.red("[X]"),
arrow: color.cyan(""), arrow: color.cyan("->"),
bullet: color.dim(""), bullet: color.dim("*"),
info: color.blue(""), info: color.blue("[i]"),
warn: color.yellow(""), warn: color.yellow("[!]"),
star: color.yellow(""), star: color.yellow("*"),
} }
function formatProvider(name: string, enabled: boolean, detail?: string): string { function formatProvider(name: string, enabled: boolean, detail?: string): string {
@ -295,14 +295,13 @@ async function runNonTuiInstall(args: InstallArgs): Promise<number> {
printStep(step++, totalSteps, "Checking OpenCode installation...") printStep(step++, totalSteps, "Checking OpenCode installation...")
const installed = await isOpenCodeInstalled() const installed = await isOpenCodeInstalled()
if (!installed) {
printError("OpenCode is not installed on this system.")
printInfo("Visit https://opencode.ai/docs for installation instructions")
return 1
}
const version = await getOpenCodeVersion() const version = await getOpenCodeVersion()
printSuccess(`OpenCode ${version ?? ""} detected`) if (!installed) {
printWarning("OpenCode binary not found. Plugin will be configured, but you'll need to install OpenCode to use it.")
printInfo("Visit https://opencode.ai/docs for installation instructions")
} else {
printSuccess(`OpenCode ${version ?? ""} detected`)
}
if (isUpdate) { if (isUpdate) {
const initial = detectedToInitialValues(detected) const initial = detectedToInitialValues(detected)
@ -351,7 +350,7 @@ async function runNonTuiInstall(args: InstallArgs): Promise<number> {
if (!config.hasClaude) { if (!config.hasClaude) {
console.log() console.log()
console.log(color.bgRed(color.white(color.bold(" ⚠️ CRITICAL WARNING ")))) console.log(color.bgRed(color.white(color.bold(" CRITICAL WARNING "))))
console.log() console.log()
console.log(color.red(color.bold(" Sisyphus agent is STRONGLY optimized for Claude Opus 4.5."))) console.log(color.red(color.bold(" Sisyphus agent is STRONGLY optimized for Claude Opus 4.5.")))
console.log(color.red(" Without Claude, you may experience significantly degraded performance:")) console.log(color.red(" Without Claude, you may experience significantly degraded performance:"))
@ -375,7 +374,7 @@ async function runNonTuiInstall(args: InstallArgs): Promise<number> {
`${color.bold("Pro Tip:")} Include ${color.cyan("ultrawork")} (or ${color.cyan("ulw")}) in your prompt.\n` + `${color.bold("Pro Tip:")} Include ${color.cyan("ultrawork")} (or ${color.cyan("ulw")}) in your prompt.\n` +
`All features work like magic—parallel agents, background tasks,\n` + `All features work like magic—parallel agents, background tasks,\n` +
`deep exploration, and relentless execution until completion.`, `deep exploration, and relentless execution until completion.`,
"🪄 The Magic Word" "The Magic Word"
) )
console.log(`${SYMBOLS.star} ${color.yellow("If you found this helpful, consider starring the repo!")}`) console.log(`${SYMBOLS.star} ${color.yellow("If you found this helpful, consider starring the repo!")}`)
@ -390,7 +389,7 @@ async function runNonTuiInstall(args: InstallArgs): Promise<number> {
(config.hasClaude ? ` ${SYMBOLS.bullet} Anthropic ${color.gray("→ Claude Pro/Max")}\n` : "") + (config.hasClaude ? ` ${SYMBOLS.bullet} Anthropic ${color.gray("→ Claude Pro/Max")}\n` : "") +
(config.hasGemini ? ` ${SYMBOLS.bullet} Google ${color.gray("→ OAuth with Antigravity")}\n` : "") + (config.hasGemini ? ` ${SYMBOLS.bullet} Google ${color.gray("→ OAuth with Antigravity")}\n` : "") +
(config.hasCopilot ? ` ${SYMBOLS.bullet} GitHub ${color.gray("→ Copilot")}` : ""), (config.hasCopilot ? ` ${SYMBOLS.bullet} GitHub ${color.gray("→ Copilot")}` : ""),
"🔐 Authenticate Your Providers" "Authenticate Your Providers"
) )
} }
@ -416,16 +415,14 @@ export async function install(args: InstallArgs): Promise<number> {
s.start("Checking OpenCode installation") s.start("Checking OpenCode installation")
const installed = await isOpenCodeInstalled() const installed = await isOpenCodeInstalled()
if (!installed) {
s.stop("OpenCode is not installed")
p.log.error("OpenCode is not installed on this system.")
p.note("Visit https://opencode.ai/docs for installation instructions", "Installation Guide")
p.outro(color.red("Please install OpenCode first."))
return 1
}
const version = await getOpenCodeVersion() const version = await getOpenCodeVersion()
s.stop(`OpenCode ${version ?? "installed"} ${color.green("✓")}`) if (!installed) {
s.stop(`OpenCode binary not found ${color.yellow("[!]")}`)
p.log.warn("OpenCode binary not found. Plugin will be configured, but you'll need to install OpenCode to use it.")
p.note("Visit https://opencode.ai/docs for installation instructions", "Installation Guide")
} else {
s.stop(`OpenCode ${version ?? "installed"} ${color.green("[OK]")}`)
}
const config = await runTuiMode(detected) const config = await runTuiMode(detected)
if (!config) return 1 if (!config) return 1
@ -470,7 +467,7 @@ export async function install(args: InstallArgs): Promise<number> {
if (!config.hasClaude) { if (!config.hasClaude) {
console.log() console.log()
console.log(color.bgRed(color.white(color.bold(" ⚠️ CRITICAL WARNING ")))) console.log(color.bgRed(color.white(color.bold(" CRITICAL WARNING "))))
console.log() console.log()
console.log(color.red(color.bold(" Sisyphus agent is STRONGLY optimized for Claude Opus 4.5."))) console.log(color.red(color.bold(" Sisyphus agent is STRONGLY optimized for Claude Opus 4.5.")))
console.log(color.red(" Without Claude, you may experience significantly degraded performance:")) console.log(color.red(" Without Claude, you may experience significantly degraded performance:"))
@ -495,7 +492,7 @@ export async function install(args: InstallArgs): Promise<number> {
`Include ${color.cyan("ultrawork")} (or ${color.cyan("ulw")}) in your prompt.\n` + `Include ${color.cyan("ultrawork")} (or ${color.cyan("ulw")}) in your prompt.\n` +
`All features work like magic—parallel agents, background tasks,\n` + `All features work like magic—parallel agents, background tasks,\n` +
`deep exploration, and relentless execution until completion.`, `deep exploration, and relentless execution until completion.`,
"🪄 The Magic Word" "The Magic Word"
) )
p.log.message(`${color.yellow("★")} If you found this helpful, consider starring the repo!`) p.log.message(`${color.yellow("★")} If you found this helpful, consider starring the repo!`)
@ -510,7 +507,7 @@ export async function install(args: InstallArgs): Promise<number> {
if (config.hasCopilot) providers.push(`GitHub ${color.gray("→ Copilot")}`) if (config.hasCopilot) providers.push(`GitHub ${color.gray("→ Copilot")}`)
console.log() console.log()
console.log(color.bold("🔐 Authenticate Your Providers")) console.log(color.bold("Authenticate Your Providers"))
console.log() console.log()
console.log(` Run ${color.cyan("opencode auth login")} and select:`) console.log(` Run ${color.cyan("opencode auth login")} and select:`)
for (const provider of providers) { for (const provider of providers) {