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