From 16d10fcd09ed9d32f140b0b751f2cf17a579a8bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Andr=C3=A9?= Date: Thu, 17 Sep 2026 22:09:47 +0200 Subject: [PATCH] fix(opencode): discover server info while preserving legacy health OpenCode 2.0.7 exposes /api/info instead of /api/status or /api/health. Extend authenticated host and WSL lifecycle discovery with an info probe only after both earlier routes return 404. Preserve the shared deadline, response bounds, schema validation and credential handling, and reject redirects. Older V2 services continue to connect through health without a new minimum-version gate. Record the discovered route on the connection and translate the pinned client's GET server.status call to info. Keep structural OpenAPI negotiation for unknown runtimes, and do not retry failed API operations or translate mutations. Add host/WSL discovery, malformed-response, cancellation/deadline and generated-client regressions. Exercise production lifecycle probes in the isolated native location fixture. All 44 focused tests and server typecheck pass; real isolated OpenCode 2.0.0 and 2.0.7 pass discovery, canonical status and their native location scenarios. Update integration references and record the route audit and validation boundaries. This follow-up to #695 is independent of #649 worktree catalogue changes. --- .../references/sdk-api-reference.md | 4 +- .../references/sdk-critical-behaviors.md | 2 +- dev-docs/OPENCODE_V2_COMPATIBILITY.md | 27 +++++++++ .../compatibility/contract-fixture.ts | 15 ++++- .../src/opencode/compatibility/runtime.ts | 2 +- .../opencode/compatibility/transport.test.ts | 41 +++++++++++++ .../src/opencode/compatibility/transport.ts | 3 + .../workspaces/host-opencode-service.test.ts | 60 ++++++++++++++++++- .../src/workspaces/opencode-cli-service.ts | 14 ++++- .../workspaces/wsl-opencode-service.test.ts | 19 +++++- scripts/test-opencode-location-native.mjs | 19 +++--- 11 files changed, 186 insertions(+), 20 deletions(-) diff --git a/.opencode/skills/codenomad-architecture-guide/references/sdk-api-reference.md b/.opencode/skills/codenomad-architecture-guide/references/sdk-api-reference.md index b579004aa..d1d811295 100644 --- a/.opencode/skills/codenomad-architecture-guide/references/sdk-api-reference.md +++ b/.opencode/skills/codenomad-architecture-guide/references/sdk-api-reference.md @@ -2,7 +2,7 @@ ## Package -CodeNomad server and UI pin `@opencode/client@2.0.4`. The runtime CLI is managed independently; startup validates authenticated loopback `/api/status`, falling back only on HTTP 404 to earlier V2 `/api/health` with the same endpoint, credentials and deadline. Each response has its own validated schema and a 64 KiB bound. Discovery does not prove compatibility for other APIs. Review official V2 docs, installed declarations, generated routes and native regression tests together when upgrading. +CodeNomad server and UI pin `@opencode/client@2.0.4`. The runtime CLI is managed independently; startup validates authenticated loopback `/api/status`, then `/api/health`, then `/api/info`, advancing only on HTTP 404 with the same endpoint, credentials and deadline. Each response has its own validated schema and a 64 KiB bound. The shared transport maps canonical `server.status()` to the discovered route. Discovery does not prove compatibility for other APIs. Review official V2 docs, installed declarations, generated routes and native regression tests together when upgrading. Cross-runtime adaptation lives in `packages/server/src/opencode/compatibility/`. The shared connection binds authenticated runtime identity, the canonical client and forwarding transport. Known published contracts select their serializer directly; unknown versions require authenticated bounded OpenAPI recognition before calls. Never add operation-specific retry fallbacks in UI stores or Yolo. See `dev-docs/OPENCODE_V2_COMPATIBILITY.md` for the evidence matrix and maintained issue register. @@ -17,7 +17,7 @@ Do not replace the shared network service with `@opencode-ai/sdk` unless CodeNom | Area | Calls | CodeNomad caller | |---|---|---| -| Service | CLI `service status/start/get password`; authenticated `/api/status`, then `/api/health` only on 404; `Service.headers` for probes and API calls | `packages/server/src/workspaces/opencode-service.ts`, `packages/server/src/workspaces/opencode-cli-service.ts`, `packages/server/src/workspaces/host-opencode-service.ts`, `packages/server/src/workspaces/wsl-opencode-service.ts` | +| Service | CLI `service status/start/get password`; authenticated `/api/status`, `/api/health`, `/api/info` in order, advancing only on 404; `Service.headers` for probes and API calls | `packages/server/src/workspaces/opencode-service.ts`, `packages/server/src/workspaces/opencode-cli-service.ts`, `packages/server/src/workspaces/host-opencode-service.ts`, `packages/server/src/workspaces/wsl-opencode-service.ts` | | Location | `client.location.get`, `client.debug.location.evict` | shared service wrapper | | Events | `client.event.subscribe()` | `packages/server/src/workspaces/instance-events.ts` | | Sessions | `list/get/create/fork/remove/update/prompt/command/shell/interrupt` | UI session stores | diff --git a/.opencode/skills/codenomad-architecture-guide/references/sdk-critical-behaviors.md b/.opencode/skills/codenomad-architecture-guide/references/sdk-critical-behaviors.md index f3bd498d4..de4e5d7da 100644 --- a/.opencode/skills/codenomad-architecture-guide/references/sdk-critical-behaviors.md +++ b/.opencode/skills/codenomad-architecture-guide/references/sdk-critical-behaviors.md @@ -2,7 +2,7 @@ ## Contract -- Server and UI pin `@opencode/client@2.0.4`. Manage the runtime CLI independently: startup checks authenticated loopback `/api/status`, falling back only on HTTP 404 to earlier V2 `/api/health`. Both probes share the endpoint, credentials, 64 KiB response bound and absolute deadline; authentication, transport and malformed status failures do not trigger fallback. Discovery alone does not prove client/API compatibility. Review documentation, installed declarations and proxy/API parity whenever the client contract changes. +- Server and UI pin `@opencode/client@2.0.4`. Manage the runtime CLI independently: startup checks authenticated loopback `/api/status`, then `/api/health`, then `/api/info`, advancing only on HTTP 404. All probes share the endpoint, credentials, 64 KiB response bound and absolute deadline; authentication, transport and malformed response failures do not trigger fallback. The shared transport maps canonical `server.status()` to the discovered route. Discovery alone does not prove client/API compatibility. Review documentation, installed declarations and proxy/API parity whenever the client contract changes. - The package root is the generated zero-Effect Promise client. Use installed declarations, not current public `@opencode-ai/sdk` examples. - Native routes are `/api/*`; CodeNomad exposes them only through the authorized `/workspaces/:id/instance` proxy. - That proxy is an explicit method/path allowlist. Future upstream APIs are not exposed automatically. diff --git a/dev-docs/OPENCODE_V2_COMPATIBILITY.md b/dev-docs/OPENCODE_V2_COMPATIBILITY.md index 508e4e952..07fdb1545 100644 --- a/dev-docs/OPENCODE_V2_COMPATIBILITY.md +++ b/dev-docs/OPENCODE_V2_COMPATIBILITY.md @@ -80,6 +80,33 @@ The native fixture now resolves its temporary root with `realpath` **before** co The corrected full native suite passes on Linux/WSL 2.0.4 with a symlinked TEMP root, and the Windows 2.0.4 rendered/native suite passes. Script syntax, workflow YAML parsing and whitespace checks pass; an independent gatekeeper review reports no actionable findings. This corrects the fixture namespace rather than preinstalling the plugin, restarting the daemon, or extending the timeout. Actual macOS confirmation requires the next CI run; the prior runner's raw watcher paths were not retained. Existing user daemons configured through symlinked config roots remain an upstream watcher limitation, not a production fix claimed by this fixture change. +### Server-info discovery follow-up (2026-09-17) + +The installed 2.0.7 daemon returns 404 for both `/api/status` and `/api/health`, +and exposes authenticated `/api/info`. Lifecycle validation now tries the three +read routes in that order, advancing only on 404 and sharing the same credentials, +response-size bound and absolute deadline. Redirects are rejected. Earlier V2 +services still stop at `/api/health`; no minimum release or new version exception +is introduced. Unknown versions still negotiate their actual OpenAPI contract. + +The connection records `info` discovery so canonical `client.server.status()` +uses `/api/info` too. Translation is limited to GET and does not retry a failed +request with another route. A comparison of the 132 pinned-client HTTP method/path +pairs with the installed 2.0.7 schema found 131 unchanged pairs and this single +status/info difference. Experimental paths were already part of the merged modern +contract; this is not another experimental-route migration. + +Host/WSL regressions cover the third probe, credentials, body validation and shared +deadline. Client regressions cover 2.0.0 health, 2.0.4 status, negotiated 2.0.7 info +and an unknown future version. The isolated location fixture exercises production +lifecycle validation and canonical `client.server.status()` before location tests. +On Windows, that fixture passes against real isolated 2.0.0 and 2.0.7 daemons: +2.0.0 takes the health route and passes the legacy identity/Form/Shell/PTY/SSE/ +cursor/rollback cases; 2.0.7 takes the info route and passes modern location and +obsolete-selector checks. The 44 focused lifecycle/transport/negotiation tests +and server typecheck also pass. +Route presence and these targeted checks do not certify every runtime behavior. + ### Validation boundaries UI/server/Electron typechecks and production builds pass. The Windows Tauri release executable also builds (`npm run build --workspace @codenomad/tauri-app -- --no-bundle`). Electron and Tauri packaged-resource smoke checks pass. Full desktop interaction through Developer Mode could not run: the visible application reports Developer Mode inactive. Interactive TUI validation is not claimed. No shared daemon or user database is used by the native fixtures. diff --git a/packages/server/src/opencode/compatibility/contract-fixture.ts b/packages/server/src/opencode/compatibility/contract-fixture.ts index 11eaa938d..2407f3675 100644 --- a/packages/server/src/opencode/compatibility/contract-fixture.ts +++ b/packages/server/src/opencode/compatibility/contract-fixture.ts @@ -1,4 +1,4 @@ -// Minimal structural excerpts from the published earlier V2 OpenAPI document. +// Minimal structural excerpts from the published V2 OpenAPI contracts. // For test fixtures only; real-runtime tests also negotiate full native schemas. export const legacyContractFixture = { paths: { @@ -12,3 +12,16 @@ export const legacyContractFixture = { }, components: { schemas: { "Session.Inbox.User": { properties: { timeCreated: { type: "number" } } } } }, } + +export const modernContractFixture = { + paths: { + "/api/session/{sessionID}": { patch: {} }, + "/api/form": { get: {} }, + "/api/session/{sessionID}/form/{formID}": { delete: {} }, + "/api/experimental/session/{sessionID}/wait": { post: {} }, + "/api/session/{sessionID}/fork": { post: { requestBody: { content: { "application/json": { schema: { properties: { before: {} } } } } } } }, + "/api/session/{sessionID}/command": { post: { requestBody: { content: { "application/json": { schema: { properties: { name: {} } } } } } } }, + "/api/session/{sessionID}/permission/{requestID}/reply": { post: { requestBody: { content: { "application/json": { schema: { properties: { decision: {} } } } } } } }, + }, + components: { schemas: { "Session.Inbox.User": { properties: { time: { type: "object" } } } } }, +} diff --git a/packages/server/src/opencode/compatibility/runtime.ts b/packages/server/src/opencode/compatibility/runtime.ts index 5354518de..f914a5d84 100644 --- a/packages/server/src/opencode/compatibility/runtime.ts +++ b/packages/server/src/opencode/compatibility/runtime.ts @@ -3,7 +3,7 @@ import type { Endpoint } from "@opencode/client/service" export interface RuntimeIdentity { version: string pid: number - discovery: "status" | "health" + discovery: "status" | "health" | "info" /** Filled only by a successful, authenticated schema negotiation. */ contract?: { profile?: Exclude } } diff --git a/packages/server/src/opencode/compatibility/transport.test.ts b/packages/server/src/opencode/compatibility/transport.test.ts index 263ceff68..3dde1efe5 100644 --- a/packages/server/src/opencode/compatibility/transport.test.ts +++ b/packages/server/src/opencode/compatibility/transport.test.ts @@ -6,6 +6,47 @@ import { OpenCode } from "@opencode/client" import type { Endpoint } from "@opencode/client/service" import { rememberRuntime } from "./runtime" import { createRuntimeFetch } from "./transport" +import { modernContractFixture } from "./contract-fixture" + +test("the pinned status client follows the discovered route across legacy, modern and info runtimes", async () => { + for (const [version, discovery, route] of [ + ["2.0.0", "health", "/api/health"], + ["2.0.4", "status", "/api/status"], + ["2.0.7", "info", "/api/info"], + ["future-release", "info", "/api/info"], + ] as const) { + const endpoint: Endpoint = { url: "http://127.0.0.1:4321", auth: { type: "basic", username: "opencode", password: "fixture" } } + rememberRuntime(endpoint, { version, pid: 123, discovery }) + const seen: string[] = [] + const fetch = createRuntimeFetch(endpoint, async (input, init) => { + const path = new URL(String(input)).pathname + seen.push(path) + assert.equal(init?.redirect, "error") + assert.equal(new Headers(init?.headers).get("authorization"), `Basic ${Buffer.from("opencode:fixture").toString("base64")}`) + if (path === "/openapi.json") return Response.json(modernContractFixture) + assert.equal(path, route) + return Response.json(discovery === "health" ? { healthy: true, version, pid: 123 } : { version, pid: 123, urls: [endpoint.url] }) + }) + const client = OpenCode.make({ baseUrl: endpoint.url, fetch }) + assert.deepEqual(await client.server.status(), { version, pid: 123, urls: [endpoint.url] }) + assert.deepEqual(seen, discovery === "info" ? ["/openapi.json", route] : [route]) + } +}) + +test("info translation is GET-only and does not probe alternate routes on failure", async () => { + const endpoint: Endpoint = { url: "http://127.0.0.1:4321" } + rememberRuntime(endpoint, { version: "2.0.7", pid: 123, discovery: "info" }) + const seen: string[] = [] + const fetch = createRuntimeFetch(endpoint, async (input, init) => { + const path = new URL(String(input)).pathname + seen.push(`${init?.method ?? "GET"} ${path}`) + return path === "/openapi.json" ? Response.json(modernContractFixture) : new Response(null, { status: 404 }) + }) + const client = OpenCode.make({ baseUrl: endpoint.url, fetch }) + await assert.rejects(client.server.status()) + await fetch(`${endpoint.url}/api/status`, { method: "POST" }) + assert.deepEqual(seen, ["GET /openapi.json", "GET /api/info", "POST /api/status"]) +}) test("the pinned client uses the earlier wire contract and retains admission/list timestamps", async () => { const seen: Array<{ method: string; path: string; body: unknown }> = [] diff --git a/packages/server/src/opencode/compatibility/transport.ts b/packages/server/src/opencode/compatibility/transport.ts index c9c364b46..f8d177c39 100644 --- a/packages/server/src/opencode/compatibility/transport.ts +++ b/packages/server/src/opencode/compatibility/transport.ts @@ -48,6 +48,9 @@ export function createRuntimeTransport(endpoint: Endpoint, fetcher: typeof fetch // Credentialed native requests must never follow a redirect to another host. const options: RequestInit = { method: request.method, headers, signal: AbortSignal.any([request.signal, lifetime]), redirect: "error" } const originalPath = url.pathname + if (originalPath === "/api/status" && request.method === "GET" && identity?.discovery === "info") { + url.pathname = "/api/info" + } if (profile === "legacy" || headers.has(LOCATION_CONTEXT_HEADER)) { const text = request.body ? await request.text() : undefined let body: unknown = text ? JSON.parse(text) : undefined diff --git a/packages/server/src/workspaces/host-opencode-service.test.ts b/packages/server/src/workspaces/host-opencode-service.test.ts index 84b5d9238..30c462d37 100644 --- a/packages/server/src/workspaces/host-opencode-service.test.ts +++ b/packages/server/src/workspaces/host-opencode-service.test.ts @@ -4,6 +4,7 @@ import { describe, it } from "node:test" import { HostOpenCodeService, hostOpenCodeServiceIdentity } from "./host-opencode-service" import type { OpenCodeCliServiceDependencies, ServiceExecOptions } from "./opencode-cli-service" import { OPENCODE_V2_REQUIRED_ERROR_CODE } from "../api-types" +import { runtimeIdentity } from "../opencode/compatibility/runtime" const url = "http://127.0.0.1:4321" @@ -79,7 +80,7 @@ describe("HostOpenCodeService", () => { requests.push(String(input)) assert.equal(new Headers(init?.headers).get("authorization"), `Basic ${Buffer.from("opencode:password").toString("base64")}`) if (requests.length === 1) return new Response(new ReadableStream({ cancel() { cancelled = true } }), { status: 404 }) - return Response.json({ healthy: true, version: "2.0.3", pid: 123 }) + return Response.json({ healthy: true, version: "2.0.0", pid: 123 }) }, }) assert.equal((await service[operation]())?.url, url) @@ -88,6 +89,63 @@ describe("HostOpenCodeService", () => { } }) + it("discovers server.info by route presence for any version on discovery and startup", async () => { + for (const operation of ["discover", "ensure"] as const) { + for (const version of ["2.0.7", "future-release"]) { + const requests: string[] = [] + let cancelled = 0 + const service = createService([], {}, { + execFile: async (_file, args) => ({ stdout: args.at(-1) === "password" ? "password\n" : `${url}\n`, stderr: "" }), + fetch: async (input, init) => { + requests.push(String(input)) + assert.equal(new Headers(init?.headers).get("authorization"), `Basic ${Buffer.from("opencode:password").toString("base64")}`) + assert.equal(init?.redirect, "error") + return String(input).endsWith("/api/info") + ? Response.json({ version, pid: 123, urls: [url], paths: { tmp: "/tmp/opencode" } }) + : new Response(new ReadableStream({ cancel() { cancelled++ } }), { status: 404 }) + }, + }) + const endpoint = await service[operation]() + assert.equal(runtimeIdentity(endpoint!)?.discovery, "info") + assert.deepEqual(requests, [`${url}/api/status`, `${url}/api/health`, `${url}/api/info`]) + assert.equal(cancelled, 2) + } + } + }) + + it("validates info responses and preserves the final probe deadline", async (context) => { + for (const response of [ + () => new Response(null, { status: 401 }), + () => new Response(null, { status: 404 }), + () => new Response(null, { status: 503 }), + () => new Response("invalid JSON"), + () => Response.json({ version: "2.0.7", pid: 123 }), + () => Response.json({ version: "2.0.7", pid: -1, urls: [url] }), + () => new Response(" ".repeat(64 * 1024 + 1)), + ]) { + const requests: string[] = [] + const service = createService([], {}, { + fetch: async (input) => { + requests.push(String(input)) + return requests.length < 3 ? new Response(null, { status: 404 }) : response() + }, + }) + await assert.rejects(service.ensure()) + assert.deepEqual(requests, [`${url}/api/status`, `${url}/api/health`, `${url}/api/info`]) + } + let now = 1000 + context.mock.method(Date, "now", () => now) + const requests: string[] = [] + const service = createService([], {}, { + fetch: async (input) => { + requests.push(String(input)) + return new Response(new ReadableStream({ cancel() { if (requests.length === 2) now = 1500 } }), { status: 404 }) + }, + }) + await assert.rejects(service.ensure(1500), /timed out/) + assert.deepEqual(requests, [`${url}/api/status`, `${url}/api/health`]) + }) + it("does not downgrade on authentication, server, transport or malformed status failures", async () => { for (const response of [ () => new Response(null, { status: 401 }), diff --git a/packages/server/src/workspaces/opencode-cli-service.ts b/packages/server/src/workspaces/opencode-cli-service.ts index 27ace623a..04477d925 100644 --- a/packages/server/src/workspaces/opencode-cli-service.ts +++ b/packages/server/src/workspaces/opencode-cli-service.ts @@ -121,7 +121,7 @@ export class OpenCodeCliService implements OpenCodeServiceLifecycle { } private async validateStatus(endpoint: Endpoint, deadlineAt: number): Promise { - let kind: "status" | "health" = "status" + let kind: "status" | "health" | "info" = "status" let response = await this.fetchServiceStatus(endpoint, kind, deadlineAt) // Earlier V2 runtimes expose health instead of status. Negotiate only on // route absence, using the same authenticated endpoint and deadline. @@ -130,6 +130,13 @@ export class OpenCodeCliService implements OpenCodeServiceLifecycle { kind = "health" response = await this.fetchServiceStatus(endpoint, kind, deadlineAt) } + // The current service exposes server.info. Discover by route presence, + // never by a release-number allowlist, and only retry this read on 404. + if (response.status === 404) { + await this.withDeadline(response.body?.cancel().catch(() => undefined) ?? Promise.resolve(), deadlineAt, "health response") + kind = "info" + response = await this.fetchServiceStatus(endpoint, kind, deadlineAt) + } if (response.status === 401) { throw new Error(`${this.options.label} OpenCode service authentication failed at ${endpoint.url} (HTTP 401)`) } @@ -148,18 +155,19 @@ export class OpenCodeCliService implements OpenCodeServiceLifecycle { } catch { throw new Error(`${this.options.label} OpenCode service returned an invalid ${kind} response at ${endpoint.url}`) } - if (!(kind === "status" ? isServiceStatusResponse(payload) : isServiceHealthResponse(payload))) { + if (!(kind === "health" ? isServiceHealthResponse(payload) : isServiceStatusResponse(payload))) { throw new Error(`${this.options.label} OpenCode service returned an invalid ${kind} response at ${endpoint.url}`) } const { version, pid } = payload as { version: string; pid: number } rememberRuntime(endpoint, { version, pid, discovery: kind }) } - private async fetchServiceStatus(endpoint: Endpoint, kind: "status" | "health", deadlineAt: number): Promise { + private async fetchServiceStatus(endpoint: Endpoint, kind: "status" | "health" | "info", deadlineAt: number): Promise { try { const timeout = this.remaining(deadlineAt, `${kind} validation`) return await this.withDeadline(this.dependencies.fetch(new URL(`/api/${kind}`, endpoint.url), { headers: Service.headers(endpoint), + redirect: "error", signal: AbortSignal.timeout(timeout), }), deadlineAt, `${kind} validation`) } catch { diff --git a/packages/server/src/workspaces/wsl-opencode-service.test.ts b/packages/server/src/workspaces/wsl-opencode-service.test.ts index 1c294c1f4..71f9635c4 100644 --- a/packages/server/src/workspaces/wsl-opencode-service.test.ts +++ b/packages/server/src/workspaces/wsl-opencode-service.test.ts @@ -164,9 +164,24 @@ describe("WslOpenCodeService", () => { assert.deepEqual(requests, [`${url}/api/status`, `${url}/api/health`]) }) + it("falls back to authenticated Windows info for a current WSL V2 service", async () => { + const requests: string[] = [] + const endpoint = await harness({ status: `${url}\n`, password: "secret\n" }, { + fetch: async (input, init) => { + requests.push(String(input)) + assert.equal(new Headers(init?.headers).get("authorization"), `Basic ${Buffer.from("opencode:secret").toString("base64")}`) + assert.equal(init?.redirect, "error") + return requests.length < 3 ? new Response(null, { status: 404 }) + : Response.json({ version: "2.0.7", pid: 123, urls: [url] }) + }, + }).service.discover() + assert.equal(endpoint?.url, url) + assert.deepEqual(requests, [`${url}/api/status`, `${url}/api/health`, `${url}/api/info`]) + }) + it("rejects missing, unauthenticated, malformed and oversized fallback health responses", async () => { for (const [response, expected] of [ - [() => new Response(null, { status: 404 }), /health check failed.*404/], + [() => new Response(null, { status: 404 }), /info check failed.*404/], [() => new Response(null, { status: 401 }), /authentication failed.*401/], [() => new Response(null, { status: 503 }), /health check failed.*503/], [() => new Response("invalid JSON"), /invalid health response/], @@ -186,7 +201,7 @@ describe("WslOpenCodeService", () => { return requests.length === 1 ? new Response(null, { status: 404 }) : response() }, }).service.discover(), expected) - assert.deepEqual(requests, [`${url}/api/status`, `${url}/api/health`]) + assert.deepEqual(requests, [`${url}/api/status`, `${url}/api/health`, ...(response().status === 404 ? [`${url}/api/info`] : [])]) } }) diff --git a/scripts/test-opencode-location-native.mjs b/scripts/test-opencode-location-native.mjs index 2ddba9c03..b62ecb048 100644 --- a/scripts/test-opencode-location-native.mjs +++ b/scripts/test-opencode-location-native.mjs @@ -209,20 +209,21 @@ async function runIsolated(cli) { await delay(20) } const baseUrl = output.match(/http:\/\/127\.0\.0\.1:\d+/)[0] - const endpoint = { url: baseUrl, auth: { type: "basic", username: "opencode", password } } - const authorization = `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}` - let discovery = "status" - let response = await fetch(`${baseUrl}/api/status`, { headers: { authorization } }) - if (response.status === 404) { discovery = "health"; response = await fetch(`${baseUrl}/api/health`, { headers: { authorization } }) } - assert.equal(response.status, 200) - const identity = await response.json() - const { rememberRuntime } = await tsImport("../packages/server/src/opencode/compatibility/runtime.ts", import.meta.url) + const { OpenCodeCliService } = await tsImport("../packages/server/src/workspaces/opencode-cli-service.ts", import.meta.url) + const { runtimeIdentity } = await tsImport("../packages/server/src/opencode/compatibility/runtime.ts", import.meta.url) + // Replace only CLI discovery; exercise production authenticated health probes. + const lifecycle = new OpenCodeCliService({ label: "Fixture", timeoutMs: 30_000, + command: args => ({ command: cli, args, options: {} }), + }, { execFile: async (_file, args) => ({ stdout: args.at(-1) === "password" ? password : baseUrl, stderr: "" }) }) + const endpoint = await lifecycle.discover() + const identity = runtimeIdentity(endpoint) const { createRuntimeTransport } = await tsImport("../packages/server/src/opencode/compatibility/transport.ts", import.meta.url) - rememberRuntime(endpoint, { version: identity.version, pid: identity.pid, discovery }) const transport = createRuntimeTransport(endpoint) const client = OpenCode.make({ baseUrl, fetch: transport.fetch }) const connection = { endpoint, client, ...transport, assertCurrent() {}, invalidate() {} } console.log(`Testing official runtime ${identity.version}`) + assert.equal((await client.server.status()).version, identity.version) + console.log(`PASS: production ${identity.discovery} discovery and canonical client.server.status()`) await testNativeLocationIdentity({ client, connection, root }) } finally { child.kill()