| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232 |
- import { describe, expect, test } from "bun:test"
- import { ClientError, OpenCode } from "@opencode-ai/client/promise"
- import { OPENCODE_VERSION } from "../src/version"
- import path from "node:path"
- import { createMiniConnection, mergeInput as mergeInteractiveInput, resolveMiniTarget } from "../src/mini"
- import { mergeInput as mergeNonInteractiveInput, parseRunModel } from "../src/run/run"
- import { parseSessionTargetModel } from "../src/session-target"
- async function cli(args: string[]) {
- const child = Bun.spawn([process.execPath, "run", "src/index.ts", ...args], {
- cwd: path.join(import.meta.dir, ".."),
- stdout: "pipe",
- stderr: "pipe",
- })
- const [stdout, stderr, exitCode] = await Promise.all([
- new Response(child.stdout).text(),
- new Response(child.stderr).text(),
- child.exited,
- ])
- return { stdout, stderr, exitCode }
- }
- describe("mini command", () => {
- test("uses piped stdin as the initial prompt", () => {
- expect(mergeInteractiveInput("from stdin", undefined)).toBe("from stdin")
- expect(mergeInteractiveInput("from stdin", "from flag")).toBe("from stdin\nfrom flag")
- })
- test("constructs a fresh authenticated client for a replacement endpoint", async () => {
- const authorization: Array<string | null> = []
- const initial = Bun.serve({
- port: 0,
- fetch() {
- return Response.json({ healthy: true, version: OPENCODE_VERSION, pid: process.pid })
- },
- })
- const replacement = Bun.serve({
- port: 0,
- fetch(request) {
- authorization.push(request.headers.get("authorization"))
- return Response.json({ healthy: true, version: OPENCODE_VERSION, pid: process.pid })
- },
- })
- const controller = new AbortController()
- let signal: AbortSignal | undefined
- try {
- const connection = createMiniConnection({
- endpoint: { url: initial.url.toString() },
- reconnect: async (next) => {
- signal = next
- return {
- url: replacement.url.toString(),
- auth: { type: "basic", username: "replacement", password: "secret" },
- }
- },
- })
- const client = await connection.reconnect?.(controller.signal)
- if (!client) throw new Error("Expected a replacement client")
- await client.health.get()
- expect(client).not.toBe(connection.sdk)
- expect(signal).toBe(controller.signal)
- expect(authorization).toEqual([`Basic ${btoa("replacement:secret")}`])
- expect(createMiniConnection({ endpoint: { url: initial.url.toString() } }).reconnect).toBeUndefined()
- } finally {
- initial.stop(true)
- replacement.stop(true)
- }
- })
- test("re-resolves a managed target when the endpoint moves before transport construction", async () => {
- const initial = OpenCode.make({ baseUrl: "https://initial.opencode.test" })
- const replacement = OpenCode.make({ baseUrl: "https://replacement.opencode.test" })
- const controller = new AbortController()
- const seen: (typeof initial)[] = []
- let reconnects = 0
- const result = await resolveMiniTarget({
- sdk: initial,
- reconnect: async (signal) => {
- expect(signal).toBe(controller.signal)
- reconnects++
- if (reconnects === 1) throw new Error("service still moving")
- return replacement
- },
- signal: controller.signal,
- resolve: async (sdk) => {
- seen.push(sdk)
- if (sdk === initial) throw new ClientError("Transport")
- return "ses-replacement"
- },
- })
- expect(seen).toEqual([initial, replacement])
- expect(reconnects).toBe(2)
- expect(result).toEqual({ sdk: replacement, value: "ses-replacement" })
- })
- test("merges non-interactive argument and stdin input", () => {
- expect(mergeNonInteractiveInput("from args", "from stdin")).toBe("from args\nfrom stdin")
- expect(mergeNonInteractiveInput(undefined, "from stdin")).toBe("from stdin")
- })
- test("parses model variants from the model reference", () => {
- expect(JSON.stringify(parseRunModel("openrouter/openai/gpt-5#high"))).toBe(
- JSON.stringify({ model: { providerID: "openrouter", modelID: "openai/gpt-5" }, variant: "high" }),
- )
- expect(parseSessionTargetModel("openrouter/openai/gpt-5#high")).toEqual({
- providerID: "openrouter",
- id: "openai/gpt-5",
- variant: "high",
- })
- })
- test("is registered in the preview CLI", async () => {
- const result = await cli(["--help"])
- expect(result.exitCode).toBe(0)
- expect(result.stdout).toContain("mini Start the minimal interactive interface")
- expect(result.stdout).toContain("run Run OpenCode with a message")
- })
- test("exposes run without legacy interactive, attach, or command modes", async () => {
- const result = await cli(["run", "--help"])
- expect(result.exitCode).toBe(0)
- expect(result.stdout).toContain("--server string")
- expect(result.stdout).not.toContain("--interactive")
- expect(result.stdout).not.toContain("--variant")
- expect(result.stdout).not.toContain("--attach")
- expect(result.stdout).not.toContain("--command")
- })
- test("keeps option-like prompt text after the argument separator", async () => {
- const result = await cli(["run", "--server", "http://127.0.0.1:1", "--", "--foo"])
- expect(result.exitCode).toBe(1)
- expect(result.stderr).not.toContain("You must provide a message")
- })
- test("passes explicit selections to session creation without catalog preflight", async () => {
- const requests: string[] = []
- let session: unknown
- const server = Bun.serve({
- port: 0,
- async fetch(request) {
- const url = new URL(request.url)
- requests.push(url.pathname)
- if (url.pathname === "/api/health")
- return Response.json({ healthy: true, version: OPENCODE_VERSION, pid: process.pid })
- if (url.pathname === "/api/location")
- return Response.json({ directory: process.cwd(), project: { id: "global", directory: process.cwd() } })
- if (url.pathname === "/api/session") {
- session = await request.json()
- return new Response("boom", { status: 500 })
- }
- return new Response(undefined, { status: 404 })
- },
- })
- try {
- const result = await cli([
- "run",
- "--server",
- server.url.toString(),
- "--model",
- "definitely/missing",
- "--agent",
- "definitely-missing",
- "hi",
- ])
- expect(result.exitCode).toBe(1)
- expect(result.stderr).toContain("UnexpectedStatus")
- expect(session).toMatchObject({
- agent: "definitely-missing",
- model: { providerID: "definitely", id: "missing" },
- })
- expect(requests).not.toContain("/api/model")
- expect(requests).not.toContain("/api/agent")
- expect(requests).not.toContain("/api/location/wait")
- } finally {
- server.stop(true)
- }
- })
- test("reports pre-admission errors as JSON", async () => {
- const server = Bun.serve({
- port: 0,
- fetch(request) {
- if (new URL(request.url).pathname === "/api/session") return new Response("boom", { status: 500 })
- return Response.json({ healthy: true, version: "incompatible", pid: process.pid })
- },
- })
- try {
- const result = await cli(["run", "--format", "json", "--server", server.url.toString(), "hi"])
- expect(result.exitCode).toBe(1)
- expect(JSON.parse(result.stdout)).toMatchObject({
- type: "error",
- sessionID: "",
- error: { type: "unknown", message: "UnexpectedStatus" },
- })
- } finally {
- server.stop(true)
- }
- })
- test("uses the shared V2 server option instead of an attach command", async () => {
- const result = await cli(["mini", "--help"])
- expect(result.exitCode).toBe(0)
- expect(result.stdout).toContain("--server string")
- expect(result.stdout).toContain("--prompt string")
- expect(result.stdout).toContain("--replay")
- expect(result.stdout).toContain("disable with --no-replay")
- expect(result.stdout).toContain("--replay-limit integer")
- expect(result.stdout).toContain("Limit replay to the newest N messages (default: 200)")
- expect(result.stdout).not.toContain("SUBCOMMANDS")
- })
- test("routes local and explicit-server invocations into mini", async () => {
- for (const args of [["mini"], ["mini", "--no-replay"], ["mini", "--server", "http://127.0.0.1:1"]]) {
- const result = await cli(args)
- expect(result.exitCode).toBe(1)
- expect(result.stderr).toContain("opencode mini requires a TTY stdout")
- }
- })
- })
|