1933 lines · 73.0 KB · TypeScript
RawDownload
import * as NodeServices from "@effect/platform-node/NodeServices";import { describe, it, assert } from "@effect/vitest";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 PubSub from "effect/PubSub";import * as Ref from "effect/Ref";import * as Schema from "effect/Schema";import * as Scope from "effect/Scope";import * as Sink from "effect/Sink";import * as Stream from "effect/Stream";import * as TestClock from "effect/testing/TestClock";import * as CodexErrors from "effect-codex-app-server/errors";import {  ClaudeSettings,  CodexSettings,  DEFAULT_SERVER_SETTINGS,  ProviderDriverKind,  ProviderInstanceId,  ServerSettings,  type ServerProvider,  type ServerProviderSlashCommand,  type ServerSettings as ContractServerSettings,} from "@t3tools/contracts";import * as PlatformError from "effect/PlatformError";import { HttpClient, HttpClientResponse } from "effect/unstable/http";import { ChildProcessSpawner } from "effect/unstable/process";import { deepMerge } from "@t3tools/shared/Struct";import { createModelCapabilities } from "@t3tools/shared/model";import { applyServerSettingsPatch } from "@t3tools/shared/serverSettings"; import { checkCodexProviderStatus, type CodexAppServerProviderSnapshot } from "./CodexProvider.ts";import { checkClaudeProviderStatus } from "./ClaudeProvider.ts";import * as OpenCodeRuntime from "../opencodeRuntime.ts";import * as ProviderEventLoggers from "./ProviderEventLoggers.ts";import { ProviderInstanceRegistryHydrationLive } from "./ProviderInstanceRegistryHydration.ts";import {  haveProvidersChanged,  mergeProviderSnapshot,  mergeProviderSnapshots,  ProviderRegistryLive,  selectProvidersByKind,} from "./ProviderRegistry.ts";import * as ServerConfig from "../../config.ts";import * as ServerSettingsModule from "../../serverSettings.ts";import { readProviderStatusCache, resolveProviderStatusCachePath } from "../providerStatusCache.ts";import type { ProviderInstance } from "../ProviderDriver.ts";import * as ProviderInstanceRegistry from "../Services/ProviderInstanceRegistry.ts";import * as ProviderRegistry from "../Services/ProviderRegistry.ts";import { makeManualOnlyProviderMaintenanceCapabilities } from "../providerMaintenance.ts";const decodeServerSettings = Schema.decodeSync(ServerSettings);const encodeServerSettings = Schema.encodeSync(ServerSettings);const encodedDefaultServerSettings = encodeServerSettings(DEFAULT_SERVER_SETTINGS); const defaultClaudeSettings: ClaudeSettings = Schema.decodeSync(ClaudeSettings)({});const defaultCodexSettings: CodexSettings = Schema.decodeSync(CodexSettings)({});const disabledCodexSettings: CodexSettings = Schema.decodeSync(CodexSettings)({  enabled: false,}); process.env.T3CODE_CURSOR_ENABLED = "1"; // ── Test helpers ──────────────────────────────────────────────────── const encoder = new TextEncoder(); const TestHttpClientLive = Layer.succeed(  HttpClient.HttpClient,  HttpClient.make((request) =>    Effect.succeed(HttpClientResponse.fromWeb(request, Response.json({ version: "0.0.0" }))),  ),); function selectDescriptor(  id: string,  label: string,  options: ReadonlyArray<{ id: string; label: string; isDefault?: boolean }>,) {  return {    id,    label,    type: "select" as const,    options: [...options],    ...(options.find((option) => option.isDefault)?.id      ? { currentValue: options.find((option) => option.isDefault)?.id }      : {}),  };} function booleanDescriptor(id: string, label: string) {  return {    id,    label,    type: "boolean" as const,  };} type TestClaudeCapabilities = {  readonly email: string | undefined;  readonly subscriptionType: string | undefined;  readonly tokenSource: string | undefined;  readonly slashCommands: ReadonlyArray<ServerProviderSlashCommand>;}; function claudeCapabilities(overrides: Partial<TestClaudeCapabilities> = {}) {  return () =>    Effect.succeed({      email: undefined,      subscriptionType: undefined,      tokenSource: undefined,      slashCommands: [],      ...overrides,    });} const noClaudeCapabilities = () =>  Effect.sync(() => undefined as TestClaudeCapabilities | undefined); function mockHandle(result: { stdout: string; stderr: string; code: number }) {  return ChildProcessSpawner.makeHandle({    pid: ChildProcessSpawner.ProcessId(1),    exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(result.code)),    isRunning: Effect.succeed(false),    kill: () => Effect.void,    unref: Effect.succeed(Effect.void),    stdin: Sink.drain,    stdout: Stream.make(encoder.encode(result.stdout)),    stderr: Stream.make(encoder.encode(result.stderr)),    all: Stream.empty,    getInputFd: () => Sink.drain,    getOutputFd: () => Stream.empty,  });} function mockSpawnerLayer(  handler: (args: ReadonlyArray<string>) => {    stdout: string;    stderr: string;    code: number;  },) {  return Layer.succeed(    ChildProcessSpawner.ChildProcessSpawner,    ChildProcessSpawner.make((command) => {      const cmd = command as unknown as { args: ReadonlyArray<string> };      return Effect.succeed(mockHandle(handler(cmd.args)));    }),  );} function recordingMockSpawnerLayer(  handler: (args: ReadonlyArray<string>) => {    stdout: string;    stderr: string;    code: number;  },) {  const commands: Array<{    readonly args: ReadonlyArray<string>;    readonly env: NodeJS.ProcessEnv | undefined;  }> = [];  const layer = Layer.succeed(    ChildProcessSpawner.ChildProcessSpawner,    ChildProcessSpawner.make((command) => {      const cmd = command as unknown as {        args: ReadonlyArray<string>;        options?: {          readonly env?: NodeJS.ProcessEnv;        };      };      commands.push({ args: cmd.args, env: cmd.options?.env });      return Effect.succeed(mockHandle(handler(cmd.args)));    }),  );  return { layer, commands };} function mockCommandSpawnerLayer(  handler: (    command: string,    args: ReadonlyArray<string>,  ) => { stdout: string; stderr: string; code: number },) {  return Layer.succeed(    ChildProcessSpawner.ChildProcessSpawner,    ChildProcessSpawner.make((command) => {      const cmd = command as unknown as {        command: string;        args: ReadonlyArray<string>;      };      return Effect.succeed(mockHandle(handler(cmd.command, cmd.args)));    }),  );} function failingSpawnerLayer(description: string) {  return Layer.succeed(    ChildProcessSpawner.ChildProcessSpawner,    ChildProcessSpawner.make(() =>      Effect.fail(        PlatformError.systemError({          _tag: "NotFound",          module: "ChildProcess",          method: "spawn",          description,        }),      ),    ),  );} function hangingScopedSpawnerLayer(killCalls: Ref.Ref<number>) {  return Layer.succeed(    ChildProcessSpawner.ChildProcessSpawner,    ChildProcessSpawner.make(() =>      Effect.gen(function* () {        const handle = ChildProcessSpawner.makeHandle({          pid: ChildProcessSpawner.ProcessId(1),          exitCode: Effect.never,          isRunning: Effect.succeed(true),          kill: () => Ref.update(killCalls, (current) => current + 1),          unref: Effect.succeed(Effect.void),          stdin: Sink.drain,          stdout: Stream.never,          stderr: Stream.never,          all: Stream.never,          getInputFd: () => Sink.drain,          getOutputFd: () => Stream.empty,        });        yield* Effect.addFinalizer(() => handle.kill().pipe(Effect.ignore));        return handle;      }),    ),  );} const codexModelCapabilities = createModelCapabilities({  optionDescriptors: [    selectDescriptor("reasoningEffort", "Reasoning", [      { id: "high", label: "High", isDefault: true },      { id: "low", label: "Low" },    ]),    booleanDescriptor("fastMode", "Fast Mode"),  ],}) satisfies NonNullable<ServerProvider["models"][number]["capabilities"]>; function makeCodexProbeSnapshot(  input: Partial<CodexAppServerProviderSnapshot> = {},): CodexAppServerProviderSnapshot {  return {    version: "1.0.0",    account: {      account: {        type: "chatgpt",        email: "test@example.com",        planType: "pro",      },      requiresOpenaiAuth: false,    },    models: [      {        slug: "gpt-live-codex",        name: "GPT Live Codex",        isCustom: false,        capabilities: codexModelCapabilities,      },    ],    skills: [],    ...input,  };} function makeMutableServerSettingsService(  initial: ContractServerSettings = DEFAULT_SERVER_SETTINGS,) {  return Effect.gen(function* () {    const settingsRef = yield* Ref.make(initial);    const changes = yield* PubSub.unbounded<ContractServerSettings>();     return {      start: Effect.void,      ready: Effect.void,      getSettings: Ref.get(settingsRef),      updateSettings: (patch) =>        Effect.gen(function* () {          const current = yield* Ref.get(settingsRef);          const next = applyServerSettingsPatch(current, patch);          encodeServerSettings(next);          yield* Ref.set(settingsRef, next);          yield* PubSub.publish(changes, next);          return next;        }),      get streamChanges() {        return Stream.fromPubSub(changes);      },    } satisfies ServerSettingsModule.ServerSettingsService["Service"];  });} it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), TestHttpClientLive))(  "ProviderRegistry",  (it) => {    describe("checkCodexProviderStatus", () => {      it.effect("uses the app-server account and model list for provider status", () =>        Effect.gen(function* () {          const status = yield* checkCodexProviderStatus(defaultCodexSettings, () =>            Effect.succeed(              makeCodexProbeSnapshot({                skills: [                  {                    name: "github:gh-fix-ci",                    path: "/Users/test/.codex/skills/gh-fix-ci/SKILL.md",                    enabled: true,                    displayName: "CI Debug",                    shortDescription: "Debug failing GitHub Actions checks",                  },                ],              }),            ),          );          assert.strictEqual(status.status, "ready");          assert.strictEqual(status.installed, true);          assert.strictEqual(status.version, "1.0.0");          assert.strictEqual(status.auth.status, "authenticated");          assert.strictEqual(status.auth.type, "chatgpt");          assert.strictEqual(status.auth.label, "ChatGPT Pro 20x Subscription");          assert.strictEqual(status.auth.email, "test@example.com");          assert.deepStrictEqual(status.models, [            {              slug: "gpt-live-codex",              name: "GPT Live Codex",              isCustom: false,              capabilities: codexModelCapabilities,            },          ]);          assert.deepStrictEqual(status.skills, [            {              name: "github:gh-fix-ci",              path: "/Users/test/.codex/skills/gh-fix-ci/SKILL.md",              enabled: true,              displayName: "CI Debug",              shortDescription: "Debug failing GitHub Actions checks",            },          ]);        }),      );       it.effect("returns unauthenticated when app-server requires OpenAI auth", () =>        Effect.gen(function* () {          const status = yield* checkCodexProviderStatus(defaultCodexSettings, () =>            Effect.succeed(              makeCodexProbeSnapshot({                account: {                  account: null,                  requiresOpenaiAuth: true,                },              }),            ),          );           assert.strictEqual(status.status, "error");          assert.strictEqual(status.auth.status, "unauthenticated");          assert.strictEqual(            status.message,            "Codex CLI is not authenticated. Run `codex login` and try again.",          );        }),      );       it.effect(        "returns ready with unknown auth when app-server does not require OpenAI auth",        () =>          Effect.gen(function* () {            const status = yield* checkCodexProviderStatus(defaultCodexSettings, () =>              Effect.succeed(                makeCodexProbeSnapshot({                  account: {                    account: null,                    requiresOpenaiAuth: false,                  },                }),              ),            );             assert.strictEqual(status.status, "ready");            assert.strictEqual(status.auth.status, "unknown");          }),      );       it.effect("returns an api key label for codex api key auth", () =>        Effect.gen(function* () {          const status = yield* checkCodexProviderStatus(defaultCodexSettings, () =>            Effect.succeed(              makeCodexProbeSnapshot({                account: {                  account: { type: "apiKey" },                  requiresOpenaiAuth: false,                },              }),            ),          );           assert.strictEqual(status.status, "ready");          assert.strictEqual(status.auth.status, "authenticated");          assert.strictEqual(status.auth.type, "apiKey");          assert.strictEqual(status.auth.label, "OpenAI API Key");        }),      );       it.effect("returns an Amazon Bedrock label for codex Bedrock auth", () =>        Effect.gen(function* () {          const status = yield* checkCodexProviderStatus(defaultCodexSettings, () =>            Effect.succeed(              makeCodexProbeSnapshot({                account: {                  account: { type: "amazonBedrock" },                  requiresOpenaiAuth: false,                },              }),            ),          );           assert.strictEqual(status.status, "ready");          assert.strictEqual(status.auth.status, "authenticated");          assert.strictEqual(status.auth.type, "amazonBedrock");          assert.strictEqual(status.auth.label, "Amazon Bedrock");        }),      );       it.effect("returns unavailable when codex is missing", () =>        Effect.gen(function* () {          const status = yield* checkCodexProviderStatus(defaultCodexSettings, () =>            Effect.fail(              new CodexErrors.CodexAppServerSpawnError({                command: "codex app-server",                cause: new Error("spawn codex ENOENT"),              }),            ),          );          assert.strictEqual(status.status, "error");          assert.strictEqual(status.installed, false);          assert.strictEqual(status.auth.status, "unknown");          assert.strictEqual(            status.message,            "Codex CLI (`codex`) is not installed or not on PATH.",          );        }),      );       it.effect("closes the app-server probe scope when provider status times out", () =>        Effect.gen(function* () {          const killCalls = yield* Ref.make(0);          const statusFiber = yield* checkCodexProviderStatus(defaultCodexSettings).pipe(            Effect.provide(hangingScopedSpawnerLayer(killCalls)),            Effect.forkChild,          );           yield* Effect.yieldNow;          yield* TestClock.adjust("11 seconds");          yield* Effect.yieldNow;           const status = yield* Fiber.join(statusFiber);          assert.strictEqual(status.status, "error");          assert.strictEqual(            status.message,            "Timed out while checking Codex app-server provider status.",          );          assert.strictEqual(yield* Ref.get(killCalls), 1);        }),      );    });     describe("ProviderRegistryLive", () => {      it("treats equal provider snapshots as unchanged", () => {        const providers = [          {            instanceId: ProviderInstanceId.make("codex"),            driver: ProviderDriverKind.make("codex"),            status: "ready",            enabled: true,            installed: true,            auth: { status: "authenticated" },            checkedAt: "2026-03-25T00:00:00.000Z",            version: "1.0.0",            models: [],            slashCommands: [],            skills: [],          },          {            instanceId: ProviderInstanceId.make("claudeAgent"),            driver: ProviderDriverKind.make("claudeAgent"),            status: "warning",            enabled: true,            installed: true,            auth: { status: "unknown" },            checkedAt: "2026-03-25T00:00:00.000Z",            version: "1.0.0",            models: [],            slashCommands: [],

Lines 1–500 of 1933.