import type { Options as ClaudeQueryOptions, PermissionMode, PermissionResult, SDKMessage, SDKUserMessage, } from "@anthropic-ai/claude-agent-sdk"; import { ApprovalRequestId, ThreadId } from "@t3tools/contracts"; import { assert, describe, it } from "@effect/vitest"; import { Effect, Fiber, Random, Stream } from "effect"; import { ProviderAdapterValidationError } from "../Errors.ts"; import { ClaudeCodeAdapter } from "../Services/ClaudeCodeAdapter.ts"; import { makeClaudeCodeAdapterLive, type ClaudeCodeAdapterLiveOptions, } from "./ClaudeCodeAdapter.ts"; class FakeClaudeQuery implements AsyncIterable { private readonly queue: Array = []; private readonly resolvers: Array<(value: IteratorResult) => void> = []; private done = false; public readonly interruptCalls: Array = []; public readonly setModelCalls: Array = []; public readonly setPermissionModeCalls: Array = []; public readonly setMaxThinkingTokensCalls: Array = []; public closeCalls = 0; emit(message: SDKMessage): void { if (this.done) { return; } const resolver = this.resolvers.shift(); if (resolver) { resolver({ done: false, value: message }); return; } this.queue.push(message); } finish(): void { if (this.done) { return; } this.done = true; for (const resolver of this.resolvers.splice(0)) { resolver({ done: true, value: undefined }); } } readonly interrupt = async (): Promise => { this.interruptCalls.push(undefined); }; readonly setModel = async (model?: string): Promise => { this.setModelCalls.push(model); }; readonly setPermissionMode = async (mode: PermissionMode): Promise => { this.setPermissionModeCalls.push(mode); }; readonly setMaxThinkingTokens = async (maxThinkingTokens: number | null): Promise => { this.setMaxThinkingTokensCalls.push(maxThinkingTokens); }; readonly close = (): void => { this.closeCalls += 1; this.finish(); }; [Symbol.asyncIterator](): AsyncIterator { return { next: () => { if (this.queue.length > 0) { const value = this.queue.shift(); if (value) { return Promise.resolve({ done: false, value, }); } } if (this.done) { return Promise.resolve({ done: true, value: undefined, }); } return new Promise((resolve) => { this.resolvers.push(resolve); }); }, }; } } interface Harness { readonly layer: ReturnType; readonly query: FakeClaudeQuery; readonly getLastCreateQueryInput: () => | { readonly prompt: AsyncIterable; readonly options: ClaudeQueryOptions; } | undefined; } function makeHarness(config?: { readonly nativeEventLogPath?: string; readonly nativeEventLogger?: ClaudeCodeAdapterLiveOptions["nativeEventLogger"]; }): Harness { const query = new FakeClaudeQuery(); let createInput: | { readonly prompt: AsyncIterable; readonly options: ClaudeQueryOptions; } | undefined; const adapterOptions: ClaudeCodeAdapterLiveOptions = { createQuery: (input) => { createInput = input; return query; }, ...(config?.nativeEventLogger ? { nativeEventLogger: config.nativeEventLogger, } : {}), ...(config?.nativeEventLogPath ? { nativeEventLogPath: config.nativeEventLogPath, } : {}), }; return { layer: makeClaudeCodeAdapterLive(adapterOptions), query, getLastCreateQueryInput: () => createInput, }; } function makeDeterministicRandomService(seed = 0x1234_5678): { nextIntUnsafe: () => number; nextDoubleUnsafe: () => number; } { let state = seed >>> 0; const nextIntUnsafe = (): number => { state = (Math.imul(1_664_525, state) + 1_013_904_223) >>> 0; return state; }; return { nextIntUnsafe, nextDoubleUnsafe: () => nextIntUnsafe() / 0x1_0000_0000, }; } const THREAD_ID = ThreadId.makeUnsafe("thread-claude-1"); const RESUME_THREAD_ID = ThreadId.makeUnsafe("thread-claude-resume"); describe("ClaudeCodeAdapterLive", () => { it.effect("returns validation error for non-claudeCode provider on startSession", () => { const harness = makeHarness(); return Effect.gen(function* () { const adapter = yield* ClaudeCodeAdapter; const result = yield* adapter .startSession({ threadId: THREAD_ID, provider: "codex", runtimeMode: "full-access" }) .pipe(Effect.result); assert.equal(result._tag, "Failure"); if (result._tag !== "Failure") { return; } assert.deepEqual( result.failure, new ProviderAdapterValidationError({ provider: "claudeCode", operation: "startSession", issue: "Expected provider 'claudeCode' but received 'codex'.", }), ); }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }); it.effect("derives bypass permission mode from full-access runtime policy", () => { const harness = makeHarness(); return Effect.gen(function* () { const adapter = yield* ClaudeCodeAdapter; yield* adapter.startSession({ threadId: THREAD_ID, provider: "claudeCode", runtimeMode: "full-access", }); const createInput = harness.getLastCreateQueryInput(); assert.equal(createInput?.options.permissionMode, "bypassPermissions"); assert.equal(createInput?.options.allowDangerouslySkipPermissions, true); }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }); it.effect("keeps explicit claude permission mode over runtime-derived defaults", () => { const harness = makeHarness(); return Effect.gen(function* () { const adapter = yield* ClaudeCodeAdapter; yield* adapter.startSession({ threadId: THREAD_ID, provider: "claudeCode", runtimeMode: "full-access", providerOptions: { claudeCode: { permissionMode: "plan", }, }, }); const createInput = harness.getLastCreateQueryInput(); assert.equal(createInput?.options.permissionMode, "plan"); assert.equal(createInput?.options.allowDangerouslySkipPermissions, undefined); }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }); it.effect("maps Claude stream/runtime messages to canonical provider runtime events", () => { const harness = makeHarness(); return Effect.gen(function* () { const adapter = yield* ClaudeCodeAdapter; const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 11).pipe( Stream.runCollect, Effect.forkChild, ); const session = yield* adapter.startSession({ threadId: THREAD_ID, provider: "claudeCode", model: "claude-sonnet-4-5", runtimeMode: "full-access", }); const turn = yield* adapter.sendTurn({ threadId: session.threadId, input: "hello", attachments: [], }); harness.query.emit({ type: "stream_event", session_id: "sdk-session-1", uuid: "stream-1", parent_tool_use_id: null, event: { type: "content_block_delta", index: 0, delta: { type: "text_delta", text: "Hi", }, }, } as unknown as SDKMessage); harness.query.emit({ type: "stream_event", session_id: "sdk-session-1", uuid: "stream-2", parent_tool_use_id: null, event: { type: "content_block_start", index: 1, content_block: { type: "tool_use", id: "tool-1", name: "Bash", input: { command: "ls", }, }, }, } as unknown as SDKMessage); harness.query.emit({ type: "stream_event", session_id: "sdk-session-1", uuid: "stream-3", parent_tool_use_id: null, event: { type: "content_block_stop", index: 1, }, } as unknown as SDKMessage); harness.query.emit({ type: "assistant", session_id: "sdk-session-1", uuid: "assistant-1", parent_tool_use_id: null, message: { id: "assistant-message-1", content: [{ type: "text", text: "Hi" }], }, } as unknown as SDKMessage); harness.query.emit({ type: "result", subtype: "success", is_error: false, errors: [], session_id: "sdk-session-1", uuid: "result-1", } as unknown as SDKMessage); const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); assert.deepEqual( runtimeEvents.map((event) => event.type), [ "session.started", "session.configured", "session.state.changed", "turn.started", "thread.started", "content.delta", "item.started", "item.completed", "item.updated", "item.completed", "turn.completed", ], ); const turnStarted = runtimeEvents[3]; assert.equal(turnStarted?.type, "turn.started"); if (turnStarted?.type === "turn.started") { assert.equal(String(turnStarted.turnId), String(turn.turnId)); } const deltaEvent = runtimeEvents.find((event) => event.type === "content.delta"); assert.equal(deltaEvent?.type, "content.delta"); if (deltaEvent?.type === "content.delta") { assert.equal(deltaEvent.payload.delta, "Hi"); assert.equal(String(deltaEvent.turnId), String(turn.turnId)); } const toolStarted = runtimeEvents.find((event) => event.type === "item.started"); assert.equal(toolStarted?.type, "item.started"); if (toolStarted?.type === "item.started") { assert.equal(toolStarted.payload.itemType, "command_execution"); } const turnCompleted = runtimeEvents[runtimeEvents.length - 1]; assert.equal(turnCompleted?.type, "turn.completed"); if (turnCompleted?.type === "turn.completed") { assert.equal(String(turnCompleted.turnId), String(turn.turnId)); assert.equal(turnCompleted.payload.state, "completed"); } }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }); it.effect( "emits completion only after turn result when assistant frames arrive before deltas", () => { const harness = makeHarness(); return Effect.gen(function* () { const adapter = yield* ClaudeCodeAdapter; const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 9).pipe( Stream.runCollect, Effect.forkChild, ); const session = yield* adapter.startSession({ threadId: THREAD_ID, provider: "claudeCode", runtimeMode: "full-access", }); const turn = yield* adapter.sendTurn({ threadId: session.threadId, input: "hello", attachments: [], }); harness.query.emit({ type: "assistant", session_id: "sdk-session-early-assistant", uuid: "assistant-early", parent_tool_use_id: null, message: { id: "assistant-message-early", content: [ { type: "tool_use", id: "tool-early", name: "Read", input: { path: "a.ts" } }, ], }, } as unknown as SDKMessage); harness.query.emit({ type: "stream_event", session_id: "sdk-session-early-assistant", uuid: "stream-early", parent_tool_use_id: null, event: { type: "content_block_delta", index: 0, delta: { type: "text_delta", text: "Late text", }, }, } as unknown as SDKMessage); harness.query.emit({ type: "result", subtype: "success", is_error: false, errors: [], session_id: "sdk-session-early-assistant", uuid: "result-early", } as unknown as SDKMessage); const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); assert.deepEqual( runtimeEvents.map((event) => event.type), [ "session.started", "session.configured", "session.state.changed", "turn.started", "thread.started", "item.updated", "content.delta", "item.completed", "turn.completed", ], ); const deltaIndex = runtimeEvents.findIndex((event) => event.type === "content.delta"); const completedIndex = runtimeEvents.findIndex((event) => event.type === "item.completed"); assert.equal(deltaIndex >= 0 && completedIndex >= 0 && deltaIndex < completedIndex, true); const deltaEvent = runtimeEvents[deltaIndex]; assert.equal(deltaEvent?.type, "content.delta"); if (deltaEvent?.type === "content.delta") { assert.equal(deltaEvent.payload.delta, "Late text"); assert.equal(String(deltaEvent.turnId), String(turn.turnId)); } }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }, ); it.effect("falls back to assistant payload text when stream deltas are absent", () => { const harness = makeHarness(); return Effect.gen(function* () { const adapter = yield* ClaudeCodeAdapter; const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 9).pipe( Stream.runCollect, Effect.forkChild, ); const session = yield* adapter.startSession({ threadId: THREAD_ID, provider: "claudeCode", runtimeMode: "full-access", }); const turn = yield* adapter.sendTurn({ threadId: session.threadId, input: "hello", attachments: [], }); harness.query.emit({ type: "assistant", session_id: "sdk-session-fallback-text", uuid: "assistant-fallback", parent_tool_use_id: null, message: { id: "assistant-message-fallback", content: [{ type: "text", text: "Fallback hello" }], }, } as unknown as SDKMessage); harness.query.emit({ type: "result", subtype: "success", is_error: false, errors: [], session_id: "sdk-session-fallback-text", uuid: "result-fallback", } as unknown as SDKMessage); const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); assert.deepEqual( runtimeEvents.map((event) => event.type), [ "session.started", "session.configured", "session.state.changed", "turn.started", "thread.started", "item.updated", "content.delta", "item.completed", "turn.completed", ], ); const deltaEvent = runtimeEvents.find((event) => event.type === "content.delta"); assert.equal(deltaEvent?.type, "content.delta"); if (deltaEvent?.type === "content.delta") { assert.equal(deltaEvent.payload.delta, "Fallback hello"); assert.equal(String(deltaEvent.turnId), String(turn.turnId)); } }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }); it.effect("does not fabricate provider thread ids before first SDK session_id", () => { const harness = makeHarness(); return Effect.gen(function* () { const adapter = yield* ClaudeCodeAdapter; const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 5).pipe( Stream.runCollect, Effect.forkChild, ); const session = yield* adapter.startSession({ threadId: THREAD_ID, provider: "claudeCode", runtimeMode: "full-access", }); assert.equal(session.threadId, THREAD_ID); const turn = yield* adapter.sendTurn({ threadId: session.threadId, input: "hello", attachments: [], }); assert.equal(turn.threadId, THREAD_ID); harness.query.emit({ type: "stream_event", session_id: "sdk-thread-real", uuid: "stream-thread-real", parent_tool_use_id: null, event: { type: "message_start", message: { id: "msg-thread-real", }, }, } as unknown as SDKMessage); harness.query.emit({ type: "result", subtype: "success", is_error: false, errors: [], session_id: "sdk-thread-real", uuid: "result-thread-real", } as unknown as SDKMessage); const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); assert.deepEqual( runtimeEvents.map((event) => event.type), [ "session.started", "session.configured", "session.state.changed", "turn.started", "thread.started", ], ); const sessionStarted = runtimeEvents[0]; assert.equal(sessionStarted?.type, "session.started"); if (sessionStarted?.type === "session.started") { assert.equal(sessionStarted.threadId, THREAD_ID); } const threadStarted = runtimeEvents[4]; assert.equal(threadStarted?.type, "thread.started"); if (threadStarted?.type === "thread.started") { assert.equal(threadStarted.threadId, THREAD_ID); assert.deepEqual(threadStarted.payload, { providerThreadId: "sdk-thread-real", }); } }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }); it.effect("bridges approval request/response lifecycle through canUseTool", () => { const harness = makeHarness(); return Effect.gen(function* () { const adapter = yield* ClaudeCodeAdapter; const session = yield* adapter.startSession({ threadId: THREAD_ID, provider: "claudeCode", runtimeMode: "approval-required", }); yield* Stream.take(adapter.streamEvents, 3).pipe(Stream.runDrain); const createInput = harness.getLastCreateQueryInput(); const canUseTool = createInput?.options.canUseTool; assert.equal(typeof canUseTool, "function"); if (!canUseTool) { return; } const permissionPromise = canUseTool( "Bash", { command: "pwd" }, { signal: new AbortController().signal, suggestions: [ { type: "setMode", mode: "default", destination: "session", }, ], toolUseID: "tool-use-1", }, ); const requested = yield* Stream.runHead(adapter.streamEvents); assert.equal(requested._tag, "Some"); if (requested._tag !== "Some") { return; } assert.equal(requested.value.type, "request.opened"); if (requested.value.type !== "request.opened") { return; } const runtimeRequestId = requested.value.requestId; assert.equal(typeof runtimeRequestId, "string"); if (runtimeRequestId === undefined) { return; } yield* adapter.respondToRequest( session.threadId, ApprovalRequestId.makeUnsafe(runtimeRequestId), "accept", ); const resolved = yield* Stream.runHead(adapter.streamEvents); assert.equal(resolved._tag, "Some"); if (resolved._tag !== "Some") { return; } assert.equal(resolved.value.type, "request.resolved"); if (resolved.value.type !== "request.resolved") { return; } assert.equal(resolved.value.requestId, requested.value.requestId); assert.equal(resolved.value.payload.decision, "accept"); const permissionResult = yield* Effect.promise(() => permissionPromise); assert.equal((permissionResult as PermissionResult).behavior, "allow"); }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }); it.effect("passes parsed resume cursor values to Claude query options", () => { const harness = makeHarness(); return Effect.gen(function* () { const adapter = yield* ClaudeCodeAdapter; const session = yield* adapter.startSession({ threadId: RESUME_THREAD_ID, provider: "claudeCode", resumeCursor: { threadId: "resume-thread-1", resume: "550e8400-e29b-41d4-a716-446655440000", resumeSessionAt: "assistant-99", turnCount: 3, }, runtimeMode: "full-access", }); assert.equal(session.threadId, RESUME_THREAD_ID); assert.deepEqual(session.resumeCursor, { threadId: RESUME_THREAD_ID, resume: "550e8400-e29b-41d4-a716-446655440000", resumeSessionAt: "assistant-99", turnCount: 3, }); const createInput = harness.getLastCreateQueryInput(); assert.equal(createInput?.options.resume, "550e8400-e29b-41d4-a716-446655440000"); assert.equal(createInput?.options.resumeSessionAt, "assistant-99"); }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }); it.effect("does not synthesize resume session id from generated thread ids", () => { const harness = makeHarness(); return Effect.gen(function* () { const adapter = yield* ClaudeCodeAdapter; const session = yield* adapter.startSession({ threadId: THREAD_ID, provider: "claudeCode", runtimeMode: "full-access", }); assert.equal("resume" in (session.resumeCursor as Record), false); const createInput = harness.getLastCreateQueryInput(); assert.equal(createInput?.options.resume, undefined); }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }); it.effect( "supports rollbackThread by trimming in-memory turns and preserving earlier turns", () => { const harness = makeHarness(); return Effect.gen(function* () { const adapter = yield* ClaudeCodeAdapter; const session = yield* adapter.startSession({ threadId: THREAD_ID, provider: "claudeCode", runtimeMode: "full-access", }); const firstTurn = yield* adapter.sendTurn({ threadId: session.threadId, input: "first", attachments: [], }); const firstCompletedFiber = yield* Stream.filter( adapter.streamEvents, (event) => event.type === "turn.completed", ).pipe(Stream.runHead, Effect.forkChild); harness.query.emit({ type: "result", subtype: "success", is_error: false, errors: [], session_id: "sdk-session-rollback", uuid: "result-first", } as unknown as SDKMessage); const firstCompleted = yield* Fiber.join(firstCompletedFiber); assert.equal(firstCompleted._tag, "Some"); if (firstCompleted._tag === "Some" && firstCompleted.value.type === "turn.completed") { assert.equal(String(firstCompleted.value.turnId), String(firstTurn.turnId)); } const secondTurn = yield* adapter.sendTurn({ threadId: session.threadId, input: "second", attachments: [], }); const secondCompletedFiber = yield* Stream.filter( adapter.streamEvents, (event) => event.type === "turn.completed", ).pipe(Stream.runHead, Effect.forkChild); harness.query.emit({ type: "result", subtype: "success", is_error: false, errors: [], session_id: "sdk-session-rollback", uuid: "result-second", } as unknown as SDKMessage); const secondCompleted = yield* Fiber.join(secondCompletedFiber); assert.equal(secondCompleted._tag, "Some"); if (secondCompleted._tag === "Some" && secondCompleted.value.type === "turn.completed") { assert.equal(String(secondCompleted.value.turnId), String(secondTurn.turnId)); } const threadBeforeRollback = yield* adapter.readThread(session.threadId); assert.equal(threadBeforeRollback.turns.length, 2); const rolledBack = yield* adapter.rollbackThread(session.threadId, 1); assert.equal(rolledBack.turns.length, 1); assert.equal(rolledBack.turns[0]?.id, firstTurn.turnId); const threadAfterRollback = yield* adapter.readThread(session.threadId); assert.equal(threadAfterRollback.turns.length, 1); assert.equal(threadAfterRollback.turns[0]?.id, firstTurn.turnId); }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }, ); it.effect("updates model on sendTurn when model override is provided", () => { const harness = makeHarness(); return Effect.gen(function* () { const adapter = yield* ClaudeCodeAdapter; const session = yield* adapter.startSession({ threadId: THREAD_ID, provider: "claudeCode", runtimeMode: "full-access", }); yield* adapter.sendTurn({ threadId: session.threadId, input: "hello", model: "claude-opus-4-6", attachments: [], }); assert.deepEqual(harness.query.setModelCalls, ["claude-opus-4-6"]); }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }); it.effect("writes provider-native observability records when enabled", () => { const nativeEvents: Array<{ event?: { provider?: string; method?: string; threadId?: string; turnId?: string; }; }> = []; const harness = makeHarness({ nativeEventLogger: { filePath: "memory://claude-native-events", write: (event) => { nativeEvents.push(event as (typeof nativeEvents)[number]); return Effect.void; }, close: () => Effect.void, }, }); return Effect.gen(function* () { const adapter = yield* ClaudeCodeAdapter; const session = yield* adapter.startSession({ threadId: THREAD_ID, provider: "claudeCode", runtimeMode: "full-access", }); const turn = yield* adapter.sendTurn({ threadId: session.threadId, input: "hello", attachments: [], }); const turnCompletedFiber = yield* Stream.filter( adapter.streamEvents, (event) => event.type === "turn.completed", ).pipe(Stream.runHead, Effect.forkChild); harness.query.emit({ type: "stream_event", session_id: "sdk-session-native-log", uuid: "stream-native-log", parent_tool_use_id: null, event: { type: "content_block_delta", index: 0, delta: { type: "text_delta", text: "hi", }, }, } as unknown as SDKMessage); harness.query.emit({ type: "result", subtype: "success", is_error: false, errors: [], session_id: "sdk-session-native-log", uuid: "result-native-log", } as unknown as SDKMessage); const turnCompleted = yield* Fiber.join(turnCompletedFiber); assert.equal(turnCompleted._tag, "Some"); assert.equal(nativeEvents.length > 0, true); assert.equal( nativeEvents.some((record) => record.event?.provider === "claudeCode"), true, ); assert.equal( nativeEvents.some( (record) => String( (record.event as { readonly providerThreadId?: string } | undefined) ?.providerThreadId, ) === "sdk-session-native-log", ), true, ); assert.equal( nativeEvents.some((record) => String(record.event?.turnId) === String(turn.turnId)), true, ); assert.equal( nativeEvents.some( (record) => record.event?.method === "claude/stream_event/content_block_delta/text_delta", ), true, ); }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }); });