|
|
@@ -3,7 +3,8 @@ import { mkdtemp, rm } from "node:fs/promises"
|
|
|
import { tmpdir } from "node:os"
|
|
|
import { join } from "node:path"
|
|
|
import { Flag } from "@opencode-ai/core/flag/flag"
|
|
|
-import { Effect, Option, Schema, Stream } from "effect"
|
|
|
+import { Deferred, Effect, Latch, Option, Schema, Stream } from "effect"
|
|
|
+import type { OpenCodeEvent } from "../src"
|
|
|
|
|
|
test("embedded client uses the real router and handlers", async () => {
|
|
|
const directory = await mkdtemp(join(tmpdir(), "opencode-embedded-"))
|
|
|
@@ -103,6 +104,88 @@ test("embedded client uses the real router and handlers", async () => {
|
|
|
}
|
|
|
})
|
|
|
|
|
|
+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()}`)
|
|
|
+
|
|
|
+ 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.events.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.sessions.create({
|
|
|
+ id: sessionID,
|
|
|
+ location: Location.Ref.make({ directory: AbsolutePath.make(directory) }),
|
|
|
+ })
|
|
|
+ yield* opencode.sessions.prompt({ sessionID, prompt: Prompt.make({ text: "Observe this input" }) })
|
|
|
+
|
|
|
+ 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)
|
|
|
+
|
|
|
+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,
|
|
|
+ )
|
|
|
+
|
|
|
+ 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: sessionID,
|
|
|
+ location: Location.Ref.make({ directory: AbsolutePath.make(directory) }),
|
|
|
+ })
|
|
|
+ yield* first.sessions.switchAgent({ sessionID, agent: 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)
|
|
|
+ })
|
|
|
+ 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
|