169 lines
4.7 KiB
TypeScript
169 lines
4.7 KiB
TypeScript
/// <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", async () => {
|
|
// given
|
|
const selectSpy = spyOn(p, "select").mockResolvedValue("opencode")
|
|
|
|
// when
|
|
const value = await prompts.promptInstallPlatform("opencode")
|
|
|
|
// 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("preserves Codex as the initial platform", async () => {
|
|
// given
|
|
const selectSpy = spyOn(p, "select").mockResolvedValue("codex")
|
|
|
|
// when
|
|
const value = await prompts.promptInstallPlatform("codex")
|
|
|
|
// then
|
|
expect(value).toBe("codex")
|
|
expect(selectSpy).toHaveBeenCalledTimes(1)
|
|
expect(selectSpy.mock.calls[0]?.[0]).toMatchObject({
|
|
initialValue: "codex",
|
|
options: [
|
|
{ value: "opencode" },
|
|
{ value: "codex" },
|
|
{ value: "both" },
|
|
],
|
|
})
|
|
})
|
|
})
|
|
|
|
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?")
|
|
})
|
|
})
|