mini.test.ts 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. import { describe, expect, test } from "bun:test"
  2. import { InstallationVersion } from "@opencode-ai/core/installation/version"
  3. import path from "node:path"
  4. import { mergeInteractiveInput, mergeNonInteractiveInput, parseRunModel, pickRunModel } from "../src/mini"
  5. import { toolInlineInfo, toolView } from "../src/mini/tool"
  6. async function cli(args: string[]) {
  7. const child = Bun.spawn([process.execPath, "run", "src/index.ts", ...args], {
  8. cwd: path.join(import.meta.dir, ".."),
  9. stdout: "pipe",
  10. stderr: "pipe",
  11. })
  12. const [stdout, stderr, exitCode] = await Promise.all([
  13. new Response(child.stdout).text(),
  14. new Response(child.stderr).text(),
  15. child.exited,
  16. ])
  17. return { stdout, stderr, exitCode }
  18. }
  19. describe("mini command", () => {
  20. test("renders the renamed shell tool with the shell rule", () => {
  21. const part = {
  22. id: "part-shell",
  23. sessionID: "session-shell",
  24. messageID: "message-shell",
  25. callID: "call-shell",
  26. tool: "shell",
  27. state: {
  28. status: "pending" as const,
  29. input: { command: "pwd" },
  30. },
  31. } as const
  32. expect(toolView(part.tool)).toEqual({ output: true, final: false })
  33. expect(toolInlineInfo(part)).toMatchObject({ icon: "$", title: "pwd", mode: "block" })
  34. })
  35. test("uses piped stdin as the initial prompt", () => {
  36. expect(mergeInteractiveInput("from stdin", undefined)).toBe("from stdin")
  37. expect(mergeInteractiveInput("from stdin", "from flag")).toBe("from stdin\nfrom flag")
  38. })
  39. test("keeps run as mini's non-interactive input mode", () => {
  40. expect(mergeNonInteractiveInput("from args", "from stdin")).toBe("from args\nfrom stdin")
  41. expect(mergeNonInteractiveInput(undefined, "from stdin")).toBe("from stdin")
  42. })
  43. test("applies a variant to a resumed session's model", () => {
  44. expect(
  45. pickRunModel(
  46. undefined,
  47. "high",
  48. { providerID: "session-provider", modelID: "session-model" },
  49. { providerID: "default-provider", modelID: "default-model" },
  50. ),
  51. ).toEqual({ providerID: "session-provider", modelID: "session-model" })
  52. })
  53. test("parses model variants from the model reference", () => {
  54. expect(JSON.stringify(parseRunModel("openrouter/openai/gpt-5#high"))).toBe(
  55. JSON.stringify({ model: { providerID: "openrouter", modelID: "openai/gpt-5" }, variant: "high" }),
  56. )
  57. })
  58. test("is registered in the preview CLI", async () => {
  59. const result = await cli(["--help"])
  60. expect(result.exitCode).toBe(0)
  61. expect(result.stdout).toContain("mini Start the minimal interactive interface")
  62. expect(result.stdout).toContain("run Run OpenCode with a message")
  63. })
  64. test("exposes run without legacy attach or command modes", async () => {
  65. const result = await cli(["run", "--help"])
  66. expect(result.exitCode).toBe(0)
  67. expect(result.stdout).toContain("--server string")
  68. expect(result.stdout).not.toContain("--variant")
  69. expect(result.stdout).not.toContain("--attach")
  70. expect(result.stdout).not.toContain("--command")
  71. })
  72. test("keeps option-like prompt text after the argument separator", async () => {
  73. const result = await cli(["run", "--server", "http://127.0.0.1:1", "--", "--foo"])
  74. expect(result.exitCode).toBe(1)
  75. expect(result.stderr).not.toContain("You must provide a message")
  76. })
  77. test("preserves a run failure exit code", async () => {
  78. let modelRequests = 0
  79. const server = Bun.serve({
  80. port: 0,
  81. fetch(request) {
  82. const url = new URL(request.url)
  83. if (url.pathname === "/api/health")
  84. return Response.json({ healthy: true, version: InstallationVersion, pid: process.pid })
  85. if (url.pathname === "/api/location")
  86. return Response.json({ directory: process.cwd(), project: { id: "global", directory: process.cwd() } })
  87. if (url.pathname === "/api/model") {
  88. modelRequests++
  89. return Response.json({
  90. location: { directory: process.cwd(), project: { id: "global", directory: process.cwd() } },
  91. data: modelRequests === 1 ? [{ id: "missing", providerID: "definitely" }] : [],
  92. })
  93. }
  94. return new Response(undefined, { status: 404 })
  95. },
  96. })
  97. try {
  98. const result = await cli([
  99. "run",
  100. "--server",
  101. server.url.toString(),
  102. "--model",
  103. "definitely/missing",
  104. "hi",
  105. ])
  106. expect(result.exitCode).toBe(1)
  107. expect(result.stderr).toContain("Model unavailable: definitely/missing")
  108. } finally {
  109. server.stop(true)
  110. }
  111. })
  112. test("reports pre-admission errors as JSON", async () => {
  113. const server = Bun.serve({
  114. port: 0,
  115. fetch(request) {
  116. if (new URL(request.url).pathname === "/api/session") return new Response("boom", { status: 500 })
  117. return Response.json({ healthy: true, version: "incompatible", pid: process.pid })
  118. },
  119. })
  120. try {
  121. const result = await cli(["run", "--format", "json", "--server", server.url.toString(), "hi"])
  122. expect(result.exitCode).toBe(1)
  123. expect(JSON.parse(result.stdout)).toMatchObject({
  124. type: "error",
  125. sessionID: "",
  126. error: { type: "unknown", message: "UnexpectedStatus" },
  127. })
  128. } finally {
  129. server.stop(true)
  130. }
  131. })
  132. test("uses the shared V2 server option instead of an attach command", async () => {
  133. const result = await cli(["mini", "--help"])
  134. expect(result.exitCode).toBe(0)
  135. expect(result.stdout).toContain("--server string")
  136. expect(result.stdout).not.toContain("SUBCOMMANDS")
  137. })
  138. test("routes local and explicit-server invocations into mini", async () => {
  139. for (const args of [["mini"], ["mini", "--server", "http://127.0.0.1:1"]]) {
  140. const result = await cli(args)
  141. expect(result.exitCode).toBe(1)
  142. expect(result.stderr).toContain("opencode mini requires a TTY stdout")
  143. }
  144. })
  145. })