feat(plugin): support plugin-provided tools (#34619)
This commit is contained in:
@@ -11,7 +11,7 @@ const opencode = yield * OpenCode.create()
|
||||
const session = yield * opencode.sessions.get({ sessionID })
|
||||
```
|
||||
|
||||
It also exports `Tool` and exposes local-only `tools.register(...)`, replacing the former `@opencode-ai/core/public` facade. Registration uses Core's host-level `ApplicationTools` service shared by the host's Locations; each Location retains its own `ToolRegistry` for overlay, lookup, and settlement. Closing the owning Effect Scope releases router resources, location services, fibers, and scoped tool registrations.
|
||||
It also exports `Tool` for plugins that register tools with `ctx.tool.register(...)`. Embedded plugins run through the ordinary discovery flow and register tools into each Location's `ToolRegistry` through the normal `Tools.Service.register(...)` path. Closing the owning Effect Scope releases router resources, location services, fibers, and scoped tool registrations.
|
||||
|
||||
`sessions.events({ sessionID, after })` replays durable events after the optional aggregate sequence, then emits newly committed durable events. `sessions.interrupt(...)` targets execution owned by this host, and `sessions.message(...)` retrieves one projected Session message.
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { OpenCode } from "@opencode-ai/client/effect"
|
||||
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
|
||||
import { SdkPlugins } from "@opencode-ai/core/plugin/sdk"
|
||||
import { ApplicationTools } from "@opencode-ai/core/tool/application-tools"
|
||||
import { createEmbeddedRoutes } from "@opencode-ai/server/routes"
|
||||
import { Context, Effect, Layer, Scope } from "effect"
|
||||
import { FetchHttpClient, HttpRouter, HttpServer } from "effect/unstable/http"
|
||||
@@ -9,18 +8,18 @@ import { FetchHttpClient, HttpRouter, HttpServer } from "effect/unstable/http"
|
||||
export const create = Effect.fn("OpenCode.create")(function* () {
|
||||
const scope = yield* Scope.Scope
|
||||
const memoMap = yield* Layer.makeMemoMap
|
||||
const sdkPlugins = SdkPlugins.makeStore()
|
||||
const context = yield* Layer.buildWithMemoMap(
|
||||
Layer.mergeAll(ApplicationTools.layer, PermissionSaved.defaultLayer, SdkPlugins.layer),
|
||||
Layer.mergeAll(PermissionSaved.defaultLayer, SdkPlugins.layerWithStore(sdkPlugins)),
|
||||
memoMap,
|
||||
scope,
|
||||
)
|
||||
const tools = Context.get(context, ApplicationTools.Service)
|
||||
const plugins = Context.get(context, SdkPlugins.Service)
|
||||
const permissions = Context.get(context, PermissionSaved.Service)
|
||||
const web = yield* Effect.acquireRelease(
|
||||
Effect.sync(() =>
|
||||
HttpRouter.toWebHandler(
|
||||
createEmbeddedRoutes().pipe(
|
||||
createEmbeddedRoutes(sdkPlugins).pipe(
|
||||
HttpRouter.provideRequest(Layer.succeed(PermissionSaved.Service, permissions)),
|
||||
Layer.provide(HttpServer.layerServices),
|
||||
),
|
||||
@@ -38,7 +37,8 @@ export const create = Effect.fn("OpenCode.create")(function* () {
|
||||
)
|
||||
return {
|
||||
...client,
|
||||
tools: { register: tools.register },
|
||||
sessions: client.session,
|
||||
events: client.event,
|
||||
// The embedded host contributes plugins through the ordinary discovery flow:
|
||||
// each plugin's `effect` runs inside every Location with the real
|
||||
// `PluginContext`, so `ctx.agent.transform` and every other hook behave exactly
|
||||
|
||||
@@ -1,212 +1,206 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import { mkdtemp, rm } from "node:fs/promises"
|
||||
import { tmpdir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import { expect } from "bun:test"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { Deferred, Effect, Latch, Option, Schema, Stream } from "effect"
|
||||
import { Deferred, Effect, Latch, Layer, Option, Schema, Stream } from "effect"
|
||||
import { testEffect } from "../../core/test/lib/effect"
|
||||
import { tmpdir } from "../../core/test/fixture/tmpdir"
|
||||
import type { OpenCodeEvent } from "../src"
|
||||
|
||||
test("embedded client uses the real router and handlers", async () => {
|
||||
const directory = await mkdtemp(join(tmpdir(), "opencode-embedded-"))
|
||||
const database = Flag.OPENCODE_DB
|
||||
Flag.OPENCODE_DB = join(directory, "opencode.sqlite")
|
||||
const { AbsolutePath, Agent, Location, Model, OpenCode, Prompt, Provider, Session, Tool } = await import("../src")
|
||||
const sessionID = Session.ID.make(`ses_embedded_${crypto.randomUUID()}`)
|
||||
const model = Model.Ref.make({ id: Model.ID.make("embedded"), providerID: Provider.ID.make("test") })
|
||||
Flag.OPENCODE_DB = ":memory:"
|
||||
|
||||
try {
|
||||
const program = Effect.gen(function* () {
|
||||
const opencode = yield* OpenCode.create()
|
||||
yield* opencode.tools.register({
|
||||
embedded_tool: Tool.make({
|
||||
description: "Embedded test tool",
|
||||
input: Schema.Struct({}),
|
||||
output: Schema.Struct({ ok: Schema.Boolean }),
|
||||
execute: () => Effect.succeed({ ok: true }),
|
||||
}),
|
||||
})
|
||||
const it = testEffect(Layer.empty)
|
||||
type Sdk = typeof import("../src")
|
||||
type Fixture = { readonly directory: string; readonly sdk: Sdk }
|
||||
|
||||
const created = yield* opencode.session.create({
|
||||
id: sessionID,
|
||||
agent: Agent.ID.make("build"),
|
||||
location: Location.Ref.make({ directory: AbsolutePath.make(directory) }),
|
||||
})
|
||||
yield* opencode.session.switchModel({ sessionID, model })
|
||||
const selected = yield* opencode.session.get({ sessionID })
|
||||
const page = yield* opencode.session.list({ directory: AbsolutePath.make(directory) })
|
||||
const active = yield* opencode.session.active()
|
||||
const admitted = yield* opencode.session.prompt({
|
||||
sessionID,
|
||||
prompt: Prompt.make({ text: "Do not run" }),
|
||||
resume: false,
|
||||
})
|
||||
const context = yield* opencode.session.context({ sessionID })
|
||||
const wake = yield* opencode.session.prompt({
|
||||
sessionID,
|
||||
prompt: Prompt.make({ text: "Promote this input" }),
|
||||
})
|
||||
const prompted = yield* opencode.session.events({ sessionID }).pipe(
|
||||
Stream.filter((event) => event.type === "session.next.prompted" && event.data.messageID === wake.id),
|
||||
Stream.runHead,
|
||||
Effect.timeout("10 seconds"),
|
||||
Effect.map(Option.getOrThrow),
|
||||
)
|
||||
const wakeContext = yield* opencode.session.context({ sessionID })
|
||||
const event = yield* opencode.session
|
||||
.events({ sessionID })
|
||||
.pipe(Stream.take(1), Stream.runHead, Effect.map(Option.getOrUndefined))
|
||||
const modelMessage = Option.fromNullishOr(context.find((message) => message.type === "model-switched")).pipe(
|
||||
Option.getOrThrow,
|
||||
)
|
||||
const message = yield* opencode.session.message({ sessionID, messageID: modelMessage.id })
|
||||
yield* opencode.session.interrupt({ sessionID })
|
||||
const other = yield* opencode.session.create({
|
||||
location: Location.Ref.make({ directory: AbsolutePath.make(directory) }),
|
||||
})
|
||||
const missingSessionID = Session.ID.make(`ses_missing_${crypto.randomUUID()}`)
|
||||
const missing = yield* Effect.all(
|
||||
[
|
||||
opencode.session.events({ sessionID: missingSessionID }).pipe(Stream.runHead, Effect.flip),
|
||||
opencode.session.interrupt({ sessionID: missingSessionID }).pipe(Effect.flip),
|
||||
opencode.session.message({ sessionID: missingSessionID, messageID: modelMessage.id }).pipe(Effect.flip),
|
||||
],
|
||||
{ concurrency: "unbounded" },
|
||||
)
|
||||
const missingMessage = yield* Effect.flip(
|
||||
opencode.session.message({
|
||||
sessionID: other.id,
|
||||
messageID: modelMessage.id,
|
||||
}),
|
||||
)
|
||||
const withEmbedded = <A, E, R>(prefix: string, f: (fixture: Fixture) => Effect.Effect<A, E, R>) =>
|
||||
Effect.acquireRelease(
|
||||
Effect.promise(() => tmpdir(prefix)),
|
||||
(directory) => Effect.promise(() => directory[Symbol.asyncDispose]()),
|
||||
).pipe(
|
||||
Effect.flatMap((directory) =>
|
||||
Effect.promise(() => import("../src")).pipe(Effect.flatMap((sdk) => f({ directory: directory.path, sdk }))),
|
||||
),
|
||||
)
|
||||
|
||||
expect(created.id).toBe(sessionID)
|
||||
expect(selected.model?.id).toBe(model.id)
|
||||
expect(selected.model?.providerID).toBe(model.providerID)
|
||||
expect(page.data.some((session) => session.id === sessionID)).toBe(true)
|
||||
expect(active).toEqual({})
|
||||
expect(admitted.sessionID).toBe(sessionID)
|
||||
expect(prompted.type).toBe("session.next.prompted")
|
||||
expect(wakeContext).toContainEqual(expect.objectContaining({ id: wake.id, type: "user" }))
|
||||
expect(context.some((message) => message.type === "model-switched")).toBe(true)
|
||||
expect(event).toMatchObject({ type: "session.next.model.switched", durable: { seq: 1 } })
|
||||
expect(message).toEqual(modelMessage)
|
||||
expect(missing.map((error) => error._tag)).toEqual([
|
||||
"SessionNotFoundError",
|
||||
"SessionNotFoundError",
|
||||
"SessionNotFoundError",
|
||||
])
|
||||
expect(missingMessage._tag).toBe("MessageNotFoundError")
|
||||
})
|
||||
await Effect.runPromise(Effect.scoped(program))
|
||||
} finally {
|
||||
Flag.OPENCODE_DB = database
|
||||
await rm(directory, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
const sessionID = (fixture: Fixture) => fixture.sdk.Session.ID.create()
|
||||
|
||||
test("Location-owned runner events reach the ready global client", async () => {
|
||||
const directory = await mkdtemp(join(tmpdir(), "opencode-embedded-events-"))
|
||||
const database = Flag.OPENCODE_DB
|
||||
Flag.OPENCODE_DB = join(directory, "opencode.sqlite")
|
||||
const { AbsolutePath, Location, OpenCode, Prompt, Session } = await import("../src")
|
||||
const sessionID = Session.ID.make(`ses_embedded_${crypto.randomUUID()}`)
|
||||
const location = (fixture: Fixture) =>
|
||||
fixture.sdk.Location.Ref.make({ directory: fixture.sdk.AbsolutePath.make(fixture.directory) })
|
||||
|
||||
try {
|
||||
const program = Effect.gen(function* () {
|
||||
const opencode = yield* OpenCode.create()
|
||||
const connected = yield* Latch.make(false)
|
||||
const prompted = yield* Deferred.make<OpenCodeEvent>()
|
||||
yield* opencode.event.subscribe().pipe(
|
||||
Stream.runForEach((event) =>
|
||||
event.type === "server.connected"
|
||||
? connected.open
|
||||
: event.type === "session.next.prompted" && event.data.sessionID === sessionID
|
||||
? Deferred.succeed(prompted, event).pipe(Effect.asVoid)
|
||||
: Effect.void,
|
||||
),
|
||||
Effect.forkScoped,
|
||||
)
|
||||
yield* connected.await
|
||||
yield* opencode.session.create({
|
||||
id: sessionID,
|
||||
location: Location.Ref.make({ directory: AbsolutePath.make(directory) }),
|
||||
})
|
||||
yield* opencode.session.prompt({ sessionID, prompt: Prompt.make({ text: "Observe this input" }) })
|
||||
it.live(
|
||||
"embedded client uses the real router and handlers",
|
||||
() =>
|
||||
withEmbedded("opencode-embedded-", (fixture) =>
|
||||
Effect.gen(function* () {
|
||||
const opencode = yield* fixture.sdk.OpenCode.create()
|
||||
const id = sessionID(fixture)
|
||||
const model = fixture.sdk.Model.Ref.make({
|
||||
id: fixture.sdk.Model.ID.make("embedded"),
|
||||
providerID: fixture.sdk.Provider.ID.make("test"),
|
||||
})
|
||||
|
||||
const event = yield* Deferred.await(prompted).pipe(Effect.timeout("4 seconds"))
|
||||
expect(event.durable).toEqual(expect.objectContaining({ aggregateID: sessionID, seq: expect.any(Number) }))
|
||||
})
|
||||
await Effect.runPromise(Effect.scoped(program))
|
||||
} finally {
|
||||
Flag.OPENCODE_DB = database
|
||||
await rm(directory, { recursive: true, force: true })
|
||||
}
|
||||
}, 10_000)
|
||||
yield* opencode.plugin({
|
||||
id: `embedded-tools-${crypto.randomUUID()}`,
|
||||
effect: (ctx) =>
|
||||
ctx.tool
|
||||
.register({
|
||||
embedded_tool: fixture.sdk.Tool.make({
|
||||
description: "Embedded test tool",
|
||||
input: Schema.Struct({}),
|
||||
output: Schema.Struct({ ok: Schema.Boolean }),
|
||||
execute: () => Effect.succeed({ ok: true }),
|
||||
}),
|
||||
})
|
||||
.pipe(Effect.orDie),
|
||||
})
|
||||
|
||||
test("independent embedded hosts do not share live notifications", async () => {
|
||||
const directory = await mkdtemp(join(tmpdir(), "opencode-embedded-hosts-"))
|
||||
const database = Flag.OPENCODE_DB
|
||||
Flag.OPENCODE_DB = join(directory, "opencode.sqlite")
|
||||
const { AbsolutePath, Agent, Location, OpenCode, Session } = await import("../src")
|
||||
const sessionID = Session.ID.make(`ses_embedded_${crypto.randomUUID()}`)
|
||||
|
||||
try {
|
||||
const program = Effect.gen(function* () {
|
||||
const first = yield* OpenCode.create()
|
||||
const second = yield* OpenCode.create()
|
||||
const firstReady = yield* Latch.make(false)
|
||||
const secondReady = yield* Latch.make(false)
|
||||
const firstEvent = yield* Latch.make(false)
|
||||
const secondEvent = yield* Latch.make(false)
|
||||
const observe = (ready: Latch.Latch, event: Latch.Latch) =>
|
||||
Stream.runForEach((notification: OpenCodeEvent) =>
|
||||
notification.type === "server.connected"
|
||||
? ready.open
|
||||
: notification.type === "session.next.agent.switched" && notification.data.sessionID === sessionID
|
||||
? event.open
|
||||
: Effect.void,
|
||||
const created = yield* opencode.sessions.create({
|
||||
id,
|
||||
agent: fixture.sdk.Agent.ID.make("build"),
|
||||
location: location(fixture),
|
||||
})
|
||||
yield* opencode.sessions.switchModel({ sessionID: id, model })
|
||||
const selected = yield* opencode.sessions.get({ sessionID: id })
|
||||
const page = yield* opencode.sessions.list({ directory: fixture.sdk.AbsolutePath.make(fixture.directory) })
|
||||
const active = yield* opencode.sessions.active()
|
||||
const admitted = yield* opencode.sessions.prompt({
|
||||
sessionID: id,
|
||||
prompt: fixture.sdk.Prompt.make({ text: "Do not run" }),
|
||||
resume: false,
|
||||
})
|
||||
const context = yield* opencode.sessions.context({ sessionID: id })
|
||||
const wake = yield* opencode.sessions.prompt({
|
||||
sessionID: id,
|
||||
prompt: fixture.sdk.Prompt.make({ text: "Promote this input" }),
|
||||
})
|
||||
const prompted = yield* opencode.sessions.events({ sessionID: id }).pipe(
|
||||
Stream.filter((event) => event.type === "session.next.prompted" && event.data.messageID === wake.id),
|
||||
Stream.runHead,
|
||||
Effect.timeout("10 seconds"),
|
||||
Effect.map(Option.getOrThrow),
|
||||
)
|
||||
const wakeContext = yield* opencode.sessions.context({ sessionID: id })
|
||||
const event = yield* opencode.sessions
|
||||
.events({ sessionID: id })
|
||||
.pipe(Stream.take(1), Stream.runHead, Effect.map(Option.getOrUndefined))
|
||||
const modelMessage = Option.fromNullishOr(context.find((message) => message.type === "model-switched")).pipe(
|
||||
Option.getOrThrow,
|
||||
)
|
||||
const message = yield* opencode.sessions.message({ sessionID: id, messageID: modelMessage.id })
|
||||
yield* opencode.sessions.interrupt({ sessionID: id })
|
||||
const other = yield* opencode.sessions.create({ location: location(fixture) })
|
||||
const missingSessionID = fixture.sdk.Session.ID.create()
|
||||
const missing = yield* Effect.all(
|
||||
[
|
||||
opencode.sessions.events({ sessionID: missingSessionID }).pipe(Stream.runHead, Effect.flip),
|
||||
opencode.sessions.interrupt({ sessionID: missingSessionID }).pipe(Effect.flip),
|
||||
opencode.sessions.message({ sessionID: missingSessionID, messageID: modelMessage.id }).pipe(Effect.flip),
|
||||
],
|
||||
{ concurrency: "unbounded" },
|
||||
)
|
||||
const missingMessage = yield* Effect.flip(
|
||||
opencode.sessions.message({
|
||||
sessionID: other.id,
|
||||
messageID: modelMessage.id,
|
||||
}),
|
||||
)
|
||||
|
||||
yield* first.event.subscribe().pipe(observe(firstReady, firstEvent), Effect.forkScoped)
|
||||
yield* second.event.subscribe().pipe(observe(secondReady, secondEvent), Effect.forkScoped)
|
||||
yield* Effect.all([firstReady.await, secondReady.await], { discard: true })
|
||||
yield* first.session.create({
|
||||
id: sessionID,
|
||||
location: Location.Ref.make({ directory: AbsolutePath.make(directory) }),
|
||||
})
|
||||
yield* first.session.switchAgent({ sessionID, agent: Agent.ID.make("plan") })
|
||||
expect(created.id).toBe(id)
|
||||
expect(selected.model?.id).toBe(model.id)
|
||||
expect(selected.model?.providerID).toBe(model.providerID)
|
||||
expect(page.data.some((session) => session.id === id)).toBe(true)
|
||||
expect(active).toEqual({})
|
||||
expect(admitted.sessionID).toBe(id)
|
||||
expect(prompted.type).toBe("session.next.prompted")
|
||||
expect(wakeContext).toContainEqual(expect.objectContaining({ id: wake.id, type: "user" }))
|
||||
expect(context.some((message) => message.type === "model-switched")).toBe(true)
|
||||
expect(event).toMatchObject({ type: "session.next.model.switched", durable: { seq: 1 } })
|
||||
expect(message).toEqual(modelMessage)
|
||||
expect(missing.map((error) => error._tag)).toEqual([
|
||||
"SessionNotFoundError",
|
||||
"SessionNotFoundError",
|
||||
"SessionNotFoundError",
|
||||
])
|
||||
expect(missingMessage._tag).toBe("MessageNotFoundError")
|
||||
}),
|
||||
),
|
||||
10_000,
|
||||
)
|
||||
|
||||
yield* firstEvent.await.pipe(Effect.timeout("2 seconds"))
|
||||
expect(Option.isNone(yield* secondEvent.await.pipe(Effect.timeoutOption("100 millis")))).toBe(true)
|
||||
})
|
||||
await Effect.runPromise(Effect.scoped(program))
|
||||
} finally {
|
||||
Flag.OPENCODE_DB = database
|
||||
await rm(directory, { recursive: true, force: true })
|
||||
}
|
||||
}, 10_000)
|
||||
|
||||
test("embedded client is available as a Layer service", async () => {
|
||||
const directory = await mkdtemp(join(tmpdir(), "opencode-embedded-layer-"))
|
||||
const database = Flag.OPENCODE_DB
|
||||
Flag.OPENCODE_DB = join(directory, "opencode.sqlite")
|
||||
const { AbsolutePath, Location, OpenCode, Session } = await import("../src")
|
||||
const sessionID = Session.ID.make(`ses_embedded_${crypto.randomUUID()}`)
|
||||
|
||||
try {
|
||||
const created = await Effect.runPromise(
|
||||
it.live(
|
||||
"Location-owned runner events reach the ready global client",
|
||||
() =>
|
||||
withEmbedded("opencode-embedded-events-", (fixture) =>
|
||||
Effect.gen(function* () {
|
||||
const opencode = yield* OpenCode.Service
|
||||
return yield* opencode.session.create({
|
||||
id: sessionID,
|
||||
location: Location.Ref.make({ directory: AbsolutePath.make(directory) }),
|
||||
})
|
||||
}).pipe(Effect.provide(OpenCode.layer), Effect.scoped),
|
||||
)
|
||||
const opencode = yield* fixture.sdk.OpenCode.create()
|
||||
const id = sessionID(fixture)
|
||||
const connected = yield* Latch.make(false)
|
||||
const prompted = yield* Deferred.make<OpenCodeEvent>()
|
||||
|
||||
expect(created.id).toBe(sessionID)
|
||||
} finally {
|
||||
Flag.OPENCODE_DB = database
|
||||
await rm(directory, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
yield* opencode.events.subscribe().pipe(
|
||||
Stream.runForEach((event) =>
|
||||
event.type === "server.connected"
|
||||
? connected.open
|
||||
: event.type === "session.next.prompted" && event.data.sessionID === id
|
||||
? Deferred.succeed(prompted, event).pipe(Effect.asVoid)
|
||||
: Effect.void,
|
||||
),
|
||||
Effect.forkScoped,
|
||||
)
|
||||
yield* connected.await
|
||||
yield* opencode.sessions.create({ id, location: location(fixture) })
|
||||
yield* opencode.sessions.prompt({
|
||||
sessionID: id,
|
||||
prompt: fixture.sdk.Prompt.make({ text: "Observe this input" }),
|
||||
})
|
||||
|
||||
const event = yield* Deferred.await(prompted).pipe(Effect.timeout("4 seconds"))
|
||||
expect(event.durable).toEqual(expect.objectContaining({ aggregateID: id, seq: expect.any(Number) }))
|
||||
}),
|
||||
),
|
||||
10_000,
|
||||
)
|
||||
|
||||
it.live(
|
||||
"independent embedded hosts do not share live notifications",
|
||||
() =>
|
||||
withEmbedded("opencode-embedded-hosts-", (fixture) =>
|
||||
Effect.gen(function* () {
|
||||
const first = yield* fixture.sdk.OpenCode.create()
|
||||
const second = yield* fixture.sdk.OpenCode.create()
|
||||
const id = sessionID(fixture)
|
||||
const firstReady = yield* Latch.make(false)
|
||||
const secondReady = yield* Latch.make(false)
|
||||
const firstEvent = yield* Latch.make(false)
|
||||
const secondEvent = yield* Latch.make(false)
|
||||
const observe = (ready: Latch.Latch, event: Latch.Latch) =>
|
||||
Stream.runForEach((notification: OpenCodeEvent) =>
|
||||
notification.type === "server.connected"
|
||||
? ready.open
|
||||
: notification.type === "session.next.agent.switched" && notification.data.sessionID === id
|
||||
? event.open
|
||||
: Effect.void,
|
||||
)
|
||||
|
||||
yield* first.events.subscribe().pipe(observe(firstReady, firstEvent), Effect.forkScoped)
|
||||
yield* second.events.subscribe().pipe(observe(secondReady, secondEvent), Effect.forkScoped)
|
||||
yield* Effect.all([firstReady.await, secondReady.await], { discard: true })
|
||||
yield* first.sessions.create({ id, location: location(fixture) })
|
||||
yield* first.sessions.switchAgent({ sessionID: id, agent: fixture.sdk.Agent.ID.make("plan") })
|
||||
|
||||
yield* firstEvent.await.pipe(Effect.timeout("2 seconds"))
|
||||
expect(Option.isNone(yield* secondEvent.await.pipe(Effect.timeoutOption("100 millis")))).toBe(true)
|
||||
}),
|
||||
),
|
||||
10_000,
|
||||
)
|
||||
|
||||
it.live("embedded client is available as a Layer service", () =>
|
||||
withEmbedded("opencode-embedded-layer-", (fixture) => {
|
||||
const id = sessionID(fixture)
|
||||
return Effect.gen(function* () {
|
||||
const opencode = yield* fixture.sdk.OpenCode.Service
|
||||
const created = yield* opencode.sessions.create({ id, location: location(fixture) })
|
||||
expect(created.id).toBe(id)
|
||||
}).pipe(Effect.provide(fixture.sdk.OpenCode.layer))
|
||||
}),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user