diff --git a/packages/opencode/test/pty/pty-session.test.ts b/packages/opencode/test/pty/pty-session.test.ts index 8c5d804b7..9c58ab935 100644 --- a/packages/opencode/test/pty/pty-session.test.ts +++ b/packages/opencode/test/pty/pty-session.test.ts @@ -1,103 +1,96 @@ -import { describe, expect, test } from "bun:test" -import { AppRuntime } from "../../src/effect/app-runtime" +import { describe, expect } from "bun:test" import { Bus } from "../../src/bus" -import { Effect } from "effect" -import { Instance } from "../../src/project/instance" -import { WithInstance } from "../../src/project/with-instance" +import { Config } from "../../src/config/config" +import { Plugin } from "../../src/plugin" import { Pty } from "../../src/pty" import type { PtyID } from "../../src/pty/schema" -import { tmpdir } from "../fixture/fixture" -import { setTimeout as sleep } from "node:timers/promises" +import { Effect, Layer, Queue } from "effect" +import { testEffect } from "../lib/effect" -const wait = async (fn: () => boolean, ms = 5000) => { - const end = Date.now() + ms - while (Date.now() < end) { - if (fn()) return - await sleep(25) - } - throw new Error("timeout waiting for pty events") -} +type PtyEvent = { type: "created" | "exited" | "deleted"; id: PtyID } -const pick = (log: Array<{ type: "created" | "exited" | "deleted"; id: PtyID }>, id: PtyID) => { - return log.filter((evt) => evt.id === id).map((evt) => evt.type) +const it = testEffect( + Pty.layer.pipe( + Layer.provideMerge(Bus.layer), + Layer.provideMerge(Config.defaultLayer), + Layer.provideMerge(Plugin.defaultLayer), + ), +) +const ptyTest = process.platform === "win32" ? it.instance.skip : it.instance + +const subscribePtyEvents = Effect.fn("PtySessionTest.subscribePtyEvents")(function* () { + const bus = yield* Bus.Service + const events = yield* Queue.unbounded() + + const subscribe = (effect: Effect.Effect<() => void, never, A>) => + Effect.acquireRelease(effect, (off) => Effect.sync(off)) + + yield* subscribe( + bus.subscribeCallback(Pty.Event.Created, (evt) => { + Queue.offerUnsafe(events, { type: "created", id: evt.properties.info.id }) + }), + ) + yield* subscribe( + bus.subscribeCallback(Pty.Event.Exited, (evt) => { + Queue.offerUnsafe(events, { type: "exited", id: evt.properties.id }) + }), + ) + yield* subscribe( + bus.subscribeCallback(Pty.Event.Deleted, (evt) => { + Queue.offerUnsafe(events, { type: "deleted", id: evt.properties.id }) + }), + ) + + return events +}) + +const createPty = Effect.fn("PtySessionTest.createPty")(function* (input: Pty.CreateInput) { + const pty = yield* Pty.Service + return yield* Effect.acquireRelease(pty.create(input), (info) => pty.remove(info.id).pipe(Effect.ignore)) +}) + +const waitForEvents = (events: Queue.Queue, id: PtyID, count: number) => { + return Effect.gen(function* () { + const picked: Array = [] + while (picked.length < count) { + const evt = yield* Queue.take(events) + if (evt.id === id) picked.push(evt.type) + } + return picked + }).pipe( + Effect.timeoutOrElse({ + duration: "5 seconds", + orElse: () => Effect.fail(new Error("timeout waiting for pty events")), + }), + ) } describe("pty", () => { - test("publishes created, exited, deleted in order for a short-lived process", async () => { - if (process.platform === "win32") return + ptyTest("publishes created, exited, deleted in order for a short-lived process", () => + Effect.gen(function* () { + const events = yield* subscribePtyEvents() + const info = yield* createPty({ + command: "/usr/bin/env", + args: ["sh", "-c", "sleep 0.1"], + title: "sleep", + }) - await using dir = await tmpdir({ git: true }) + expect(yield* waitForEvents(events, info.id, 3)).toEqual(["created", "exited", "deleted"]) + }), + { git: true }, + ) - await WithInstance.provide({ - directory: dir.path, - fn: () => - AppRuntime.runPromise( - Effect.gen(function* () { - const pty = yield* Pty.Service - const log: Array<{ type: "created" | "exited" | "deleted"; id: PtyID }> = [] - const off = [ - Bus.subscribe(Pty.Event.Created, (evt) => log.push({ type: "created", id: evt.properties.info.id })), - Bus.subscribe(Pty.Event.Exited, (evt) => log.push({ type: "exited", id: evt.properties.id })), - Bus.subscribe(Pty.Event.Deleted, (evt) => log.push({ type: "deleted", id: evt.properties.id })), - ] + ptyTest("publishes created, exited, deleted in order for /bin/sh + remove", () => + Effect.gen(function* () { + const pty = yield* Pty.Service + const events = yield* subscribePtyEvents() + const info = yield* createPty({ command: "/bin/sh", title: "sh" }) - let id: PtyID | undefined - try { - const info = yield* pty.create({ - command: "/usr/bin/env", - args: ["sh", "-c", "sleep 0.1"], - title: "sleep", - }) - id = info.id - - yield* Effect.promise(() => wait(() => pick(log, id!).includes("exited"))) - - yield* pty.remove(id) - yield* Effect.promise(() => wait(() => pick(log, id!).length >= 3)) - expect(pick(log, id!)).toEqual(["created", "exited", "deleted"]) - } finally { - off.forEach((x) => x()) - if (id) yield* pty.remove(id) - } - }), - ), - }) - }) - - test("publishes created, exited, deleted in order for /bin/sh + remove", async () => { - if (process.platform === "win32") return - - await using dir = await tmpdir({ git: true }) - - await WithInstance.provide({ - directory: dir.path, - fn: () => - AppRuntime.runPromise( - Effect.gen(function* () { - const pty = yield* Pty.Service - const log: Array<{ type: "created" | "exited" | "deleted"; id: PtyID }> = [] - const off = [ - Bus.subscribe(Pty.Event.Created, (evt) => log.push({ type: "created", id: evt.properties.info.id })), - Bus.subscribe(Pty.Event.Exited, (evt) => log.push({ type: "exited", id: evt.properties.id })), - Bus.subscribe(Pty.Event.Deleted, (evt) => log.push({ type: "deleted", id: evt.properties.id })), - ] - - let id: PtyID | undefined - try { - const info = yield* pty.create({ command: "/bin/sh", title: "sh" }) - id = info.id - - yield* Effect.promise(() => sleep(100)) - - yield* pty.remove(id) - yield* Effect.promise(() => wait(() => pick(log, id!).length >= 3)) - expect(pick(log, id!)).toEqual(["created", "exited", "deleted"]) - } finally { - off.forEach((x) => x()) - if (id) yield* pty.remove(id) - } - }), - ), - }) - }) + expect(yield* waitForEvents(events, info.id, 1)).toEqual(["created"]) + yield* pty.write(info.id, "exit\n") + expect(yield* waitForEvents(events, info.id, 2)).toEqual(["exited", "deleted"]) + yield* pty.remove(info.id) + }), + { git: true }, + ) })