mini.test.ts 5.2 KB

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