From b68637bbf0b4a4fd7b465d138e33c44ec31758b4 Mon Sep 17 00:00:00 2001 From: vimtor Date: Tue, 1 Sep 2026 10:18:22 +0200 Subject: [PATCH 1/4] test(server): wait for plugin readiness --- packages/server/test/fetch.test.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/server/test/fetch.test.ts b/packages/server/test/fetch.test.ts index 3074d5916707..ddc5ea488952 100644 --- a/packages/server/test/fetch.test.ts +++ b/packages/server/test/fetch.test.ts @@ -3,7 +3,7 @@ import { createServer } from "node:http" import { makeMemoryDriver } from "@opencode-ai/core/environment/index" import { Workspace } from "@opencode-ai/core/workspace" import { WorkspaceDriver } from "@opencode-ai/core/workspace/driver" -import { Effect } from "effect" +import { Effect, Schedule } from "effect" import { tmpdir } from "../../core/test/fixture/tmpdir" import { it } from "../../core/test/lib/effect" import { ServerFetch } from "../src/fetch" @@ -59,7 +59,20 @@ function occupy(port: number, cancel = false) { } const ready = (handler: Handler) => - Effect.promise(() => handler(new Request("http://opencode.local/api/model/default"))) + Effect.promise(async () => { + const response = await handler(new Request("http://opencode.local/api/model/default")) + const body: unknown = await response.json() + if ( + !response.ok || + typeof body !== "object" || + body === null || + !("data" in body) || + body.data === undefined || + body.data === null + ) + throw new Error("Plugins not ready") + return response + }).pipe(Effect.retry(Schedule.spaced("10 millis")), Effect.timeout("2 seconds")) const connectOpenAI = (handler: Handler) => Effect.promise(() => From 362733e752fd0f228aafe53d56ee6b0625f2245e Mon Sep 17 00:00:00 2001 From: vimtor Date: Tue, 1 Sep 2026 10:27:03 +0200 Subject: [PATCH 2/4] test(server): observe plugin inventory --- packages/server/test/fetch.test.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/server/test/fetch.test.ts b/packages/server/test/fetch.test.ts index ddc5ea488952..2826a5a5e05a 100644 --- a/packages/server/test/fetch.test.ts +++ b/packages/server/test/fetch.test.ts @@ -60,17 +60,18 @@ function occupy(port: number, cancel = false) { const ready = (handler: Handler) => Effect.promise(async () => { - const response = await handler(new Request("http://opencode.local/api/model/default")) + const response = await handler(new Request("http://opencode.local/api/plugin")) const body: unknown = await response.json() - if ( - !response.ok || - typeof body !== "object" || - body === null || - !("data" in body) || - body.data === undefined || - body.data === null + if (!response.ok || typeof body !== "object" || body === null || !("data" in body) || !Array.isArray(body.data)) + throw new Error("Expected a plugin list response") + const ids = new Set( + body.data.flatMap((plugin) => + typeof plugin === "object" && plugin !== null && "id" in plugin && typeof plugin.id === "string" + ? [plugin.id] + : [], + ), ) - throw new Error("Plugins not ready") + if (!ids.has("opencode.agent") || !ids.has("opencode.provider.openai")) throw new Error("Plugins not ready") return response }).pipe(Effect.retry(Schedule.spaced("10 millis")), Effect.timeout("2 seconds")) From fe96299d60e9d2bde3e72446b734ec2de67e2454 Mon Sep 17 00:00:00 2001 From: vimtor Date: Tue, 1 Sep 2026 10:34:13 +0200 Subject: [PATCH 3/4] test(server): check required registrations --- packages/server/test/fetch.test.ts | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/packages/server/test/fetch.test.ts b/packages/server/test/fetch.test.ts index 2826a5a5e05a..64fe8833cf5d 100644 --- a/packages/server/test/fetch.test.ts +++ b/packages/server/test/fetch.test.ts @@ -60,19 +60,25 @@ function occupy(port: number, cancel = false) { const ready = (handler: Handler) => Effect.promise(async () => { - const response = await handler(new Request("http://opencode.local/api/plugin")) - const body: unknown = await response.json() - if (!response.ok || typeof body !== "object" || body === null || !("data" in body) || !Array.isArray(body.data)) - throw new Error("Expected a plugin list response") - const ids = new Set( - body.data.flatMap((plugin) => - typeof plugin === "object" && plugin !== null && "id" in plugin && typeof plugin.id === "string" - ? [plugin.id] - : [], - ), + const integration = await handler(new Request("http://opencode.local/api/integration/openai")) + const agent = await handler(new Request("http://opencode.local/api/agent/build")) + const body: unknown = await integration.json() + if ( + !integration.ok || + !agent.ok || + typeof body !== "object" || + body === null || + !("data" in body) || + typeof body.data !== "object" || + body.data === null || + !("methods" in body.data) || + !Array.isArray(body.data.methods) || + !body.data.methods.some( + (method) => typeof method === "object" && method !== null && "id" in method && method.id === "chatgpt-browser", + ) ) - if (!ids.has("opencode.agent") || !ids.has("opencode.provider.openai")) throw new Error("Plugins not ready") - return response + throw new Error("Plugins not ready") + return integration }).pipe(Effect.retry(Schedule.spaced("10 millis")), Effect.timeout("2 seconds")) const connectOpenAI = (handler: Handler) => From 51721c4f3c38b6614fbab21a50d17a3dc1e9a510 Mon Sep 17 00:00:00 2001 From: vimtor Date: Tue, 1 Sep 2026 10:42:40 +0200 Subject: [PATCH 4/4] test(server): poll plugin-owned state --- packages/server/test/fetch.test.ts | 82 +++++++++++++++++------------- 1 file changed, 46 insertions(+), 36 deletions(-) diff --git a/packages/server/test/fetch.test.ts b/packages/server/test/fetch.test.ts index 64fe8833cf5d..a8e3a0eea976 100644 --- a/packages/server/test/fetch.test.ts +++ b/packages/server/test/fetch.test.ts @@ -3,7 +3,9 @@ import { createServer } from "node:http" import { makeMemoryDriver } from "@opencode-ai/core/environment/index" import { Workspace } from "@opencode-ai/core/workspace" import { WorkspaceDriver } from "@opencode-ai/core/workspace/driver" -import { Effect, Schedule } from "effect" +import { Agent } from "@opencode-ai/schema/agent" +import { Integration } from "@opencode-ai/schema/integration" +import { Effect, Schedule, Schema } from "effect" import { tmpdir } from "../../core/test/fixture/tmpdir" import { it } from "../../core/test/lib/effect" import { ServerFetch } from "../src/fetch" @@ -58,39 +60,35 @@ function occupy(port: number, cancel = false) { }) } -const ready = (handler: Handler) => - Effect.promise(async () => { - const integration = await handler(new Request("http://opencode.local/api/integration/openai")) - const agent = await handler(new Request("http://opencode.local/api/agent/build")) - const body: unknown = await integration.json() - if ( - !integration.ok || - !agent.ok || - typeof body !== "object" || - body === null || - !("data" in body) || - typeof body.data !== "object" || - body.data === null || - !("methods" in body.data) || - !Array.isArray(body.data.methods) || - !body.data.methods.some( - (method) => typeof method === "object" && method !== null && "id" in method && method.id === "chatgpt-browser", +const connectOpenAI = (handler: Handler) => + Effect.gen(function* () { + // Catalog endpoints no longer wait for plugin initialization. + yield* Effect.gen(function* () { + const response = yield* Effect.promise(() => handler(new Request("http://opencode.local/api/integration"))) + expect(response.status).toBe(200) + const body = Schema.decodeUnknownSync(Schema.Struct({ data: Schema.Array(Integration.Info) }))( + yield* Effect.promise(() => response.json()), ) + return body.data.some( + (integration) => + integration.id === "openai" && + integration.methods.some((method) => method.type === "oauth" && method.id === "chatgpt-browser"), + ) + }).pipe( + Effect.filterOrFail((ready) => ready), + Effect.retry(Schedule.spaced("10 millis")), + Effect.timeout("2 seconds"), ) - throw new Error("Plugins not ready") - return integration - }).pipe(Effect.retry(Schedule.spaced("10 millis")), Effect.timeout("2 seconds")) - -const connectOpenAI = (handler: Handler) => - Effect.promise(() => - handler( - new Request("http://opencode.local/api/integration/openai/connect/oauth", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ methodID: "chatgpt-browser" }), - }), - ), - ) + return yield* Effect.promise(() => + handler( + new Request("http://opencode.local/api/integration/openai/connect/oauth", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ methodID: "chatgpt-browser" }), + }), + ), + ) + }) const workspaceDriver = WorkspaceDriver.make({ create: ({ workspaceID }) => Effect.succeed({ binding: { workspaceID } }), @@ -209,7 +207,6 @@ it.live("cancels a stale OpenAI OAuth callback server before falling back", () = Effect.gen(function* () { const requests = yield* occupy(1455, true) const handler = yield* ServerFetch.make(options) - yield* ready(handler) const response = yield* connectOpenAI(handler) expect(response.status).toBe(200) @@ -223,7 +220,6 @@ it.live("falls back to port 1457 when OpenAI OAuth port 1455 remains busy", () = Effect.gen(function* () { const requests = yield* occupy(1455) const handler = yield* ServerFetch.make(options) - yield* ready(handler) const response = yield* connectOpenAI(handler) expect(response.status).toBe(200) @@ -240,7 +236,6 @@ it.live( yield* occupy(1455) yield* occupy(1457) const handler = yield* ServerFetch.make(options) - yield* ready(handler) const response = yield* connectOpenAI(handler) expect(response.status).toBe(400) @@ -459,7 +454,22 @@ it.live("routes pending requests by Session without loading an instance", () => expect(yield* Effect.promise(() => globalForms.json())).toMatchObject({ data: [{ title: "Global form" }] }) // Agent permission policy is installed by plugin activation. - expect((yield* ready(handler)).status).toBe(200) + yield* Effect.gen(function* () { + const response = yield* Effect.promise(() => handler(new Request("http://opencode.local/api/agent"))) + expect(response.status).toBe(200) + const body = Schema.decodeUnknownSync(Schema.Struct({ data: Schema.Array(Agent.Info) }))( + yield* Effect.promise(() => response.json()), + ) + return body.data.some( + (agent) => + agent.id === "build" && + agent.permissions.some((rule) => rule.action === "shell" && rule.resource === "*" && rule.effect === "ask"), + ) + }).pipe( + Effect.filterOrFail((ready) => ready), + Effect.retry(Schedule.spaced("10 millis")), + Effect.timeout("2 seconds"), + ) const createdPermission = yield* Effect.promise(() => handler( new Request(`http://opencode.local/api/session/${created.data.id}/permission`, {