import { afterEach, describe, expect, mock, spyOn, test } from "bun:test" import { OpencodeClient } from "@opencode-ai/sdk/v2" import type { Resolved } from "@opencode-ai/tui/config" import { TuiConfig } from "@/config/tui" import { resolveDiffStyle, resolveModelInfo, resolveRunTuiConfig } from "@/cli/cmd/run/runtime.boot" import { createTuiResolvedConfig } from "../../fixture/tui-runtime" function ok(data: T) { return Promise.resolve({ data, error: undefined, request: new Request("https://opencode.test"), response: new Response(), }) } function provider(id: string, name: string) { return { id, name, api: { type: "native" as const, settings: {} }, request: { headers: {}, body: {} }, } } function model(id: string, providerID: string, context: number, variants: string[] = []) { return { id, providerID, api: { id: providerID, type: "native" as const, settings: {}, }, name: id, capabilities: { tools: true, input: ["text"], output: ["text"], }, request: { headers: {}, body: {}, }, variants: variants.map((variant) => ({ id: variant, headers: {}, body: {}, })), time: { released: 1, }, cost: [ { input: 0, output: 0, cache: { read: 0, write: 0, }, }, ], limit: { context, output: 8192, }, status: "active" as const, enabled: true, } } function config(input?: { leader?: string leaderTimeout?: number diff_style?: "auto" | "stacked" bindings?: Partial<{ commandList: string[] variantCycle: string[] interrupt: string[] historyPrevious: string[] historyNext: string[] inputClear: string[] inputSubmit: string[] inputNewline: string[] }> }): Resolved { const bind = input?.bindings return createTuiResolvedConfig({ diff_style: input?.diff_style, leader_timeout: input?.leaderTimeout, keybinds: { ...(input?.leader && { leader: input.leader }), ...(bind?.commandList && { command_list: bind.commandList }), ...(bind?.variantCycle && { variant_cycle: bind.variantCycle }), ...(bind?.interrupt && { session_interrupt: bind.interrupt }), ...(bind?.historyPrevious && { history_previous: bind.historyPrevious }), ...(bind?.historyNext && { history_next: bind.historyNext }), ...(bind?.inputClear && { input_clear: bind.inputClear }), ...(bind?.inputSubmit && { input_submit: bind.inputSubmit }), ...(bind?.inputNewline && { input_newline: bind.inputNewline }), }, }) } describe("run runtime boot", () => { afterEach(() => { mock.restore() }) test("reads footer keybinds from resolved keybind config", async () => { spyOn(TuiConfig, "get").mockResolvedValue( config({ leader: "ctrl+g", bindings: { commandList: ["ctrl+p"], variantCycle: ["ctrl+t", "alt+t"], interrupt: ["ctrl+c"], historyPrevious: ["k"], historyNext: ["j"], inputClear: ["ctrl+l"], inputSubmit: ["ctrl+s"], inputNewline: ["alt+return"], }, }), ) const result = await resolveRunTuiConfig() expect(result.keybinds.get("leader")?.[0]?.key).toBe("ctrl+g") expect(result.leader_timeout).toBe(2000) expect(result.keybinds.get("command.palette.show")?.[0]?.key).toBe("ctrl+p") expect(result.keybinds.get("variant.cycle").map((item) => item.key)).toEqual(["ctrl+t", "alt+t"]) expect(result.keybinds.get("session.interrupt")?.[0]?.key).toBe("ctrl+c") expect(result.keybinds.get("prompt.history.previous")?.[0]?.key).toBe("k") expect(result.keybinds.get("prompt.history.next")?.[0]?.key).toBe("j") expect(result.keybinds.get("prompt.clear")?.[0]?.key).toBe("ctrl+l") expect(result.keybinds.get("input.submit")?.[0]?.key).toBe("ctrl+s") expect(result.keybinds.get("input.newline")?.[0]?.key).toBe("alt+return") }) test("falls back to default tui keymap config when config load fails", async () => { spyOn(TuiConfig, "get").mockRejectedValue(new Error("boom")) const result = await resolveRunTuiConfig() expect(result.keybinds.get("leader")?.[0]?.key).toBe("ctrl+x") expect(result.leader_timeout).toBe(2000) expect(result.diff_style).toBe("auto") expect(result.keybinds.get("command.palette.show")?.[0]?.key).toBe("ctrl+p") expect(result.keybinds.get("variant.cycle")?.[0]?.key).toBe("ctrl+t") expect(result.keybinds.get("session.interrupt")?.[0]?.key).toBe("escape") expect(result.keybinds.get("prompt.history.previous")?.[0]?.key).toBe("up") expect(result.keybinds.get("prompt.history.next")?.[0]?.key).toBe("down") expect(result.keybinds.get("prompt.clear")?.[0]?.key).toBe("ctrl+c") expect(result.keybinds.get("input.submit")?.[0]?.key).toBe("return") expect(result.keybinds.get("input.newline")?.[0]?.key).toBe("shift+return,ctrl+return,alt+return,ctrl+j") }) test("preserves disabled leader from resolved tui config", async () => { spyOn(TuiConfig, "get").mockResolvedValue(config({ leader: "none" })) const result = await resolveRunTuiConfig() expect(result.keybinds.get("leader")).toEqual([]) }) test("reads diff style and falls back to auto", async () => { spyOn(TuiConfig, "get").mockResolvedValue(config({ diff_style: "stacked" })) await expect(resolveDiffStyle()).resolves.toBe("stacked") mock.restore() spyOn(TuiConfig, "get").mockRejectedValue(new Error("boom")) await expect(resolveDiffStyle()).resolves.toBe("auto") }) test("loads v2 providers and models for model selector data", async () => { const sdk = new OpencodeClient() const providers = [provider("openai", "OpenAI")] const models = [model("gpt-5", "openai", 128000, ["high", "minimal"])] // The generated methods have conditional return types for throwOnError; these mocks represent the successful branch. // @ts-expect-error successful SDK response is valid for both modes at runtime const providerList = spyOn(sdk.v2.provider, "list").mockImplementation(() => ok({ data: providers })) // @ts-expect-error successful SDK response is valid for both modes at runtime spyOn(sdk.v2.model, "list").mockImplementation(() => ok({ data: models })) await expect(resolveModelInfo(sdk, "/workspace", { providerID: "openai", modelID: "gpt-5" })).resolves.toEqual({ providers: [ { id: "openai", name: "OpenAI", models: { "gpt-5": { id: "gpt-5", providerID: "openai", name: "gpt-5", capabilities: { tools: true, input: ["text"], output: ["text"], }, cost: { input: 0, output: 0, cache: { read: 0, write: 0, }, }, limit: { context: 128000, output: 8192, }, status: "active", variants: { high: {}, minimal: {}, }, }, }, }, ], variants: ["high", "minimal"], limits: { "openai/gpt-5": 128000, }, }) expect(providerList).toHaveBeenCalledWith( { location: { directory: "/workspace", }, }, { throwOnError: true }, ) }) test("loads context limits across v2 providers", async () => { const sdk = new OpencodeClient() const providers = [provider("openai", "OpenAI"), provider("anthropic", "Anthropic")] const models = [model("gpt-5", "openai", 128000, ["high", "minimal"]), model("sonnet", "anthropic", 200000)] // The generated methods have conditional return types for throwOnError; these mocks represent the successful branch. // @ts-expect-error successful SDK response is valid for both modes at runtime spyOn(sdk.v2.provider, "list").mockImplementation(() => ok({ data: providers })) // @ts-expect-error successful SDK response is valid for both modes at runtime spyOn(sdk.v2.model, "list").mockImplementation(() => ok({ data: models })) await expect(resolveModelInfo(sdk, "/workspace", { providerID: "openai", modelID: "gpt-5" })).resolves.toEqual({ providers: [ expect.objectContaining({ id: "openai", name: "OpenAI", models: expect.objectContaining({ "gpt-5": expect.objectContaining({ variants: { high: {}, minimal: {}, }, }), }), }), expect.objectContaining({ id: "anthropic", name: "Anthropic", models: expect.objectContaining({ sonnet: expect.objectContaining({ variants: {}, }), }), }), ], variants: ["high", "minimal"], limits: { "openai/gpt-5": 128000, "anthropic/sonnet": 200000, }, }) }) })