diff --git a/packages/core/test/mcp-oauth.test.ts b/packages/core/test/mcp-oauth.test.ts index 6b9a3302d9b6..bb99ff0b2188 100644 --- a/packages/core/test/mcp-oauth.test.ts +++ b/packages/core/test/mcp-oauth.test.ts @@ -162,6 +162,6 @@ describe("MCP OAuth", () => { }) test("rejects an invalid redirect URL", async () => { - await expect(authorize("not a URL")).rejects.toThrow("cannot be parsed as a URL") + await expect(authorize("not a URL")).rejects.toThrow(TypeError) }) }) diff --git a/packages/core/test/npm.test.ts b/packages/core/test/npm.test.ts index 13de58519878..b2ce86a884b7 100644 --- a/packages/core/test/npm.test.ts +++ b/packages/core/test/npm.test.ts @@ -66,7 +66,7 @@ async function createRegistryFixture(directory: string) { exports: "./index.js", }) await Bun.write(path.join(root, "package", "index.js"), `export const version = "${version}"\n`) - await Bun.$`tar -czf ${path.join(root, "package.tgz")} -C ${root} package` + await Bun.$`tar -czf package.tgz package`.cwd(root) tarballs.set(version, await Bun.file(path.join(root, "package.tgz")).bytes()) } const state = { latest: "1.0.0" } @@ -252,7 +252,7 @@ describe("Npm.add", () => { } }).pipe(Effect.scoped, Effect.provide(npmLayer(cache)), Effect.runPromise) - expect(entries.added.entrypoint).toEndWith("/index.js") + expect(entries.added.entrypoint).toBe(pathToFileURL(path.join(entries.added.directory, "index.js")).href) expect(entries.added.version).toBe(fixture.commit) expect(entries.cached).toEqual(entries.added) expect(entries.resolved).toEqual(entries.added) diff --git a/packages/server/test/fetch.test.ts b/packages/server/test/fetch.test.ts index 3074d5916707..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 } 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,19 +60,35 @@ function occupy(port: number, cancel = false) { }) } -const ready = (handler: Handler) => - Effect.promise(() => handler(new Request("http://opencode.local/api/model/default"))) - 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" }), - }), - ), - ) + 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"), + ) + 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 } }), @@ -189,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) @@ -203,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) @@ -220,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) @@ -439,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`, { diff --git a/packages/util/src/npm.ts b/packages/util/src/npm.ts index efd9032612a3..3b6683c01ee8 100644 --- a/packages/util/src/npm.ts +++ b/packages/util/src/npm.ts @@ -293,7 +293,8 @@ const layer = Layer.effect( installedNameValue, installed?.path ?? path.join(staging, "node_modules", installedNameValue), target, - subpaths, + // Resolve installed entrypoints after rename so Bun cannot hold the staging directory open on Windows. + installed ? [] : subpaths, ) if (!installed && !result.entrypoint) return yield* new InstallFailedError({ add: [pkg], dir: staging }) return { name: installedNameValue, result }