test(cli): batch 66 (5 files)
This commit is contained in:
@@ -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")
|
||||
})
|
||||
})
|
||||
@@ -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")
|
||||
})
|
||||
})
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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`
|
||||
}
|
||||
@@ -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?")
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user