197 lines · 6.2 KB · TypeScript
RawDownload
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<ReadonlyArray<CodexProtocol.CodexAppServerIncomingNotification>>();        const requestDeferred =          yield* Deferred.make<ReadonlyArray<CodexProtocol.CodexAppServerIncomingRequest>>();         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<string, unknown> = {};      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");    }),  );});