Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions packages/sdk/src/effect/opencode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { OpenCode, type OpenCodeClient } from "@opencode-ai/client/effect"
import type { Workspace } from "@opencode-ai/core/workspace"
import { Context, Effect, Layer } from "effect"
import type { Config, Scope } from "effect"
import { FetchHttpClient } from "effect/unstable/http"
import { FetchHttpClient, HttpClient } from "effect/unstable/http"
import { EmbeddedHost } from "../internal/host"
import type { SdkInstances } from "../internal/instances"

Expand Down Expand Up @@ -35,9 +35,12 @@ export const create: <R = never>(
R = never,
>(options: CreateOptions<R> = {}, embed: EmbedOptions = {}) {
const host = yield* Effect.acquireRelease(EmbeddedHost.create(options, embed), (host) => Effect.promise(host.close))
const httpClient = yield* HttpClient.HttpClient.pipe(Effect.provide(FetchHttpClient.layer))
const client = yield* OpenCode.make({ baseUrl: "http://opencode.local" }).pipe(
Effect.provide(
FetchHttpClient.layer.pipe(Layer.provide(Layer.succeed(FetchHttpClient.Fetch, host.fetch)), Layer.fresh),
Effect.provideService(
HttpClient.HttpClient,
// FetchHttpClient reads Fetch at request time; callers must not replace this host's in-process transport.
HttpClient.transformResponse(httpClient, Effect.provideService(FetchHttpClient.Fetch, host.fetch)),
),
)

Expand Down
65 changes: 65 additions & 0 deletions packages/sdk/test/transport.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { expect } from "bun:test"
import { Context, Effect, Exit, Layer, Scope, Stream } from "effect"
import { FetchHttpClient } from "effect/unstable/http"
import { tmpdirScoped } from "../../core/test/fixture/tmpdir"
import { testEffect } from "../../core/test/lib/effect"
import { AbsolutePath, Location, OpenCode, Session } from "../src/effect"

const it = testEffect(Layer.empty)

for (const entrypoint of ["create", "layer"] as const) {
it.live(`${entrypoint} keeps requests and streams on its own transport despite an ambient Fetch`, () =>
Effect.gen(function* () {
const directory = yield* tmpdirScoped()
const calls: string[] = []
const ambient = Object.assign(
(input: RequestInfo | URL) => {
calls.push(input instanceof Request ? input.url : String(input))
return Promise.reject(new Error("The caller's Fetch must not receive embedded SDK requests"))
},
{ preconnect: () => undefined },
)
const parent = yield* Effect.scope
const scope = yield* Scope.fork(parent)
const options: OpenCode.CreateOptions = {
app: { version: "transport-test" },
config: { directory: directory.path, project: false, content: "{}" },
events: { persist: true },
models: { fetch: false },
fs: { filewatcher: false },
}
const client = yield* (
entrypoint === "create"
? OpenCode.create(options).pipe(Scope.provide(scope))
: Layer.buildWithScope(OpenCode.layer(options), scope).pipe(Effect.map(Context.get(OpenCode.Service)))
).pipe(Effect.provideService(FetchHttpClient.Fetch, ambient))

yield* Effect.gen(function* () {
expect(yield* client.health.get()).toMatchObject({ healthy: true, version: "transport-test" })
const session = yield* client.sessions.create({
location: Location.Ref.make({ directory: AbsolutePath.make(directory.path) }),
})
expect((yield* client.sessions.get({ sessionID: session.id })).id).toBe(session.id)
const events = yield* client.sessions.log({ sessionID: session.id }).pipe(Stream.runCollect)
expect(events.some((event) => event.type === "session.created")).toBe(true)
expect(yield* client.events.subscribe().pipe(Stream.take(1), Stream.runCollect)).toMatchObject([
{ type: "server.connected" },
])
expect(yield* client.sessions.get({ sessionID: Session.ID.create() }).pipe(Effect.flip)).toMatchObject({
_tag: "SessionNotFoundError",
})
// Binding the SDK's transport must not change the caller's surrounding context.
expect(yield* FetchHttpClient.Fetch).toBe(ambient)
}).pipe(Effect.provideService(FetchHttpClient.Fetch, ambient))
expect(calls).toEqual([])

yield* Scope.close(scope, Exit.void)
expect(
Exit.isFailure(
yield* client.health.get().pipe(Effect.provideService(FetchHttpClient.Fetch, ambient), Effect.exit),
),
).toBe(true)
expect(calls).toEqual([])
}),
)
}
Loading