2995 lines · 102.5 KB · TypeScript
RawDownload
// @effect-diagnostics nodeBuiltinImport:offimport * as NodeAssert from "node:assert/strict";import * as NodeFS from "node:fs";import * as NodeOS from "node:os";import * as NodePath from "node:path";import {  ApprovalRequestId,  CodexSettings,  EventId,  ProviderDriverKind,  ProviderInstanceId,  ProviderItemId,  type ProviderApprovalDecision,  type ProviderEvent,  type ProviderSession,  type ProviderTurnStartResult,  type ProviderUserInputAnswers,  ThreadId,  TurnId,} from "@t3tools/contracts";import { createModelSelection } from "@t3tools/shared/model";import * as NodeServices from "@effect/platform-node/NodeServices";import { it, vi } from "@effect/vitest"; import * as Context from "effect/Context";import * as Effect from "effect/Effect";import * as Exit from "effect/Exit";import * as Fiber from "effect/Fiber";import * as Layer from "effect/Layer";import * as Option from "effect/Option";import * as Queue from "effect/Queue";import * as Schema from "effect/Schema";import * as Scope from "effect/Scope";import * as Stream from "effect/Stream";import * as TestClock from "effect/testing/TestClock";import * as CodexErrors from "effect-codex-app-server/errors"; import { ServerConfig } from "../../config.ts";import { ServerSettingsService } from "../../serverSettings.ts";import { ProviderAdapterValidationError } from "../Errors.ts";import type { CodexAdapterShape } from "../Services/CodexAdapter.ts";import { ProviderSessionDirectory } from "../Services/ProviderSessionDirectory.ts";import {  type CodexSessionRuntimeOptions,  type CodexSessionRuntimeSendTurnInput,  type CodexSessionRuntimeShape,  type CodexThreadSnapshot,} from "./CodexSessionRuntime.ts";import { makeCodexAdapter } from "./CodexAdapter.ts";const decodeCodexSettings = Schema.decodeSync(CodexSettings); // Test-local service tag so the rest of the file can keep using `yield* CodexAdapter`.class CodexAdapter extends Context.Service<CodexAdapter, CodexAdapterShape>()(  "t3/provider/Layers/CodexAdapter.test/CodexAdapter",) {} const asThreadId = (value: string): ThreadId => ThreadId.make(value);const asTurnId = (value: string): TurnId => TurnId.make(value);const asEventId = (value: string): EventId => EventId.make(value);const asItemId = (value: string): ProviderItemId => ProviderItemId.make(value); class FakeCodexRuntime implements CodexSessionRuntimeShape {  private readonly eventQueue = Effect.runSync(Queue.unbounded<ProviderEvent>());  private readonly now = "2026-01-01T00:00:00.000Z";   public readonly startImpl = vi.fn(() =>    Promise.resolve({      provider: ProviderDriverKind.make("codex"),      status: "ready" as const,      runtimeMode: this.options.runtimeMode,      threadId: this.options.threadId,      cwd: this.options.cwd,      ...(this.options.model ? { model: this.options.model } : {}),      createdAt: this.now,      updatedAt: this.now,    } satisfies ProviderSession),  );   public readonly sendTurnImpl = vi.fn(    (_input: CodexSessionRuntimeSendTurnInput): Promise<ProviderTurnStartResult> =>      Promise.resolve({        threadId: this.options.threadId,        turnId: asTurnId("turn-1"),      }),  );   public readonly compactThread = Effect.void;   public readonly interruptTurnImpl = vi.fn((_turnId?: TurnId): Promise<void> =>    Promise.resolve(undefined),  );   public readonly readThreadImpl = vi.fn((): Promise<CodexThreadSnapshot> =>    Promise.resolve({      threadId: "provider-thread-1",      turns: [],    }),  );   public readonly rollbackThreadImpl = vi.fn((_numTurns: number): Promise<CodexThreadSnapshot> =>    Promise.resolve({      threadId: "provider-thread-1",      turns: [],    }),  );   public readonly uploadFeedbackImpl = vi.fn((_reason?: string) =>    Promise.resolve({ threadId: "provider-thread-1" }),  );   public readonly respondToRequestImpl = vi.fn(    (_requestId: ApprovalRequestId, _decision: ProviderApprovalDecision): Promise<void> =>      Promise.resolve(undefined),  );   public readonly respondToUserInputImpl = vi.fn(    (_requestId: ApprovalRequestId, _answers: ProviderUserInputAnswers): Promise<void> =>      Promise.resolve(undefined),  );   public readonly closeImpl = vi.fn(() => Promise.resolve(undefined));   readonly options: CodexSessionRuntimeOptions;   constructor(options: CodexSessionRuntimeOptions) {    this.options = options;  }   start() {    return Effect.promise(() => this.startImpl());  }   getSession = Effect.promise(() => this.startImpl());   sendTurn(input: CodexSessionRuntimeSendTurnInput) {    return Effect.promise(() => this.sendTurnImpl(input));  }   interruptTurn(turnId?: TurnId) {    return Effect.promise(() => this.interruptTurnImpl(turnId));  }   readThread = Effect.promise(() => this.readThreadImpl());   rollbackThread(numTurns: number) {    return Effect.promise(() => this.rollbackThreadImpl(numTurns));  }   uploadFeedback(reason?: string) {    return Effect.promise(() => this.uploadFeedbackImpl(reason));  }   respondToRequest(requestId: ApprovalRequestId, decision: ProviderApprovalDecision) {    return Effect.promise(() => this.respondToRequestImpl(requestId, decision));  }   respondToUserInput(requestId: ApprovalRequestId, answers: ProviderUserInputAnswers) {    return Effect.promise(() => this.respondToUserInputImpl(requestId, answers));  }   get events() {    return Stream.fromQueue(this.eventQueue);  }   close = Effect.promise(() => this.closeImpl());   emit(event: ProviderEvent) {    return Queue.offer(this.eventQueue, event).pipe(Effect.asVoid);  }} function makeRuntimeFactory() {  const runtimes: Array<FakeCodexRuntime> = [];  const factory = vi.fn((options: CodexSessionRuntimeOptions) => {    const runtime = new FakeCodexRuntime(options);    runtimes.push(runtime);    return Effect.succeed(runtime);  });   return {    factory,    get lastRuntime(): FakeCodexRuntime | undefined {      return runtimes.at(-1);    },  };} function makeScopedRuntimeFactory(options?: { readonly failConstruction?: boolean }) {  const runtimes: Array<FakeCodexRuntime> = [];  const releasedThreadIds: Array<ThreadId> = [];   const factory = vi.fn((runtimeOptions: CodexSessionRuntimeOptions) =>    Effect.gen(function* () {      yield* Scope.Scope;      yield* Effect.addFinalizer(() =>        Effect.sync(() => {          releasedThreadIds.push(runtimeOptions.threadId);        }),      );       if (options?.failConstruction) {        return yield* new CodexErrors.CodexAppServerSpawnError({          command: `${runtimeOptions.binaryPath} app-server`,          cause: new Error("runtime construction failed"),        });      }       const runtime = new FakeCodexRuntime(runtimeOptions);      runtimes.push(runtime);      return runtime;    }),  );   return {    factory,    releasedThreadIds,    get lastRuntime(): FakeCodexRuntime | undefined {      return runtimes.at(-1);    },  };} const providerSessionDirectoryTestLayer = Layer.succeed(ProviderSessionDirectory, {  upsert: () => Effect.void,  recordImportedTranscript: () => Effect.die("unused"),  getProvider: () =>    Effect.die(new Error("ProviderSessionDirectory.getProvider is not used in test")),  getBinding: () => Effect.succeed(Option.none()),  listThreadIds: () => Effect.succeed([]),  listBindings: () => Effect.succeed([]),}); const validationRuntimeFactory = makeRuntimeFactory();const validationLayer = it.layer(  Layer.effect(    CodexAdapter,    Effect.gen(function* () {      const codexConfig = decodeCodexSettings({});      return yield* makeCodexAdapter(codexConfig, {        makeRuntime: validationRuntimeFactory.factory,      });    }),  ).pipe(    Layer.provideMerge(ServerConfig.layerTest(process.cwd(), process.cwd())),    Layer.provideMerge(ServerSettingsService.layerTest()),    Layer.provideMerge(providerSessionDirectoryTestLayer),    Layer.provideMerge(NodeServices.layer),  ),); validationLayer("CodexAdapterLive validation", (it) => {  it.effect("returns validation error for non-codex provider on startSession", () =>    Effect.gen(function* () {      const adapter = yield* CodexAdapter;      const result = yield* adapter        .startSession({          provider: ProviderDriverKind.make("claudeAgent"),          threadId: asThreadId("thread-1"),          runtimeMode: "full-access",        })        .pipe(Effect.result);       NodeAssert.equal(result._tag, "Failure");      NodeAssert.deepStrictEqual(        result.failure,        new ProviderAdapterValidationError({          provider: ProviderDriverKind.make("codex"),          operation: "startSession",          issue: "Expected provider 'codex' but received 'claudeAgent'.",        }),      );      NodeAssert.equal(validationRuntimeFactory.factory.mock.calls.length, 0);    }),  );  it.effect("maps codex model options before starting a session", () =>    Effect.gen(function* () {      validationRuntimeFactory.factory.mockClear();      const adapter = yield* CodexAdapter;       yield* adapter.startSession({        provider: ProviderDriverKind.make("codex"),        threadId: asThreadId("thread-1"),        modelSelection: createModelSelection(ProviderInstanceId.make("codex"), "gpt-5.3-codex", [          { id: "serviceTier", value: "priority" },        ]),        runtimeMode: "full-access",      });       NodeAssert.deepStrictEqual(validationRuntimeFactory.factory.mock.calls[0]?.[0], {        binaryPath: "codex",        cwd: process.cwd(),        launchArgs: "",        model: "gpt-5.3-codex",        providerInstanceId: ProviderInstanceId.make("codex"),        serviceTier: "priority",        threadId: asThreadId("thread-1"),        runtimeMode: "full-access",      });    }),  );}); const sessionRuntimeFactory = makeRuntimeFactory();const sessionErrorLayer = it.layer(  Layer.effect(    CodexAdapter,    Effect.gen(function* () {      const codexConfig = decodeCodexSettings({});      return yield* makeCodexAdapter(codexConfig, {        makeRuntime: sessionRuntimeFactory.factory,      });    }),  ).pipe(    Layer.provideMerge(ServerConfig.layerTest(process.cwd(), process.cwd())),    Layer.provideMerge(ServerSettingsService.layerTest()),    Layer.provideMerge(providerSessionDirectoryTestLayer),    Layer.provideMerge(NodeServices.layer),  ),); sessionErrorLayer("CodexAdapterLive session errors", (it) => {  it.effect("maps missing adapter sessions to ProviderAdapterSessionNotFoundError", () =>    Effect.gen(function* () {      const adapter = yield* CodexAdapter;      const result = yield* adapter        .sendTurn({          threadId: asThreadId("sess-missing"),          input: "hello",          attachments: [],        })        .pipe(Effect.result);       NodeAssert.equal(result._tag, "Failure");      NodeAssert.equal(result.failure._tag, "ProviderAdapterSessionNotFoundError");      NodeAssert.equal(result.failure.provider, "codex");      NodeAssert.equal(result.failure.threadId, "sess-missing");    }),  );   it.effect("compacts the active Codex thread and emits compacted state", () =>    Effect.gen(function* () {      const adapter = yield* CodexAdapter;      const threadId = asThreadId("thread-compact");      yield* adapter.startSession({        provider: ProviderDriverKind.make("codex"),        threadId,        runtimeMode: "full-access",      });      const runtime = sessionRuntimeFactory.lastRuntime;      NodeAssert.ok(runtime);      const compactedEventFiber = yield* adapter.streamEvents.pipe(        Stream.filter((event) => event.type === "thread.state.changed"),        Stream.runHead,        Effect.forkChild,      );      NodeAssert.ok(adapter.compaction?.type === "native");      yield* adapter.compaction.start(threadId);      yield* runtime.emit({        id: asEventId("evt-compaction-item-completed"),        kind: "notification",        provider: ProviderDriverKind.make("codex"),        createdAt: "2026-01-01T00:00:00.000Z",        method: "item/completed",        threadId,        payload: {          completedAtMs: 1_778_000_000_000,          threadId: "provider-thread-1",          turnId: "provider-compact-turn",          item: {            id: "provider-compact-item",            type: "contextCompaction",          },        },      });      const event = Option.getOrThrow(yield* Fiber.join(compactedEventFiber));      NodeAssert.ok(event.type === "thread.state.changed");      NodeAssert.equal(event.payload.state, "compacted");      yield* adapter.stopSession(threadId);    }),  );   it.effect("uploads feedback for the active Codex thread", () =>    Effect.gen(function* () {      const adapter = yield* CodexAdapter;      const threadId = asThreadId("thread-feedback");      yield* adapter.startSession({        provider: ProviderDriverKind.make("codex"),        threadId,        runtimeMode: "full-access",      });      const runtime = sessionRuntimeFactory.lastRuntime;      NodeAssert.ok(runtime);       const result = yield* adapter.uploadFeedback({        threadId,        reason: "The agent stopped early.",      });       NodeAssert.deepStrictEqual(result, { feedbackId: "provider-thread-1" });      NodeAssert.deepStrictEqual(runtime.uploadFeedbackImpl.mock.calls, [        ["The agent stopped early."],      ]);    }),  );   it.effect("rejects feedback for an unknown Codex thread", () =>    Effect.gen(function* () {      const adapter = yield* CodexAdapter;      const result = yield* adapter        .uploadFeedback({ threadId: asThreadId("thread-feedback-missing") })        .pipe(Effect.result);       NodeAssert.equal(result._tag, "Failure");      NodeAssert.equal(result.failure._tag, "ProviderAdapterSessionNotFoundError");    }),  );   it.effect("maps codex model options before sending a turn", () =>    Effect.gen(function* () {      const adapter = yield* CodexAdapter;      yield* adapter.startSession({        provider: ProviderDriverKind.make("codex"),        threadId: asThreadId("sess-missing"),        runtimeMode: "full-access",      });      const runtime = sessionRuntimeFactory.lastRuntime;      NodeAssert.ok(runtime);      runtime.sendTurnImpl.mockClear();       yield* Effect.ignore(        adapter.sendTurn({          threadId: asThreadId("sess-missing"),          input: "hello",          modelSelection: createModelSelection(ProviderInstanceId.make("codex"), "gpt-5.3-codex", [            { id: "reasoningEffort", value: "high" },            { id: "serviceTier", value: "priority" },          ]),          attachments: [],        }),      );       NodeAssert.deepStrictEqual(runtime.sendTurnImpl.mock.calls[0]?.[0], {        input: "hello",        model: "gpt-5.3-codex",        effort: "high",        serviceTier: "priority",      });    }),  );   it.effect("passes configured launch args into the session runtime", () => {    const runtimeFactory = makeRuntimeFactory();    const layer = Layer.effect(      CodexAdapter,      Effect.gen(function* () {        const codexConfig = decodeCodexSettings({ launchArgs: "--strict-config --enable foo" });        return yield* makeCodexAdapter(codexConfig, {          makeRuntime: runtimeFactory.factory,        });      }),    ).pipe(      Layer.provideMerge(ServerConfig.layerTest(process.cwd(), process.cwd())),      Layer.provideMerge(ServerSettingsService.layerTest()),      Layer.provideMerge(providerSessionDirectoryTestLayer),      Layer.provideMerge(NodeServices.layer),    );     return Effect.gen(function* () {      const adapter = yield* CodexAdapter;      yield* adapter.startSession({        provider: ProviderDriverKind.make("codex"),        threadId: asThreadId("sess-launch-args"),        runtimeMode: "full-access",      });       const runtime = runtimeFactory.lastRuntime;      NodeAssert.ok(runtime);      NodeAssert.equal(runtime.options.launchArgs, "--strict-config --enable foo");    }).pipe(Effect.provide(layer));  });   it.effect("uses T3CODE_CODEX_LAUNCH_ARGS for the session runtime", () => {    const runtimeFactory = makeRuntimeFactory();    const layer = Layer.effect(      CodexAdapter,      Effect.gen(function* () {        const codexConfig = decodeCodexSettings({ launchArgs: "--enable settings-feature" });        return yield* makeCodexAdapter(codexConfig, {          environment: { T3CODE_CODEX_LAUNCH_ARGS: " --strict-config --enable env-feature " },          makeRuntime: runtimeFactory.factory,        });      }),    ).pipe(      Layer.provideMerge(ServerConfig.layerTest(process.cwd(), process.cwd())),      Layer.provideMerge(ServerSettingsService.layerTest()),      Layer.provideMerge(providerSessionDirectoryTestLayer),      Layer.provideMerge(NodeServices.layer),    );     return Effect.gen(function* () {

Lines 1–500 of 2995.