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, 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 { 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 => { 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 => ({ threadId: ProviderThreadId.makeUnsafe("thread-1"), turnId: asTurnId("turn-1"), }), ); public interruptTurnImpl = vi.fn( async (_sessionId: ProviderSessionId, _turnId?: ProviderTurnId): Promise => 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 => undefined, ); public stopAllImpl = vi.fn(() => undefined); override startSession(input: ProviderSessionStartInput): Promise { return this.startSessionImpl(input); } override sendTurn(input: CodexAppServerSendTurnInput): Promise { return this.sendTurnImpl(input); } override interruptTurn(sessionId: ProviderSessionId, turnId?: ProviderTurnId): Promise { 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 { 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 validationManager = new FakeCodexManager(); const validationLayer = it.layer( makeCodexAdapterLive({ manager: validationManager }).pipe( Layer.provideMerge(ServerConfig.layerTest(process.cwd(), process.cwd())), 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(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(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); });