6654 lines · 222.3 KB · TypeScript
RawDownload
// @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"];}) {  const query = new FakeClaudeQuery();  let createInput:    | {        readonly prompt: AsyncIterable<SDKUserMessage>;        readonly options: ClaudeQueryOptions;      }    | undefined;   const adapterOptions: ClaudeAdapterLiveOptions = {    ...(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:          "<runtime_info>In case you're asked: you are running in T3 Code through the Claude Code harness. No need to mention this otherwise. You can embed images and videos in your response using Markdown with absolute file paths.</runtime_info>",      });      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(      Effect.provideService(Random.Random, makeDeterministicRandomService()),      Effect.provide(harness.layer),

Lines 1–500 of 6654.

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t