import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; import * as Fiber from "effect/Fiber"; import * as Queue from "effect/Queue"; import * as Stream from "effect/Stream"; import * as NodeServices from "@effect/platform-node/NodeServices"; import { assert, it } from "@effect/vitest"; import * as CodexError from "./errors.ts"; import * as CodexProtocol from "./protocol.ts"; import { makeInMemoryStdio } from "./_internal/stdio.ts"; const encoder = new TextEncoder(); const encodeJsonl = (value: unknown) => encoder.encode(`${JSON.stringify(value)}\n`); it.layer(NodeServices.layer)("effect-codex-app-server protocol", (it) => { it.effect( "encodes requests without a jsonrpc field and routes inbound requests and notifications", () => Effect.gen(function* () { const { stdio, input, output } = yield* makeInMemoryStdio(); const transport = yield* CodexProtocol.makeCodexAppServerPatchedProtocol({ stdio }); const notificationDeferred = yield* Deferred.make>(); const requestDeferred = yield* Deferred.make>(); yield* transport.incomingNotifications.pipe( Stream.take(1), Stream.runCollect, Effect.flatMap((notifications) => Deferred.succeed(notificationDeferred, notifications)), Effect.forkScoped, ); yield* transport.incomingRequests.pipe( Stream.take(1), Stream.runCollect, Effect.flatMap((requests) => Deferred.succeed(requestDeferred, requests)), Effect.forkScoped, ); yield* transport.notify("initialized"); assert.equal(yield* Queue.take(output), '{"method":"initialized"}\n'); const initializeParams = { clientInfo: { name: "effect-codex-app-server-test", title: "Effect Codex App Server Test", version: "0.0.0", }, capabilities: { experimentalApi: true, optOutNotificationMethods: null, }, }; const pendingInitialize = yield* transport .request("initialize", initializeParams) .pipe(Effect.forkScoped); assert.deepEqual(JSON.parse(yield* Queue.take(output)), { id: 1, method: "initialize", params: initializeParams, }); yield* Queue.offer( input, encodeJsonl({ method: "item/agentMessage/delta", params: { delta: "Hello from the mock peer.", itemId: "item-1", threadId: "thread-1", turnId: "turn-1", }, }), ); yield* Queue.offer( input, encodeJsonl({ id: 77, method: "item/tool/requestUserInput", params: { itemId: "item-approval-1", threadId: "thread-1", turnId: "turn-1", questions: [ { id: "approved", header: "Approve", question: "Continue?", }, ], }, }), ); yield* Queue.offer( input, encodeJsonl({ id: 1, result: { userAgent: "mock-codex-app-server", codexHome: "/tmp/codex-home", platformFamily: "unix", platformOs: "macos", }, }), ); assert.deepEqual(yield* Fiber.join(pendingInitialize), { userAgent: "mock-codex-app-server", codexHome: "/tmp/codex-home", platformFamily: "unix", platformOs: "macos", }); assert.deepEqual(yield* Deferred.await(notificationDeferred), [ { method: "item/agentMessage/delta", params: { delta: "Hello from the mock peer.", itemId: "item-1", threadId: "thread-1", turnId: "turn-1", }, }, ]); assert.deepEqual(yield* Deferred.await(requestDeferred), [ { id: 77, method: "item/tool/requestUserInput", params: { itemId: "item-approval-1", threadId: "thread-1", turnId: "turn-1", questions: [ { id: "approved", header: "Approve", question: "Continue?", }, ], }, }, ]); yield* transport.respond(77, { answers: { approved: { answers: ["yes"], }, }, }); assert.deepEqual(JSON.parse(yield* Queue.take(output)), { id: 77, result: { answers: { approved: { answers: ["yes"], }, }, }, }); yield* transport.respondError( 78, CodexError.CodexAppServerRequestError.methodNotFound("x/test"), ); assert.deepEqual(JSON.parse(yield* Queue.take(output)), { id: 78, error: { code: -32601, message: "Method not found: x/test", }, }); }), ); it.effect("surfaces JSON encoding failures as protocol parse errors", () => Effect.gen(function* () { const { stdio } = yield* makeInMemoryStdio(); const transport = yield* CodexProtocol.makeCodexAppServerPatchedProtocol({ stdio }); const bigintError = yield* transport.notify("x/test", 1n).pipe(Effect.flip); assert.instanceOf(bigintError, CodexError.CodexAppServerProtocolParseError); assert.equal(bigintError.detail, "Failed to encode Codex App Server message"); const circular: Record = {}; circular.self = circular; const circularError = yield* transport.notify("x/test", circular).pipe(Effect.flip); assert.instanceOf(circularError, CodexError.CodexAppServerProtocolParseError); assert.equal(circularError.detail, "Failed to encode Codex App Server message"); }), ); });