test(cli): batch 66 (5 files)

This commit is contained in:
YeonGyu-Kim
2026-05-30 19:12:11 +09:00
parent b66398c3a9
commit 5f13b823cc
5 changed files with 484 additions and 0 deletions
+177
View File
@@ -0,0 +1,177 @@
/// <reference types="bun-types" />
import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from "bun:test"
import { runCliInstaller } from "./cli-installer"
import * as configManager from "./config-manager"
import * as codexInstaller from "./install-codex"
import type { CodexInstallResult } from "./install-codex"
import type { InstallArgs } from "./types"
const codexResult: CodexInstallResult = {
marketplaceName: "sisyphuslabs",
installed: [],
configPath: "/tmp/codex-config.toml",
codexHome: "/tmp/codex-home",
}
function createOpenCodeArgs(platform: "opencode" | "both"): InstallArgs {
return {
tui: false,
platform,
claude: "no",
openai: "no",
gemini: "no",
copilot: "no",
opencodeZen: "no",
zaiCodingPlan: "no",
kimiForCoding: "no",
opencodeGo: "no",
vercelAiGateway: "no",
}
}
function stubOpenCodeSuccess(): void {
spyOn(configManager, "detectCurrentConfig").mockReturnValue({
isInstalled: false,
installedVersion: null,
hasClaude: false,
isMax20: false,
hasOpenAI: false,
hasGemini: false,
hasCopilot: false,
hasCodex: false,
hasOpencodeZen: false,
hasZaiCodingPlan: false,
hasKimiForCoding: false,
hasOpencodeGo: false,
hasVercelAiGateway: false,
})
spyOn(configManager, "isOpenCodeInstalled").mockResolvedValue(true)
spyOn(configManager, "getOpenCodeVersion").mockResolvedValue("1.4.0")
spyOn(configManager, "addPluginToOpenCodeConfig").mockResolvedValue({
success: true,
configPath: "/tmp/opencode.jsonc",
})
spyOn(configManager, "writeOmoConfig").mockReturnValue({
success: true,
configPath: "/tmp/oh-my-opencode.jsonc",
})
}
describe("runCliInstaller platform branching", () => {
const consoleLogMock = mock(() => {})
const consoleLog = console.log
const originalPublishLazycodex = process.env.OMO_PUBLISH_LAZYCODEX
beforeEach(() => {
consoleLogMock.mockClear()
console.log = consoleLogMock
process.env.OMO_PUBLISH_LAZYCODEX = "true"
})
afterEach(() => {
console.log = consoleLog
if (originalPublishLazycodex === undefined) {
delete process.env.OMO_PUBLISH_LAZYCODEX
} else {
process.env.OMO_PUBLISH_LAZYCODEX = originalPublishLazycodex
}
mock.restore()
})
test("runs only OpenCode installation for platform=opencode", async () => {
// given
stubOpenCodeSuccess()
const codexSpy = spyOn(codexInstaller, "runCodexInstaller").mockResolvedValue(codexResult)
const writeSpy = spyOn(configManager, "writeOmoConfig")
// when
const result = await runCliInstaller(createOpenCodeArgs("opencode"), "3.4.0")
// then
expect(result).toBe(0)
expect(writeSpy).toHaveBeenCalledTimes(1)
expect(codexSpy).not.toHaveBeenCalled()
})
test("runs only Codex installation and skips OpenCode version checks for platform=codex", async () => {
// given
const versionSpy = spyOn(configManager, "getOpenCodeVersion")
const writeSpy = spyOn(configManager, "writeOmoConfig")
const codexSpy = spyOn(codexInstaller, "runCodexInstaller").mockResolvedValue(codexResult)
// when
const result = await runCliInstaller({ tui: false, platform: "codex" }, "3.4.0")
// then
expect(result).toBe(0)
expect(versionSpy).not.toHaveBeenCalled()
expect(writeSpy).not.toHaveBeenCalled()
expect(codexSpy).toHaveBeenCalledTimes(1)
})
test("passes Codex autonomous selection into Codex installer", async () => {
// given
const codexSpy = spyOn(codexInstaller, "runCodexInstaller").mockResolvedValue(codexResult)
// when
const result = await runCliInstaller({ tui: false, platform: "codex", codexAutonomous: true }, "3.4.0")
// then
expect(result).toBe(0)
expect(codexSpy).toHaveBeenCalledWith({ autonomousPermissions: true })
})
test("runs OpenCode and Codex installation for platform=both", async () => {
// given
stubOpenCodeSuccess()
const codexSpy = spyOn(codexInstaller, "runCodexInstaller").mockResolvedValue(codexResult)
const writeSpy = spyOn(configManager, "writeOmoConfig")
// when
const result = await runCliInstaller(createOpenCodeArgs("both"), "3.4.0")
// then
expect(result).toBe(0)
expect(writeSpy).toHaveBeenCalledTimes(1)
expect(codexSpy).toHaveBeenCalledTimes(1)
})
test("fails when Codex-only installation cannot install Codex", async () => {
// given
spyOn(codexInstaller, "runCodexInstaller").mockRejectedValue(new Error("codex failed"))
// when
const result = await runCliInstaller({ tui: false, platform: "codex" }, "3.4.0")
// then
expect(result).toBe(1)
})
test("keeps OpenCode success when Codex fails for platform=both", async () => {
// given
stubOpenCodeSuccess()
spyOn(codexInstaller, "runCodexInstaller").mockRejectedValue(new Error("codex failed"))
// when
const result = await runCliInstaller(createOpenCodeArgs("both"), "3.4.0")
// then
expect(result).toBe(0)
})
test("prints star commands for OpenAgent and LazyCodex", async () => {
// given
stubOpenCodeSuccess()
spyOn(codexInstaller, "runCodexInstaller").mockResolvedValue(codexResult)
// when
const result = await runCliInstaller(createOpenCodeArgs("both"), "3.4.0")
// then
const output = consoleLogMock.mock.calls.map((call) => call.join(" ")).join("\n")
expect(result).toBe(0)
expect(output).toContain("/user/starred/code-yeongyu/oh-my-openagent")
expect(output).toContain("/user/starred/code-yeongyu/lazycodex")
})
})
+120
View File
@@ -0,0 +1,120 @@
/// <reference types="bun-types" />
import { describe, expect, test } from "bun:test"
import { resolveInstallArgs } from "./cli-program"
describe("install platform resolution", () => {
test("leaves omo install without --platform unresolved for config defaults", () => {
// given
const invocationName = "omo"
// when
const args = resolveInstallArgs({ tui: true }, invocationName)
// then
expect(args.platform).toBeUndefined()
})
test("resolves explicit --platform=codex", () => {
// given
const invocationName = "omo"
// when
const args = resolveInstallArgs({ tui: true, platform: "codex" }, invocationName)
// then
expect(args.platform).toBe("codex")
})
test("preserves explicit Codex autonomous install flag", () => {
// given
const invocationName = "omo"
// when
const args = resolveInstallArgs({ tui: true, platform: "codex", codexAutonomous: true }, invocationName)
// then
expect(args.codexAutonomous).toBe(true)
})
test("resolves explicit --platform=both", () => {
// given
const invocationName = "omo"
// when
const args = resolveInstallArgs({ tui: true, platform: "both" }, invocationName)
// then
expect(args.platform).toBe("both")
})
test("resolves explicit --platform=opencode", () => {
// given
const invocationName = "omo"
// when
const args = resolveInstallArgs({ tui: true, platform: "opencode" }, invocationName)
// then
expect(args.platform).toBe("opencode")
})
test("leaves lazycodex install unresolved when lazycodex publishing is disabled", () => {
// given
const invocationName = "lazycodex"
// when
const args = resolveInstallArgs({ tui: true }, invocationName)
// then
expect(args.platform).toBeUndefined()
})
test("defaults lazycodex install to codex platform when lazycodex publishing is enabled", () => {
// given
const invocationName = "lazycodex"
// when
const args = resolveInstallArgs({ tui: true }, invocationName, { OMO_PUBLISH_LAZYCODEX: "true" })
// then
expect(args.platform).toBe("codex")
})
test("lets lazycodex install explicitly override to both", () => {
// given
const invocationName = "lazycodex"
// when
const args = resolveInstallArgs({ tui: true, platform: "both" }, invocationName)
// then
expect(args.platform).toBe("both")
})
test("lets lazycodex install explicitly override to opencode", () => {
// given
const invocationName = "lazycodex"
// when
const args = resolveInstallArgs({ tui: true, platform: "opencode" }, invocationName)
// then
expect(args.platform).toBe("opencode")
})
test("defines Commander choices so invalid --platform values are rejected", async () => {
// given
const cliProgramSource = await Bun.file(new URL("./cli-program.ts", import.meta.url)).text()
// when
const installBlock = cliProgramSource.match(/program\s*\n\s*\.command\("install"\)([\s\S]*?)\.action\(/)
// then
expect(installBlock).not.toBeNull()
expect(installBlock?.[1]).toContain('new Option("--platform <platform>"')
expect(installBlock?.[1]).toContain('.choices(["opencode", "codex", "both"])')
expect(installBlock?.[1]).toContain("--codex-autonomous")
expect(installBlock?.[1]).toContain("--no-codex-autonomous")
})
})
+15
View File
@@ -0,0 +1,15 @@
import type { InstallPlatform } from "./types"
export const LAZYCODEX_PUBLISH_FLAG = "OMO_PUBLISH_LAZYCODEX"
export const LAZYCODEX_DISABLED_MESSAGE =
"Codex platform install is disabled. Set OMO_PUBLISH_LAZYCODEX=true to enable LazyCodex publish/install."
type Environment = Readonly<Record<string, string | undefined>>
export function isLazycodexPublishingEnabled(env: Environment = process.env): boolean {
return env[LAZYCODEX_PUBLISH_FLAG] === "true"
}
export function platformRequiresLazycodex(platform: InstallPlatform | undefined): boolean {
return platform === "codex" || platform === "both"
}
+8
View File
@@ -0,0 +1,8 @@
export const STAR_REPOSITORIES = [
"code-yeongyu/oh-my-openagent",
"code-yeongyu/lazycodex",
] as const
export function formatGitHubStarCommand(repository: string): string {
return `gh api --silent --method PUT /user/starred/${repository} >/dev/null 2>&1 || true`
}
+164
View File
@@ -0,0 +1,164 @@
/// <reference types="bun-types" />
import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from "bun:test"
import * as p from "@clack/prompts"
import * as prompts from "./tui-install-prompts"
import type { DetectedConfig, InstallConfig, InstallPlatform } from "./types"
function createDetectedConfig(): DetectedConfig {
return {
isInstalled: false,
installedVersion: null,
hasClaude: false,
isMax20: false,
hasOpenAI: false,
hasGemini: false,
hasCopilot: false,
hasCodex: false,
hasOpencodeZen: false,
hasZaiCodingPlan: false,
hasKimiForCoding: false,
hasOpencodeGo: false,
hasVercelAiGateway: false,
}
}
function withTty(): () => void {
const originalIsStdinTty = process.stdin.isTTY
const originalIsStdoutTty = process.stdout.isTTY
Object.defineProperty(process.stdin, "isTTY", { configurable: true, value: true })
Object.defineProperty(process.stdout, "isTTY", { configurable: true, value: true })
return () => {
Object.defineProperty(process.stdin, "isTTY", { configurable: true, value: originalIsStdinTty })
Object.defineProperty(process.stdout, "isTTY", { configurable: true, value: originalIsStdoutTty })
}
}
describe("promptInstallPlatform", () => {
let restoreTty: () => void
beforeEach(() => {
restoreTty = withTty()
})
afterEach(() => {
restoreTty()
mock.restore()
})
test("offers OpenCode, Codex, and Both choices when lazycodex publishing is enabled", async () => {
// given
const selectSpy = spyOn(p, "select").mockResolvedValue("opencode")
// when
const value = await prompts.promptInstallPlatform("opencode", true)
// then
expect(value).toBe("opencode")
expect(selectSpy).toHaveBeenCalledTimes(1)
expect(selectSpy.mock.calls[0]?.[0]).toMatchObject({
initialValue: "opencode",
options: [
{ value: "opencode" },
{ value: "codex" },
{ value: "both" },
],
})
})
test("hides Codex platform choices when lazycodex publishing is disabled", async () => {
// given
const selectSpy = spyOn(p, "select").mockResolvedValue("opencode")
// when
const value = await prompts.promptInstallPlatform("codex", false)
// then
expect(value).toBe("opencode")
expect(selectSpy).toHaveBeenCalledTimes(1)
expect(selectSpy.mock.calls[0]?.[0]).toMatchObject({
initialValue: "opencode",
options: [{ value: "opencode" }],
})
})
})
describe("promptInstallConfig platform branching", () => {
let restoreTty: () => void
beforeEach(() => {
restoreTty = withTty()
})
afterEach(() => {
restoreTty()
mock.restore()
})
test("skips OpenCode questions when the user selects codex", async () => {
// given
const selectSpy = spyOn(p, "select").mockResolvedValue(true)
// when
const config = await prompts.promptInstallConfig(createDetectedConfig(), "codex")
// then
expect(config).toMatchObject({
platform: "codex",
hasOpenCode: false,
hasCodex: true,
codexAutonomous: true,
} satisfies Partial<InstallConfig>)
expect(selectSpy).toHaveBeenCalledTimes(1)
expect(selectSpy.mock.calls[0]?.[0]).toMatchObject({
initialValue: true,
options: [{ value: true }, { value: false }],
})
})
test.each([
["opencode", false],
["both", true],
] satisfies readonly [InstallPlatform, boolean][])(
"asks OpenCode questions when the user selects %s",
async (platform, hasCodex) => {
// given
const selectSpy = spyOn(p, "select").mockResolvedValue("no")
// when
const config = await prompts.promptInstallConfig(createDetectedConfig(), platform)
// then
expect(config).toMatchObject({ platform, hasOpenCode: true, hasCodex } satisfies Partial<InstallConfig>)
expect(selectSpy).toHaveBeenCalledTimes(hasCodex ? 10 : 9)
},
)
test("uses explicit Codex autonomous override without asking", async () => {
// given
const selectSpy = spyOn(p, "select").mockResolvedValue("no")
// when
const config = await prompts.promptInstallConfig(createDetectedConfig(), "codex", false)
// then
expect(config).toMatchObject({
platform: "codex",
hasCodex: true,
codexAutonomous: false,
} satisfies Partial<InstallConfig>)
expect(selectSpy).not.toHaveBeenCalled()
})
test("does not ask the old Codex adapter question", async () => {
// given
const selectSpy = spyOn(p, "select").mockResolvedValue("no")
// when
await prompts.promptInstallConfig(createDetectedConfig(), "both")
// then
const messages = selectSpy.mock.calls.map((call) => call[0].message)
expect(messages).not.toContain("Install Codex harness adapter into ~/.codex?")
})
})