runtime.boot.test.ts 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. import { afterEach, describe, expect, mock, spyOn, test } from "bun:test"
  2. import { OpenCode } from "@opencode-ai/client/promise"
  3. import type { Resolved } from "../../src/config"
  4. import { resolveMiniSettings, resolveModelInfo, resolveRunTuiConfig } from "../../src/mini/runtime.boot"
  5. import { catalogModel, catalogProvider } from "./fixture/catalog"
  6. import { createTuiResolvedConfig } from "../fixture/tui-runtime"
  7. function config(input?: {
  8. leader?: string
  9. leaderTimeout?: number
  10. bindings?: Partial<{
  11. commandList: string[]
  12. variantCycle: string[]
  13. interrupt: string[]
  14. historyPrevious: string[]
  15. historyNext: string[]
  16. inputClear: string[]
  17. inputSubmit: string[]
  18. inputNewline: string[]
  19. }>
  20. }): Resolved {
  21. const bind = input?.bindings
  22. return createTuiResolvedConfig({
  23. leader: input?.leaderTimeout === undefined ? undefined : { timeout: input.leaderTimeout },
  24. keybinds: {
  25. ...(input?.leader && { leader: input.leader }),
  26. ...(bind?.commandList && { command_list: bind.commandList }),
  27. ...(bind?.variantCycle && { variant_cycle: bind.variantCycle }),
  28. ...(bind?.interrupt && { session_interrupt: bind.interrupt }),
  29. ...(bind?.historyPrevious && { history_previous: bind.historyPrevious }),
  30. ...(bind?.historyNext && { history_next: bind.historyNext }),
  31. ...(bind?.inputClear && { input_clear: bind.inputClear }),
  32. ...(bind?.inputSubmit && { input_submit: bind.inputSubmit }),
  33. ...(bind?.inputNewline && { input_newline: bind.inputNewline }),
  34. },
  35. })
  36. }
  37. describe("run runtime boot", () => {
  38. afterEach(() => {
  39. mock.restore()
  40. })
  41. test("reads footer keybinds from resolved keybind config", async () => {
  42. const input = config({
  43. leader: "ctrl+g",
  44. bindings: {
  45. commandList: ["ctrl+p"],
  46. variantCycle: ["ctrl+t", "alt+t"],
  47. interrupt: ["ctrl+c"],
  48. historyPrevious: ["k"],
  49. historyNext: ["j"],
  50. inputClear: ["ctrl+l"],
  51. inputSubmit: ["ctrl+s"],
  52. inputNewline: ["alt+return"],
  53. },
  54. })
  55. const result = await resolveRunTuiConfig(input)
  56. expect(result.keybinds.get("leader")?.[0]?.key).toBe("ctrl+g")
  57. expect(result.leader.timeout).toBe(2000)
  58. expect(result.keybinds.get("command.palette.show")?.[0]?.key).toBe("ctrl+p")
  59. expect(result.keybinds.get("variant.cycle").map((item) => item.key)).toEqual(["ctrl+t", "alt+t"])
  60. expect(result.keybinds.get("session.interrupt")?.[0]?.key).toBe("ctrl+c")
  61. expect(result.keybinds.get("prompt.history.previous")?.[0]?.key).toBe("k")
  62. expect(result.keybinds.get("prompt.history.next")?.[0]?.key).toBe("j")
  63. expect(result.keybinds.get("prompt.clear")?.[0]?.key).toBe("ctrl+l")
  64. expect(result.keybinds.get("input.submit")?.[0]?.key).toBe("ctrl+s")
  65. expect(result.keybinds.get("input.newline")?.[0]?.key).toBe("alt+return")
  66. })
  67. test("falls back to default tui keymap config when config load fails", async () => {
  68. const result = await resolveRunTuiConfig(Promise.reject(new Error("boom")))
  69. expect(result.keybinds.get("leader")?.[0]?.key).toBe("ctrl+x")
  70. expect(result.leader.timeout).toBe(2000)
  71. expect(result.keybinds.get("command.palette.show")?.[0]?.key).toBe("ctrl+p")
  72. expect(result.keybinds.get("variant.cycle")?.[0]?.key).toBe("ctrl+t")
  73. expect(result.keybinds.get("session.interrupt")?.[0]?.key).toBe("escape")
  74. expect(result.keybinds.get("prompt.history.previous")?.[0]?.key).toBe("up")
  75. expect(result.keybinds.get("prompt.history.next")?.[0]?.key).toBe("down")
  76. expect(result.keybinds.get("prompt.clear")?.[0]?.key).toBe("ctrl+c")
  77. expect(result.keybinds.get("input.submit")?.[0]?.key).toBe("return")
  78. expect(result.keybinds.get("input.newline")?.[0]?.key).toBe("shift+return,ctrl+return,ctrl+j")
  79. expect(result.keybinds.get("prompt.queue")?.[0]?.key).toBe("alt+return")
  80. })
  81. test("preserves disabled leader from resolved tui config", async () => {
  82. const result = await resolveRunTuiConfig(config({ leader: "none" }))
  83. expect(result.keybinds.get("leader")).toEqual([])
  84. })
  85. test("preserves shared config while resolving independent Mini defaults", async () => {
  86. const result = await resolveRunTuiConfig(
  87. createTuiResolvedConfig({
  88. theme: { mode: "light" },
  89. leader: { timeout: 450 },
  90. }),
  91. )
  92. expect(result.theme).toEqual({ mode: "light" })
  93. expect(result.leader.timeout).toBe(450)
  94. expect(resolveMiniSettings(result)).toEqual({
  95. thinking: "hide",
  96. shell_output: "hide",
  97. turn_summary: "show",
  98. footer: "show",
  99. splash: "show",
  100. mono: false,
  101. })
  102. expect(
  103. resolveMiniSettings({
  104. mini: {
  105. thinking: "show",
  106. shell_output: "show",
  107. turn_summary: "hide",
  108. footer: "hide",
  109. splash: "hide",
  110. mono: true,
  111. },
  112. }),
  113. ).toEqual({
  114. thinking: "show",
  115. shell_output: "show",
  116. turn_summary: "hide",
  117. footer: "hide",
  118. splash: "hide",
  119. mono: true,
  120. })
  121. })
  122. test("loads v2 providers and models for model selector data", async () => {
  123. const sdk = OpenCode.make({ baseUrl: "https://opencode.test" })
  124. const location = { directory: "/workspace", project: { id: "proj_1", directory: "/workspace" } }
  125. const providerList = spyOn(sdk.provider, "list").mockResolvedValue({
  126. location,
  127. data: [catalogProvider("openai", "OpenAI")],
  128. } as never)
  129. spyOn(sdk.model, "list").mockResolvedValue({
  130. location,
  131. data: [catalogModel({ id: "gpt-5", providerID: "openai", variants: ["high", "minimal"] })],
  132. } as never)
  133. await expect(resolveModelInfo(sdk, { directory: "/workspace" })).resolves.toEqual({
  134. providers: [
  135. {
  136. id: "openai",
  137. name: "OpenAI",
  138. models: {
  139. "gpt-5": {
  140. name: "gpt-5",
  141. cost: {
  142. input: 0,
  143. },
  144. limit: {
  145. context: 128_000,
  146. },
  147. status: "active",
  148. variants: {
  149. high: {},
  150. minimal: {},
  151. },
  152. },
  153. },
  154. },
  155. ],
  156. })
  157. expect(providerList).toHaveBeenCalledWith({
  158. location: {
  159. directory: "/workspace",
  160. },
  161. })
  162. })
  163. })