runtime.boot.test.ts 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  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,alt+return,ctrl+j")
  79. })
  80. test("preserves disabled leader from resolved tui config", async () => {
  81. const result = await resolveRunTuiConfig(config({ leader: "none" }))
  82. expect(result.keybinds.get("leader")).toEqual([])
  83. })
  84. test("preserves shared config while resolving independent Mini defaults", async () => {
  85. const result = await resolveRunTuiConfig(
  86. createTuiResolvedConfig({
  87. theme: { mode: "light" },
  88. leader: { timeout: 450 },
  89. }),
  90. )
  91. expect(result.theme).toEqual({ mode: "light" })
  92. expect(result.leader.timeout).toBe(450)
  93. expect(resolveMiniSettings(result)).toEqual({
  94. thinking: "hide",
  95. shell_output: "hide",
  96. turn_summary: "show",
  97. footer: "show",
  98. splash: "show",
  99. mono: false,
  100. })
  101. expect(
  102. resolveMiniSettings({
  103. mini: {
  104. thinking: "show",
  105. shell_output: "show",
  106. turn_summary: "hide",
  107. footer: "hide",
  108. splash: "hide",
  109. mono: true,
  110. },
  111. }),
  112. ).toEqual({
  113. thinking: "show",
  114. shell_output: "show",
  115. turn_summary: "hide",
  116. footer: "hide",
  117. splash: "hide",
  118. mono: true,
  119. })
  120. })
  121. test("loads v2 providers and models for model selector data", async () => {
  122. const sdk = OpenCode.make({ baseUrl: "https://opencode.test" })
  123. const location = { directory: "/workspace", project: { id: "proj_1", directory: "/workspace" } }
  124. const providerList = spyOn(sdk.provider, "list").mockResolvedValue({
  125. location,
  126. data: [catalogProvider("openai", "OpenAI")],
  127. } as never)
  128. spyOn(sdk.model, "list").mockResolvedValue({
  129. location,
  130. data: [catalogModel({ id: "gpt-5", providerID: "openai", variants: ["high", "minimal"] })],
  131. } as never)
  132. await expect(resolveModelInfo(sdk, { directory: "/workspace" })).resolves.toEqual({
  133. providers: [
  134. {
  135. id: "openai",
  136. name: "OpenAI",
  137. models: {
  138. "gpt-5": {
  139. name: "gpt-5",
  140. cost: {
  141. input: 0,
  142. },
  143. limit: {
  144. context: 128_000,
  145. },
  146. status: "active",
  147. variants: {
  148. high: {},
  149. minimal: {},
  150. },
  151. },
  152. },
  153. },
  154. ],
  155. })
  156. expect(providerList).toHaveBeenCalledWith({
  157. location: {
  158. directory: "/workspace",
  159. },
  160. })
  161. })
  162. })