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 => { 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 => ({ threadId: asThreadId("thread-1"), turnId: asTurnId("turn-1"), }), ); public interruptTurnImpl = vi.fn( async (_threadId: ThreadId, _turnId?: TurnId): Promise => 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 => undefined, ); public respondToUserInputImpl = vi.fn( async ( _threadId: ThreadId, _requestId: ApprovalRequestId, _answers: ProviderUserInputAnswers, ): Promise => undefined, ); public stopAllImpl = vi.fn(() => undefined); override startSession(input: CodexAppServerStartSessionInput): Promise { return this.startSessionImpl(input); } override sendTurn(input: CodexAppServerSendTurnInput): Promise { return this.sendTurnImpl(input); } override interruptTurn(threadId: ThreadId, turnId?: TurnId): Promise { 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 { return this.respondToRequestImpl(threadId, requestId, decision); } override respondToUserInput( threadId: ThreadId, requestId: ApprovalRequestId, answers: ProviderUserInputAnswers, ): Promise { 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("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("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; } assert.equal(firstEvent.value.payload.requestType, "command_execution_approval"); }), ); it.effect("preserves file-read 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-file-read-request-resolved"), kind: "notification", provider: "codex", threadId: asThreadId("thread-1"), createdAt: new Date().toISOString(), method: "serverRequest/resolved", requestId: ApprovalRequestId.makeUnsafe("req-file-read-1"), payload: { request: { method: "item/fileRead/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; } assert.equal(firstEvent.value.payload.requestType, "file_read_approval"); }), ); it.effect("preserves explicit empty multi-select user-input answers", () => Effect.gen(function* () { const adapter = yield* CodexAdapter; const firstEventFiber = yield* Stream.runHead(adapter.streamEvents).pipe(Effect.forkChild); const event: ProviderEvent = { id: asEventId("evt-user-input-empty"), kind: "notification", provider: "codex", threadId: asThreadId("thread-1"), createdAt: new Date().toISOString(), method: "item/tool/requestUserInput/answered", payload: { answers: { scope: [], }, }, }; 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, "user-input.resolved"); if (firstEvent.value.type !== "user-input.resolved") { return; } assert.deepEqual(firstEvent.value.payload.answers, { scope: [], }); }), ); it.effect("maps windowsSandbox/setupCompleted to session state and warning on failure", () => Effect.gen(function* () { const adapter = yield* CodexAdapter; const eventsFiber = yield* Stream.runCollect(Stream.take(adapter.streamEvents, 2)).pipe( Effect.forkChild, ); const event: ProviderEvent = { id: asEventId("evt-windows-sandbox-failed"), kind: "notification", provider: "codex", threadId: asThreadId("thread-1"), createdAt: new Date().toISOString(), method: "windowsSandbox/setupCompleted", message: "Sandbox setup failed", payload: { success: false, detail: "unsupported environment", }, }; lifecycleManager.emit("event", event); const events = Array.from(yield* Fiber.join(eventsFiber)); assert.equal(events.length, 2); const firstEvent = events[0]; const secondEvent = events[1]; assert.equal(firstEvent?.type, "session.state.changed"); if (firstEvent?.type === "session.state.changed") { assert.equal(firstEvent.payload.state, "error"); assert.equal(firstEvent.payload.reason, "Sandbox setup failed"); } assert.equal(secondEvent?.type, "runtime.warning"); if (secondEvent?.type === "runtime.warning") { assert.equal(secondEvent.payload.message, "Sandbox setup failed"); } }), ); it.effect( "maps requestUserInput requests and answered notifications to canonical user-input events", () => Effect.gen(function* () { const adapter = yield* CodexAdapter; const eventsFiber = yield* Stream.runCollect(Stream.take(adapter.streamEvents, 2)).pipe( Effect.forkChild, ); lifecycleManager.emit("event", { id: asEventId("evt-user-input-requested"), kind: "request", provider: "codex", threadId: asThreadId("thread-1"), createdAt: new Date().toISOString(), method: "item/tool/requestUserInput", requestId: ApprovalRequestId.makeUnsafe("req-user-input-1"), payload: { questions: [ { id: "sandbox_mode", header: "Sandbox", question: "Which mode should be used?", options: [ { label: "workspace-write", description: "Allow workspace writes only", }, ], }, ], }, } satisfies ProviderEvent); lifecycleManager.emit("event", { id: asEventId("evt-user-input-resolved"), kind: "notification", provider: "codex", threadId: asThreadId("thread-1"), createdAt: new Date().toISOString(), method: "item/tool/requestUserInput/answered", requestId: ApprovalRequestId.makeUnsafe("req-user-input-1"), payload: { answers: { sandbox_mode: { answers: ["workspace-write"], }, }, }, } satisfies ProviderEvent); const events = Array.from(yield* Fiber.join(eventsFiber)); assert.equal(events[0]?.type, "user-input.requested"); if (events[0]?.type === "user-input.requested") { assert.equal(events[0].requestId, "req-user-input-1"); assert.equal(events[0].payload.questions[0]?.id, "sandbox_mode"); } assert.equal(events[1]?.type, "user-input.resolved"); if (events[1]?.type === "user-input.resolved") { assert.equal(events[1].requestId, "req-user-input-1"); assert.deepEqual(events[1].payload.answers, { sandbox_mode: "workspace-write", }); } }), ); it.effect("maps Codex task and reasoning event chunks into canonical runtime events", () => Effect.gen(function* () { const adapter = yield* CodexAdapter; const eventsFiber = yield* Stream.runCollect(Stream.take(adapter.streamEvents, 5)).pipe( Effect.forkChild, ); lifecycleManager.emit("event", { id: asEventId("evt-codex-task-started"), kind: "notification", provider: "codex", threadId: asThreadId("thread-1"), createdAt: new Date().toISOString(), method: "codex/event/task_started", payload: { id: "turn-structured-1", msg: { type: "task_started", turn_id: "turn-structured-1", collaboration_mode_kind: "plan", }, }, } satisfies ProviderEvent); lifecycleManager.emit("event", { id: asEventId("evt-codex-agent-reasoning"), kind: "notification", provider: "codex", threadId: asThreadId("thread-1"), createdAt: new Date().toISOString(), method: "codex/event/agent_reasoning", payload: { id: "turn-structured-1", msg: { type: "agent_reasoning", text: "Need to compare both transport layers before finalizing the plan.", }, }, } satisfies ProviderEvent); lifecycleManager.emit("event", { id: asEventId("evt-codex-reasoning-delta"), kind: "notification", provider: "codex", threadId: asThreadId("thread-1"), createdAt: new Date().toISOString(), method: "codex/event/reasoning_content_delta", payload: { id: "turn-structured-1", msg: { type: "reasoning_content_delta", turn_id: "turn-structured-1", item_id: "rs_reasoning_1", delta: "**Compare** transport boundaries", summary_index: 0, }, }, } satisfies ProviderEvent); lifecycleManager.emit("event", { id: asEventId("evt-codex-task-complete"), kind: "notification", provider: "codex", threadId: asThreadId("thread-1"), createdAt: new Date().toISOString(), method: "codex/event/task_complete", payload: { id: "turn-structured-1", msg: { type: "task_complete", turn_id: "turn-structured-1", last_agent_message: "\n# Ship it\n", }, }, } satisfies ProviderEvent); const events = Array.from(yield* Fiber.join(eventsFiber)); assert.equal(events[0]?.type, "task.started"); if (events[0]?.type === "task.started") { assert.equal(events[0].turnId, "turn-structured-1"); assert.equal(events[0].payload.taskId, "turn-structured-1"); assert.equal(events[0].payload.taskType, "plan"); } assert.equal(events[1]?.type, "task.progress"); if (events[1]?.type === "task.progress") { assert.equal(events[1].payload.taskId, "turn-structured-1"); assert.equal( events[1].payload.description, "Need to compare both transport layers before finalizing the plan.", ); } assert.equal(events[2]?.type, "content.delta"); if (events[2]?.type === "content.delta") { assert.equal(events[2].turnId, "turn-structured-1"); assert.equal(events[2].itemId, "rs_reasoning_1"); assert.equal(events[2].payload.streamKind, "reasoning_summary_text"); assert.equal(events[2].payload.summaryIndex, 0); } assert.equal(events[3]?.type, "task.completed"); if (events[3]?.type === "task.completed") { assert.equal(events[3].turnId, "turn-structured-1"); assert.equal(events[3].payload.taskId, "turn-structured-1"); assert.equal(events[3].payload.summary, "\n# Ship it\n"); } assert.equal(events[4]?.type, "turn.proposed.completed"); if (events[4]?.type === "turn.proposed.completed") { assert.equal(events[4].turnId, "turn-structured-1"); assert.equal(events[4].payload.planMarkdown, "# Ship it"); } }), ); }); afterAll(() => { if (lifecycleManager.stopAllImpl.mock.calls.length === 0) { lifecycleManager.stopAll(); } assert.ok(lifecycleManager.stopAllImpl.mock.calls.length >= 1); });