849 lines · 27.4 KB · TypeScript
RawDownload
import assert from "node:assert/strict";import {  ApprovalRequestId,  EventId,  ProviderItemId,  type ProviderApprovalDecision,  type ProviderEvent,  type ProviderSession,  type ProviderTurnStartResult,  type ProviderUserInputAnswers,  ThreadId,  TurnId,} from "@t3tools/contracts";import * as NodeServices from "@effect/platform-node/NodeServices";import { afterAll, it, vi } from "@effect/vitest"; import { Effect, Fiber, Layer, Option, Stream } from "effect"; import {  CodexAppServerManager,  type CodexAppServerStartSessionInput,  type CodexAppServerSendTurnInput,} from "../../codexAppServerManager.ts";import { ServerConfig } from "../../config.ts";import { ProviderAdapterValidationError } from "../Errors.ts";import { CodexAdapter } from "../Services/CodexAdapter.ts";import { ProviderSessionDirectory } from "../Services/ProviderSessionDirectory.ts";import { makeCodexAdapterLive } from "./CodexAdapter.ts"; const asThreadId = (value: string): ThreadId => ThreadId.makeUnsafe(value);const asTurnId = (value: string): TurnId => TurnId.makeUnsafe(value);const asEventId = (value: string): EventId => EventId.makeUnsafe(value);const asItemId = (value: string): ProviderItemId => ProviderItemId.makeUnsafe(value); class FakeCodexManager extends CodexAppServerManager {  public startSessionImpl = vi.fn(    async (input: CodexAppServerStartSessionInput): Promise<ProviderSession> => {      const now = new Date().toISOString();      return {        provider: "codex",        status: "ready",        runtimeMode: input.runtimeMode,        threadId: input.threadId,        cwd: input.cwd,        createdAt: now,        updatedAt: now,      };    },  );   public sendTurnImpl = vi.fn(    async (_input: CodexAppServerSendTurnInput): Promise<ProviderTurnStartResult> => ({      threadId: asThreadId("thread-1"),      turnId: asTurnId("turn-1"),    }),  );   public interruptTurnImpl = vi.fn(    async (_threadId: ThreadId, _turnId?: TurnId): Promise<void> => undefined,  );   public readThreadImpl = vi.fn(async (_threadId: ThreadId) => ({    threadId: asThreadId("thread-1"),    turns: [],  }));   public rollbackThreadImpl = vi.fn(async (_threadId: ThreadId, _numTurns: number) => ({    threadId: asThreadId("thread-1"),    turns: [],  }));   public respondToRequestImpl = vi.fn(    async (      _threadId: ThreadId,      _requestId: ApprovalRequestId,      _decision: ProviderApprovalDecision,    ): Promise<void> => undefined,  );   public respondToUserInputImpl = vi.fn(    async (      _threadId: ThreadId,      _requestId: ApprovalRequestId,      _answers: ProviderUserInputAnswers,    ): Promise<void> => undefined,  );   public stopAllImpl = vi.fn(() => undefined);   override startSession(input: CodexAppServerStartSessionInput): Promise<ProviderSession> {    return this.startSessionImpl(input);  }   override sendTurn(input: CodexAppServerSendTurnInput): Promise<ProviderTurnStartResult> {    return this.sendTurnImpl(input);  }   override interruptTurn(threadId: ThreadId, turnId?: TurnId): Promise<void> {    return this.interruptTurnImpl(threadId, turnId);  }   override readThread(threadId: ThreadId) {    return this.readThreadImpl(threadId);  }   override rollbackThread(threadId: ThreadId, numTurns: number) {    return this.rollbackThreadImpl(threadId, numTurns);  }   override respondToRequest(    threadId: ThreadId,    requestId: ApprovalRequestId,    decision: ProviderApprovalDecision,  ): Promise<void> {    return this.respondToRequestImpl(threadId, requestId, decision);  }   override respondToUserInput(    threadId: ThreadId,    requestId: ApprovalRequestId,    answers: ProviderUserInputAnswers,  ): Promise<void> {    return this.respondToUserInputImpl(threadId, requestId, answers);  }   override stopSession(_threadId: ThreadId): void {}   override listSessions(): ProviderSession[] {    return [];  }   override hasSession(_threadId: ThreadId): boolean {    return false;  }   override stopAll(): void {    this.stopAllImpl();  }} const providerSessionDirectoryTestLayer = Layer.succeed(ProviderSessionDirectory, {  upsert: () => Effect.void,  getProvider: () =>    Effect.die(new Error("ProviderSessionDirectory.getProvider is not used in test")),  getBinding: () => Effect.succeed(Option.none()),  remove: () => Effect.void,  listThreadIds: () => Effect.succeed([]),}); const validationManager = new FakeCodexManager();const validationLayer = it.layer(  makeCodexAdapterLive({ manager: validationManager }).pipe(    Layer.provideMerge(ServerConfig.layerTest(process.cwd(), process.cwd())),    Layer.provideMerge(providerSessionDirectoryTestLayer),    Layer.provideMerge(NodeServices.layer),  ),); validationLayer("CodexAdapterLive validation", (it) => {  it.effect("returns validation error for non-codex provider on startSession", () =>    Effect.gen(function* () {      const adapter = yield* CodexAdapter;      const result = yield* adapter        .startSession({          provider: "claudeAgent",          threadId: asThreadId("thread-1"),          runtimeMode: "full-access",        })        .pipe(Effect.result);       assert.equal(result._tag, "Failure");      assert.deepStrictEqual(        result.failure,        new ProviderAdapterValidationError({          provider: "codex",          operation: "startSession",          issue: "Expected provider 'codex' but received 'claudeAgent'.",        }),      );      assert.equal(validationManager.startSessionImpl.mock.calls.length, 0);    }),  );  it.effect("maps codex model options before starting a session", () =>    Effect.gen(function* () {      validationManager.startSessionImpl.mockClear();      const adapter = yield* CodexAdapter;       yield* adapter.startSession({        provider: "codex",        threadId: asThreadId("thread-1"),        model: "gpt-5.3-codex",        modelOptions: {          codex: {            fastMode: true,          },        },        runtimeMode: "full-access",      });       assert.deepStrictEqual(validationManager.startSessionImpl.mock.calls[0]?.[0], {        provider: "codex",        threadId: asThreadId("thread-1"),        model: "gpt-5.3-codex",        serviceTier: "fast",        runtimeMode: "full-access",      });    }),  );}); const sessionErrorManager = new FakeCodexManager();sessionErrorManager.sendTurnImpl.mockImplementation(async () => {  throw new Error("Unknown session: sess-missing");});const sessionErrorLayer = it.layer(  makeCodexAdapterLive({ manager: sessionErrorManager }).pipe(    Layer.provideMerge(ServerConfig.layerTest(process.cwd(), process.cwd())),    Layer.provideMerge(providerSessionDirectoryTestLayer),    Layer.provideMerge(NodeServices.layer),  ),); sessionErrorLayer("CodexAdapterLive session errors", (it) => {  it.effect("maps unknown-session sendTurn errors to ProviderAdapterSessionNotFoundError", () =>    Effect.gen(function* () {      const adapter = yield* CodexAdapter;      const result = yield* adapter        .sendTurn({          threadId: asThreadId("sess-missing"),          input: "hello",          attachments: [],        })        .pipe(Effect.result);       assert.equal(result._tag, "Failure");      if (result._tag !== "Failure") {        return;      }       assert.equal(result.failure._tag, "ProviderAdapterSessionNotFoundError");      if (result.failure._tag !== "ProviderAdapterSessionNotFoundError") {        return;      }      assert.equal(result.failure.provider, "codex");      assert.equal(result.failure.threadId, "sess-missing");      assert.equal(result.failure.cause instanceof Error, true);    }),  );   it.effect("maps codex model options before sending a turn", () =>    Effect.gen(function* () {      sessionErrorManager.sendTurnImpl.mockClear();      const adapter = yield* CodexAdapter;       yield* Effect.ignore(        adapter.sendTurn({          threadId: asThreadId("sess-missing"),          input: "hello",          model: "gpt-5.3-codex",          modelOptions: {            codex: {              reasoningEffort: "high",              fastMode: true,            },          },          attachments: [],        }),      );       assert.deepStrictEqual(sessionErrorManager.sendTurnImpl.mock.calls[0]?.[0], {        threadId: asThreadId("sess-missing"),        input: "hello",        model: "gpt-5.3-codex",        effort: "high",        serviceTier: "fast",      });    }),  );}); const lifecycleManager = new FakeCodexManager();const lifecycleLayer = it.layer(  makeCodexAdapterLive({ manager: lifecycleManager }).pipe(    Layer.provideMerge(ServerConfig.layerTest(process.cwd(), process.cwd())),    Layer.provideMerge(providerSessionDirectoryTestLayer),    Layer.provideMerge(NodeServices.layer),  ),); lifecycleLayer("CodexAdapterLive lifecycle", (it) => {  it.effect("maps completed agent message items to canonical item.completed events", () =>    Effect.gen(function* () {      const adapter = yield* CodexAdapter;      const firstEventFiber = yield* Stream.runHead(adapter.streamEvents).pipe(Effect.forkChild);       const event: ProviderEvent = {        id: asEventId("evt-msg-complete"),        kind: "notification",        provider: "codex",        createdAt: new Date().toISOString(),        method: "item/completed",        threadId: asThreadId("thread-1"),        turnId: asTurnId("turn-1"),        itemId: asItemId("msg_1"),        payload: {          item: {            type: "agentMessage",            id: "msg_1",          },        },      };       lifecycleManager.emit("event", event);      const firstEvent = yield* Fiber.join(firstEventFiber);       assert.equal(firstEvent._tag, "Some");      if (firstEvent._tag !== "Some") {        return;      }      assert.equal(firstEvent.value.type, "item.completed");      if (firstEvent.value.type !== "item.completed") {        return;      }      assert.equal(firstEvent.value.itemId, "msg_1");      assert.equal(firstEvent.value.turnId, "turn-1");      assert.equal(firstEvent.value.payload.itemType, "assistant_message");    }),  );   it.effect("maps completed plan items to canonical proposed-plan completion events", () =>    Effect.gen(function* () {      const adapter = yield* CodexAdapter;      const firstEventFiber = yield* Stream.runHead(adapter.streamEvents).pipe(Effect.forkChild);       const event: ProviderEvent = {        id: asEventId("evt-plan-complete"),        kind: "notification",        provider: "codex",        createdAt: new Date().toISOString(),        method: "item/completed",        threadId: asThreadId("thread-1"),        turnId: asTurnId("turn-1"),        itemId: asItemId("plan_1"),        payload: {          item: {            type: "Plan",            id: "plan_1",            text: "## Final plan\n\n- one\n- two",          },        },      };       lifecycleManager.emit("event", event);      const firstEvent = yield* Fiber.join(firstEventFiber);       assert.equal(firstEvent._tag, "Some");      if (firstEvent._tag !== "Some") {        return;      }      assert.equal(firstEvent.value.type, "turn.proposed.completed");      if (firstEvent.value.type !== "turn.proposed.completed") {        return;      }      assert.equal(firstEvent.value.turnId, "turn-1");      assert.equal(firstEvent.value.payload.planMarkdown, "## Final plan\n\n- one\n- two");    }),  );   it.effect("maps plan deltas to canonical proposed-plan delta events", () =>    Effect.gen(function* () {      const adapter = yield* CodexAdapter;      const firstEventFiber = yield* Stream.runHead(adapter.streamEvents).pipe(Effect.forkChild);       lifecycleManager.emit("event", {        id: asEventId("evt-plan-delta"),        kind: "notification",        provider: "codex",        createdAt: new Date().toISOString(),        method: "item/plan/delta",        threadId: asThreadId("thread-1"),        turnId: asTurnId("turn-1"),        itemId: asItemId("plan_1"),        payload: {          delta: "## Final plan",        },      } satisfies ProviderEvent);       const firstEvent = yield* Fiber.join(firstEventFiber);       assert.equal(firstEvent._tag, "Some");      if (firstEvent._tag !== "Some") {        return;      }      assert.equal(firstEvent.value.type, "turn.proposed.delta");      if (firstEvent.value.type !== "turn.proposed.delta") {        return;      }      assert.equal(firstEvent.value.turnId, "turn-1");      assert.equal(firstEvent.value.payload.delta, "## Final plan");    }),  );   it.effect("maps session/closed lifecycle events to canonical session.exited runtime events", () =>    Effect.gen(function* () {      const adapter = yield* CodexAdapter;      const firstEventFiber = yield* Stream.runHead(adapter.streamEvents).pipe(Effect.forkChild);       const event: ProviderEvent = {        id: asEventId("evt-session-closed"),        kind: "session",        provider: "codex",        threadId: asThreadId("thread-1"),        createdAt: new Date().toISOString(),        method: "session/closed",        message: "Session stopped",      };       lifecycleManager.emit("event", event);      const firstEvent = yield* Fiber.join(firstEventFiber);       assert.equal(firstEvent._tag, "Some");      if (firstEvent._tag !== "Some") {        return;      }      assert.equal(firstEvent.value.type, "session.exited");      if (firstEvent.value.type !== "session.exited") {        return;      }      assert.equal(firstEvent.value.threadId, "thread-1");      assert.equal(firstEvent.value.payload.reason, "Session stopped");    }),  );   it.effect("maps retryable Codex error notifications to runtime.warning", () =>    Effect.gen(function* () {      const adapter = yield* CodexAdapter;      const firstEventFiber = yield* Stream.runHead(adapter.streamEvents).pipe(Effect.forkChild);       lifecycleManager.emit("event", {        id: asEventId("evt-retryable-error"),        kind: "notification",        provider: "codex",        threadId: asThreadId("thread-1"),        createdAt: new Date().toISOString(),        method: "error",        turnId: asTurnId("turn-1"),        payload: {          error: {            message: "Reconnecting... 2/5",          },          willRetry: true,        },      } satisfies ProviderEvent);       const firstEvent = yield* Fiber.join(firstEventFiber);       assert.equal(firstEvent._tag, "Some");      if (firstEvent._tag !== "Some") {        return;      }      assert.equal(firstEvent.value.type, "runtime.warning");      if (firstEvent.value.type !== "runtime.warning") {        return;      }      assert.equal(firstEvent.value.turnId, "turn-1");      assert.equal(firstEvent.value.payload.message, "Reconnecting... 2/5");    }),  );   it.effect("preserves request type when mapping serverRequest/resolved", () =>    Effect.gen(function* () {      const adapter = yield* CodexAdapter;      const firstEventFiber = yield* Stream.runHead(adapter.streamEvents).pipe(Effect.forkChild);       const event: ProviderEvent = {        id: asEventId("evt-request-resolved"),        kind: "notification",        provider: "codex",        threadId: asThreadId("thread-1"),        createdAt: new Date().toISOString(),        method: "serverRequest/resolved",        requestId: ApprovalRequestId.makeUnsafe("req-1"),        payload: {          request: {            method: "item/commandExecution/requestApproval",          },          decision: "accept",        },      };       lifecycleManager.emit("event", event);      const firstEvent = yield* Fiber.join(firstEventFiber);       assert.equal(firstEvent._tag, "Some");      if (firstEvent._tag !== "Some") {        return;      }      assert.equal(firstEvent.value.type, "request.resolved");      if (firstEvent.value.type !== "request.resolved") {        return;

Lines 1–500 of 849.