Julius Marmingelogging404e78a8 months agoHistory
290 lines · 9.3 KB · TypeScript
RawDownload
import {  ApprovalRequestId,  EventId,  ProviderItemId,  type ProviderApprovalDecision,  type ProviderEvent,  ProviderSessionId,  type ProviderSession,  type ProviderSessionStartInput,  ProviderThreadId,  ProviderTurnId,  type ProviderTurnStartResult,} from "@t3tools/contracts";import * as NodeServices from "@effect/platform-node/NodeServices";import { afterAll, assert, it, vi } from "@effect/vitest";import { assertFailure } from "@effect/vitest/utils"; import { Effect, Fiber, Layer, Option, Stream } from "effect"; import {  CodexAppServerManager,  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 asSessionId = (value: string): ProviderSessionId => ProviderSessionId.makeUnsafe(value);const asTurnId = (value: string): ProviderTurnId => ProviderTurnId.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: ProviderSessionStartInput): Promise<ProviderSession> => {      const now = new Date().toISOString();      return {        sessionId: asSessionId("sess-1"),        provider: "codex",        status: "ready",        threadId: ProviderThreadId.makeUnsafe("thread-1"),        cwd: input.cwd,        createdAt: now,        updatedAt: now,      };    },  );   public sendTurnImpl = vi.fn(    async (_input: CodexAppServerSendTurnInput): Promise<ProviderTurnStartResult> => ({      threadId: ProviderThreadId.makeUnsafe("thread-1"),      turnId: asTurnId("turn-1"),    }),  );   public interruptTurnImpl = vi.fn(    async (_sessionId: ProviderSessionId, _turnId?: ProviderTurnId): Promise<void> => undefined,  );   public readThreadImpl = vi.fn(async (_sessionId: ProviderSessionId) => ({    threadId: ProviderThreadId.makeUnsafe("thread-1"),    turns: [],  }));   public rollbackThreadImpl = vi.fn(async (_sessionId: ProviderSessionId, _numTurns: number) => ({    threadId: ProviderThreadId.makeUnsafe("thread-1"),    turns: [],  }));   public respondToRequestImpl = vi.fn(    async (      _sessionId: ProviderSessionId,      _requestId: ApprovalRequestId,      _decision: ProviderApprovalDecision,    ): Promise<void> => undefined,  );   public stopAllImpl = vi.fn(() => undefined);   override startSession(input: ProviderSessionStartInput): Promise<ProviderSession> {    return this.startSessionImpl(input);  }   override sendTurn(input: CodexAppServerSendTurnInput): Promise<ProviderTurnStartResult> {    return this.sendTurnImpl(input);  }   override interruptTurn(sessionId: ProviderSessionId, turnId?: ProviderTurnId): Promise<void> {    return this.interruptTurnImpl(sessionId, turnId);  }   override readThread(sessionId: ProviderSessionId) {    return this.readThreadImpl(sessionId);  }   override rollbackThread(sessionId: ProviderSessionId, numTurns: number) {    return this.rollbackThreadImpl(sessionId, numTurns);  }   override respondToRequest(    sessionId: ProviderSessionId,    requestId: ApprovalRequestId,    decision: ProviderApprovalDecision,  ): Promise<void> {    return this.respondToRequestImpl(sessionId, requestId, decision);  }   override stopSession(_sessionId: ProviderSessionId): void {}   override listSessions(): ProviderSession[] {    return [];  }   override hasSession(_sessionId: ProviderSessionId): 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()),  getThreadId: () => Effect.succeed(Option.none()),  remove: () => Effect.void,  listSessionIds: () => 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: "claudeCode",        })        .pipe(Effect.result);       assertFailure(        result,        new ProviderAdapterValidationError({          provider: "codex",          operation: "startSession",          issue: "Expected provider 'codex' but received 'claudeCode'.",        }),      );      assert.equal(validationManager.startSessionImpl.mock.calls.length, 0);    }),  );}); 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({          sessionId: asSessionId("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.sessionId, "sess-missing");      assert.instanceOf(result.failure.cause, Error);    }),  );}); 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 message.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",        sessionId: asSessionId("sess-1"),        createdAt: new Date().toISOString(),        method: "item/completed",        threadId: ProviderThreadId.makeUnsafe("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, "message.completed");      if (firstEvent.value.type !== "message.completed") {        return;      }      assert.equal(firstEvent.value.itemId, "msg_1");      assert.equal(firstEvent.value.turnId, "turn-1");    }),  );   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",        sessionId: asSessionId("sess-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.sessionId, "sess-1");      assert.equal(firstEvent.value.message, "Session stopped");    }),  );}); afterAll(() => {  assert.equal(lifecycleManager.stopAllImpl.mock.calls.length, 1);});