7281 lines · 244.2 KB · TypeScript
RawDownload
import { buildRuntimeInstructions } from "../RuntimeInstructions.ts";// @effect-diagnostics nodeBuiltinImport:offimport * as NodeFS from "node:fs";import * as NodeOS from "node:os";import * as NodePath from "node:path"; import * as NodeServices from "@effect/platform-node/NodeServices";import type {  Options as ClaudeQueryOptions,  PermissionMode,  PermissionResult,  SDKMessage,  SDKUserMessage,} from "@anthropic-ai/claude-agent-sdk";import {  ApprovalRequestId,  ClaudeSettings,  ProviderDriverKind,  ProviderItemId,  ProviderRuntimeEvent,  type RuntimeMode,  ThreadId,  ProviderInstanceId,} from "@t3tools/contracts";import { createModelSelection } from "@t3tools/shared/model";import { assert, describe, it } from "@effect/vitest";import * as Clock from "effect/Clock";import * as Context from "effect/Context";import * as Deferred from "effect/Deferred";import * as Effect from "effect/Effect";import * as Fiber from "effect/Fiber";import * as Layer from "effect/Layer";import * as Random from "effect/Random";import * as Ref from "effect/Ref";import * as Schema from "effect/Schema";import * as Stream from "effect/Stream";import * as TestClock from "effect/testing/TestClock"; import { attachmentRelativePath } from "../../attachmentStore.ts";import { ServerConfig } from "../../config.ts";import { ServerSettingsService } from "../../serverSettings.ts";import {  SYNTHETIC_CLAUDE_CAPABLE_MODEL,  SYNTHETIC_CLAUDE_COLLIDING_ALIAS,  SYNTHETIC_CLAUDE_MODEL_CATALOG,  SYNTHETIC_CLAUDE_STANDARD_MODEL,  SYNTHETIC_CLAUDE_THINKING_MODEL,} from "../ClaudeModelCatalog.testFixtures.ts";import { ProviderAdapterProcessError, ProviderAdapterValidationError } from "../Errors.ts";import type { ClaudeAdapterShape } from "../Services/ClaudeAdapter.ts";import type { ClaudeScopedLimitNames } from "./claudeUsageLimits.ts";import { makeClaudeAdapter, type ClaudeAdapterLiveOptions } from "./ClaudeAdapter.ts";const decodeClaudeSettings = Schema.decodeSync(ClaudeSettings);const encodeUnknownJsonString = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown)); // Test-local service tag so the rest of the file can keep using `yield* ClaudeAdapter`.class ClaudeAdapter extends Context.Service<ClaudeAdapter, ClaudeAdapterShape>()(  "t3/provider/Layers/ClaudeAdapter.test/ClaudeAdapter",) {} class FakeClaudeQuery implements AsyncIterable<SDKMessage> {  private readonly queue: Array<SDKMessage> = [];  private readonly waiters: Array<{    readonly resolve: (value: IteratorResult<SDKMessage>) => void;    readonly reject: (reason: unknown) => void;  }> = [];  private done = false;  private failure: unknown | undefined;   public readonly setModelCalls: Array<string | undefined> = [];  public readonly setPermissionModeCalls: Array<string> = [];  public readonly setMaxThinkingTokensCalls: Array<number | null> = [];  public closeCalls = 0;  public closeError: unknown | undefined;   emit(message: SDKMessage): void {    if (this.done) {      return;    }    const waiter = this.waiters.shift();    if (waiter) {      waiter.resolve({ done: false, value: message });      return;    }    this.queue.push(message);  }   fail(cause: unknown): void {    if (this.done) {      return;    }    this.done = true;    this.failure = cause;    for (const waiter of this.waiters.splice(0)) {      waiter.reject(cause);    }  }   finish(): void {    if (this.done) {      return;    }    this.done = true;    this.failure = undefined;    for (const waiter of this.waiters.splice(0)) {      waiter.resolve({ done: true, value: 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;    if (this.closeError !== undefined) {      throw this.closeError;    }    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.failure !== undefined) {          const failure = this.failure;          this.failure = undefined;          return Promise.reject(failure);        }        if (this.done) {          return Promise.resolve({            done: true,            value: undefined,          });        }        return new Promise((resolve, reject) => {          this.waiters.push({            resolve,            reject,          });        });      },    };  }} function makeHarness(config?: {  readonly nativeEventLogPath?: string;  readonly nativeEventLogger?: ClaudeAdapterLiveOptions["nativeEventLogger"];  readonly cwd?: string;  readonly baseDir?: string;  readonly claudeConfig?: Partial<ClaudeSettings>;  readonly instanceId?: ProviderInstanceId;  readonly scopedLimitNames?: ClaudeAdapterLiveOptions["scopedLimitNames"];  readonly environment?: ClaudeAdapterLiveOptions["environment"];}) {  const query = new FakeClaudeQuery();  let createInput:    | {        readonly prompt: AsyncIterable<SDKUserMessage>;        readonly options: ClaudeQueryOptions;      }    | undefined;   const adapterOptions: ClaudeAdapterLiveOptions = {    ...(config?.environment ? { environment: config.environment } : {}),    ...(config?.instanceId ? { instanceId: config.instanceId } : {}),    ...(config?.scopedLimitNames ? { scopedLimitNames: config.scopedLimitNames } : {}),    modelCatalog: Effect.succeed(SYNTHETIC_CLAUDE_MODEL_CATALOG),    createQuery: (input) => {      createInput = input;      return query;    },    ...(config?.nativeEventLogger      ? {          nativeEventLogger: config.nativeEventLogger,        }      : {}),    ...(config?.nativeEventLogPath      ? {          nativeEventLogPath: config.nativeEventLogPath,        }      : {}),  };   return {    layer: Layer.effect(      ClaudeAdapter,      Effect.gen(function* () {        const claudeConfig = decodeClaudeSettings(config?.claudeConfig ?? {});        return yield* makeClaudeAdapter(claudeConfig, adapterOptions);      }),    ).pipe(      Layer.provideMerge(        ServerConfig.layerTest(          config?.cwd ?? "/tmp/claude-adapter-test",          config?.baseDir ?? "/tmp",        ),      ),      Layer.provideMerge(ServerSettingsService.layerTest()),      Layer.provideMerge(NodeServices.layer),    ),    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,  };} async function readFirstPromptText(  input:    | {        readonly prompt: AsyncIterable<SDKUserMessage>;      }    | undefined,): Promise<string | undefined> {  const iterator = input?.prompt[Symbol.asyncIterator]();  if (!iterator) {    return undefined;  }  const next = await iterator.next();  if (next.done) {    return undefined;  }  if (typeof next.value.message.content === "string") {    return next.value.message.content;  }  const content = next.value.message.content[0];  if (!content || content.type !== "text") {    return undefined;  }  return content.text;} async function readFirstPromptMessage(  input:    | {        readonly prompt: AsyncIterable<SDKUserMessage>;      }    | undefined,): Promise<SDKUserMessage | undefined> {  const iterator = input?.prompt[Symbol.asyncIterator]();  if (!iterator) {    return undefined;  }  const next = await iterator.next();  if (next.done) {    return undefined;  }  return next.value;} /** Drains the first `count` queued prompts so consecutive turns can be compared. */async function readPromptMessages(  input:    | {        readonly prompt: AsyncIterable<SDKUserMessage>;      }    | undefined,  count: number,): Promise<Array<SDKUserMessage>> {  const iterator = input?.prompt[Symbol.asyncIterator]();  if (!iterator) {    return [];  }  const messages: Array<SDKUserMessage> = [];  while (messages.length < count) {    const next = await iterator.next();    if (next.done) {      break;    }    messages.push(next.value);  }  return messages;} const THREAD_ID = ThreadId.make("thread-claude-1");const RESUME_THREAD_ID = ThreadId.make("thread-claude-resume");const SYNTHETIC_SUBAGENT_MODEL = "claude-synthetic-subagent[expanded]"; describe("ClaudeAdapterLive", () => {  it.effect("returns validation error for non-claude provider on startSession", () => {    const harness = makeHarness();    return Effect.gen(function* () {      const adapter = yield* ClaudeAdapter;      const result = yield* adapter        .startSession({          threadId: THREAD_ID,          provider: ProviderDriverKind.make("codex"),          runtimeMode: "full-access",        })        .pipe(Effect.result);       assert.equal(result._tag, "Failure");      if (result._tag !== "Failure") {        return;      }      assert.deepEqual(        result.failure,        new ProviderAdapterValidationError({          provider: ProviderDriverKind.make("claudeAgent"),          operation: "startSession",          issue: "Expected provider 'claudeAgent' but received 'codex'.",        }),      );    }).pipe(      Effect.provideService(Random.Random, makeDeterministicRandomService()),      Effect.provide(harness.layer),    );  });   it.effect("retains Claude session startup causes without exposing their messages", () => {    const cause = new Error("credential material that must remain in the cause chain");    const layer = Layer.effect(      ClaudeAdapter,      Effect.gen(function* () {        const claudeConfig = decodeClaudeSettings({});        return yield* makeClaudeAdapter(claudeConfig, {          createQuery: () => {            throw cause;          },        });      }),    ).pipe(      Layer.provideMerge(ServerConfig.layerTest("/tmp/claude-adapter-test", "/tmp")),      Layer.provideMerge(ServerSettingsService.layerTest()),      Layer.provideMerge(NodeServices.layer),    );     return Effect.gen(function* () {      const adapter = yield* ClaudeAdapter;      const error = yield* adapter        .startSession({          threadId: THREAD_ID,          provider: ProviderDriverKind.make("claudeAgent"),          runtimeMode: "full-access",        })        .pipe(Effect.flip);       assert.instanceOf(error, ProviderAdapterProcessError);      assert.equal(error.detail, "Failed to start Claude runtime session.");      assert.strictEqual(error.cause, cause);      assert.notMatch(error.message, /credential material/u);    }).pipe(      Effect.provideService(Random.Random, makeDeterministicRandomService()),      Effect.provide(layer),    );  });   it.effect("derives bypass permission mode from full-access runtime policy", () => {    const harness = makeHarness();    return Effect.gen(function* () {      const adapter = yield* ClaudeAdapter;      yield* adapter.startSession({        threadId: THREAD_ID,        provider: ProviderDriverKind.make("claudeAgent"),        runtimeMode: "full-access",      });       const createInput = harness.getLastCreateQueryInput();      assert.deepEqual(createInput?.options.settingSources, ["user", "project", "local"]);      assert.deepEqual(createInput?.options.systemPrompt, {        type: "preset",        preset: "claude_code",        append: buildRuntimeInstructions({ harness: "Claude Code" }),      });      assert.equal(createInput?.options.permissionMode, "bypassPermissions");      assert.equal(createInput?.options.allowDangerouslySkipPermissions, true);    }).pipe(      Effect.provideService(Random.Random, makeDeterministicRandomService()),      Effect.provide(harness.layer),    );  });   it.effect("derives auto permission mode from auto runtime policy without skip flag", () => {    const harness = makeHarness();    return Effect.gen(function* () {      const adapter = yield* ClaudeAdapter;      yield* adapter.startSession({        threadId: THREAD_ID,        provider: ProviderDriverKind.make("claudeAgent"),        runtimeMode: "auto",      });       const createInput = harness.getLastCreateQueryInput();      assert.equal(createInput?.options.permissionMode, "auto");      assert.equal(createInput?.options.allowDangerouslySkipPermissions, undefined);    }).pipe(      Effect.provideService(Random.Random, makeDeterministicRandomService()),      Effect.provide(harness.layer),    );  });   it.effect("loads Claude filesystem settings sources for SDK sessions", () => {    const harness = makeHarness();    return Effect.gen(function* () {      const adapter = yield* ClaudeAdapter;      yield* adapter.startSession({        threadId: THREAD_ID,        provider: ProviderDriverKind.make("claudeAgent"),        runtimeMode: "approval-required",      });       const createInput = harness.getLastCreateQueryInput();      assert.deepEqual(createInput?.options.settingSources, ["user", "project", "local"]);      assert.equal(createInput?.options.permissionMode, undefined);      assert.equal(createInput?.options.allowDangerouslySkipPermissions, undefined);    }).pipe(      Effect.provideService(Random.Random, makeDeterministicRandomService()),      Effect.provide(harness.layer),    );  });   it.effect("uses bypass permissions for full-access claude sessions", () => {    const harness = makeHarness();    return Effect.gen(function* () {      const adapter = yield* ClaudeAdapter;      yield* adapter.startSession({        threadId: THREAD_ID,        provider: ProviderDriverKind.make("claudeAgent"),        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("passes the configured auto-compaction window to Claude", () => {    const harness = makeHarness({ claudeConfig: { autoCompactWindow: "300000" } });    return Effect.gen(function* () {      const adapter = yield* ClaudeAdapter;      yield* adapter.startSession({        threadId: THREAD_ID,        provider: ProviderDriverKind.make("claudeAgent"),        runtimeMode: "full-access",      });       const options = harness.getLastCreateQueryInput()?.options;      assert.deepEqual(options?.settings, { autoCompactWindow: 300000 });      assert.deepEqual(options?.supportedDialogKinds, ["resume_return"]);    }).pipe(      Effect.provideService(Random.Random, makeDeterministicRandomService()),      Effect.provide(harness.layer),    );  });   it.effect("forwards claude effort levels into query options", () => {    const harness = makeHarness();    return Effect.gen(function* () {      const adapter = yield* ClaudeAdapter;      yield* adapter.startSession({        threadId: THREAD_ID,        provider: ProviderDriverKind.make("claudeAgent"),        modelSelection: createModelSelection(          ProviderInstanceId.make("claudeAgent"),          SYNTHETIC_CLAUDE_CAPABLE_MODEL,          [{ id: "effort", value: "max" }],        ),        runtimeMode: "full-access",      });       const createInput = harness.getLastCreateQueryInput();      assert.equal(createInput?.options.effort, "max");    }).pipe(

Lines 1–500 of 7281.