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:
parent
bdbc8d73cb
commit
bb14537b14
1393
src/cli/__snapshots__/model-fallback.test.ts.snap
Normal file
1393
src/cli/__snapshots__/model-fallback.test.ts.snap
Normal file
File diff suppressed because it is too large
Load Diff
151
src/cli/install.test.ts
Normal file
151
src/cli/install.test.ts
Normal 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")
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -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) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user