884 lines · 26.8 KB · TypeScript
RawDownload
import fs from "node:fs";import os from "node:os";import path from "node:path"; import type {  Options as ClaudeQueryOptions,  PermissionMode,  PermissionResult,  SDKMessage,  SDKUserMessage,} from "@anthropic-ai/claude-agent-sdk";import { ApprovalRequestId, type ProviderTurnId } 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<SDKMessage> {  private readonly queue: Array<SDKMessage> = [];  private readonly resolvers: Array<(value: IteratorResult<SDKMessage>) => void> = [];  private done = false;   public readonly interruptCalls: Array<void> = [];  public readonly setModelCalls: Array<string | undefined> = [];  public readonly setPermissionModeCalls: Array<string> = [];  public readonly setMaxThinkingTokensCalls: Array<number | null> = [];  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<void> => {    this.interruptCalls.push(undefined);  };   readonly setModel = async (model?: string): Promise<void> => {    this.setModelCalls.push(model);  };   readonly setPermissionMode = async (mode: PermissionMode): Promise<void> => {    this.setPermissionModeCalls.push(mode);  };   readonly setMaxThinkingTokens = async (maxThinkingTokens: number | null): Promise<void> => {    this.setMaxThinkingTokensCalls.push(maxThinkingTokens);  };   readonly close = (): void => {    this.closeCalls += 1;    this.finish();  };   [Symbol.asyncIterator](): AsyncIterator<SDKMessage> {    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<typeof makeClaudeCodeAdapterLive>;  readonly query: FakeClaudeQuery;  readonly getLastCreateQueryInput: () =>    | {        readonly prompt: AsyncIterable<SDKUserMessage>;        readonly options: ClaudeQueryOptions;      }    | undefined;} function makeHarness(config?: { readonly nativeEventLogPath?: string }): Harness {  const query = new FakeClaudeQuery();  let createInput:    | {        readonly prompt: AsyncIterable<SDKUserMessage>;        readonly options: ClaudeQueryOptions;      }    | undefined;   const adapterOptions: ClaudeCodeAdapterLiveOptions = {    createQuery: (input) => {      createInput = input;      return query;    },    ...(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,  };} 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({ provider: "codex" }).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({        provider: "claudeCode",        approvalPolicy: "never",        sandboxMode: "danger-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({        provider: "claudeCode",        approvalPolicy: "never",        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, 8).pipe(        Stream.runCollect,        Effect.forkChild,      );       const session = yield* adapter.startSession({        provider: "claudeCode",        model: "claude-sonnet-4-5",      });       const turn = yield* adapter.sendTurn({        sessionId: session.sessionId,        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",          "turn.started",          "thread.started",          "message.delta",          "tool.started",          "tool.completed",          "message.completed",          "turn.completed",        ],      );       const turnStarted = runtimeEvents[1];      assert.equal(turnStarted?.type, "turn.started");      if (turnStarted?.type === "turn.started") {        assert.equal(turnStarted.turnId, turn.turnId);      }       const deltaEvent = runtimeEvents[3];      assert.equal(deltaEvent?.type, "message.delta");      if (deltaEvent?.type === "message.delta") {        assert.equal(deltaEvent.delta, "Hi");        assert.equal(deltaEvent.turnId, turn.turnId);      }       const toolStarted = runtimeEvents[4];      assert.equal(toolStarted?.type, "tool.started");      if (toolStarted?.type === "tool.started") {        assert.equal(toolStarted.toolKind, "command");      }       const turnCompleted = runtimeEvents[7];      assert.equal(turnCompleted?.type, "turn.completed");      if (turnCompleted?.type === "turn.completed") {        assert.equal(turnCompleted.turnId, turn.turnId);        assert.equal(turnCompleted.status, "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, 6).pipe(        Stream.runCollect,        Effect.forkChild,      );       const session = yield* adapter.startSession({        provider: "claudeCode",      });       const turn = yield* adapter.sendTurn({        sessionId: session.sessionId,        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",          "turn.started",          "thread.started",          "message.delta",          "message.completed",          "turn.completed",        ],      );       const deltaIndex = runtimeEvents.findIndex((event) => event.type === "message.delta");      const completedIndex = runtimeEvents.findIndex((event) => event.type === "message.completed");      assert.equal(deltaIndex >= 0 && completedIndex >= 0 && deltaIndex < completedIndex, true);       const deltaEvent = runtimeEvents[deltaIndex];      assert.equal(deltaEvent?.type, "message.delta");      if (deltaEvent?.type === "message.delta") {        assert.equal(deltaEvent.delta, "Late text");        assert.equal(deltaEvent.turnId, 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, 6).pipe(        Stream.runCollect,        Effect.forkChild,      );       const session = yield* adapter.startSession({        provider: "claudeCode",      });       const turn = yield* adapter.sendTurn({        sessionId: session.sessionId,        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",          "turn.started",          "thread.started",          "message.delta",          "message.completed",          "turn.completed",        ],      );       const deltaEvent = runtimeEvents.find((event) => event.type === "message.delta");      assert.equal(deltaEvent?.type, "message.delta");      if (deltaEvent?.type === "message.delta") {        assert.equal(deltaEvent.delta, "Fallback hello");        assert.equal(deltaEvent.turnId, turn.turnId);

Lines 1–500 of 884.