1059 lines · 39.9 KB · TypeScript
RawDownload
import * as NodeServices from "@effect/platform-node/NodeServices";import { describe, it, assert } from "@effect/vitest";import {  Effect,  Exit,  FileSystem,  Layer,  Path,  PubSub,  Ref,  Schema,  Scope,  Sink,  Stream,} from "effect";import {  DEFAULT_SERVER_SETTINGS,  ServerSettings,  type ServerProvider,  type ServerSettings as ContractServerSettings,} from "@t3tools/contracts";import * as PlatformError from "effect/PlatformError";import { ChildProcessSpawner } from "effect/unstable/process";import { deepMerge } from "@t3tools/shared/Struct"; import {  checkCodexProviderStatus,  hasCustomModelProvider,  parseAuthStatusFromOutput,  readCodexConfigModelProvider,} from "./CodexProvider";import { checkClaudeProviderStatus, parseClaudeAuthStatusFromOutput } from "./ClaudeProvider";import { haveProvidersChanged, ProviderRegistryLive } from "./ProviderRegistry";import { ServerSettingsService, type ServerSettingsShape } from "../../serverSettings";import { ProviderRegistry } from "../Services/ProviderRegistry"; // ── Test helpers ──────────────────────────────────────────────────── const encoder = new TextEncoder(); 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,    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 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 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 = Schema.decodeSync(ServerSettings)(deepMerge(current, patch));          yield* Ref.set(settingsRef, next);          yield* PubSub.publish(changes, next);          return next;        }),      get streamChanges() {        return Stream.fromPubSub(changes);      },    } satisfies ServerSettingsShape;  });} /** * Create a temporary CODEX_HOME scoped to the current Effect test. * Cleanup is registered in the test scope rather than via Vitest hooks. */function withTempCodexHome(configContent?: string) {  return Effect.gen(function* () {    const fileSystem = yield* FileSystem.FileSystem;    const path = yield* Path.Path;    const tmpDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-test-codex-" });     yield* Effect.acquireRelease(      Effect.sync(() => {        const originalCodexHome = process.env.CODEX_HOME;        process.env.CODEX_HOME = tmpDir;        return originalCodexHome;      }),      (originalCodexHome) =>        Effect.sync(() => {          if (originalCodexHome !== undefined) {            process.env.CODEX_HOME = originalCodexHome;          } else {            delete process.env.CODEX_HOME;          }        }),    );     if (configContent !== undefined) {      yield* fileSystem.writeFileString(path.join(tmpDir, "config.toml"), configContent);    }     return { tmpDir } as const;  });} it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest()))(  "ProviderRegistry",  (it) => {    // ── checkCodexProviderStatus tests ────────────────────────────────    //    // These tests control CODEX_HOME to ensure the custom-provider detection    // in hasCustomModelProvider() does not interfere with the auth-probe    // path being tested.     describe("checkCodexProviderStatus", () => {      it.effect("returns ready when codex is installed and authenticated", () =>        Effect.gen(function* () {          // Point CODEX_HOME at an empty tmp dir (no config.toml) so the          // default code path (OpenAI provider, auth probe runs) is exercised.          yield* withTempCodexHome();          const status = yield* checkCodexProviderStatus();          assert.strictEqual(status.provider, "codex");          assert.strictEqual(status.status, "ready");          assert.strictEqual(status.installed, true);          assert.strictEqual(status.auth.status, "authenticated");        }).pipe(          Effect.provide(            mockSpawnerLayer((args) => {              const joined = args.join(" ");              if (joined === "--version") return { stdout: "codex 1.0.0\n", stderr: "", code: 0 };              if (joined === "login status") return { stdout: "Logged in\n", stderr: "", code: 0 };              throw new Error(`Unexpected args: ${joined}`);            }),          ),        ),      );       it.effect("returns the codex plan type in auth and keeps spark for supported plans", () =>        Effect.gen(function* () {          yield* withTempCodexHome();          const status = yield* checkCodexProviderStatus(() =>            Effect.succeed({              type: "chatgpt" as const,              planType: "pro" as const,              sparkEnabled: true,            }),          );           assert.strictEqual(status.provider, "codex");          assert.strictEqual(status.status, "ready");          assert.strictEqual(status.auth.status, "authenticated");          assert.strictEqual(status.auth.type, "pro");          assert.strictEqual(status.auth.label, "ChatGPT Pro Subscription");          assert.deepStrictEqual(            status.models.some((model) => model.slug === "gpt-5.3-codex-spark"),            true,          );        }).pipe(          Effect.provide(            mockSpawnerLayer((args) => {              const joined = args.join(" ");              if (joined === "--version") return { stdout: "codex 1.0.0\n", stderr: "", code: 0 };              if (joined === "login status") return { stdout: "Logged in\n", stderr: "", code: 0 };              throw new Error(`Unexpected args: ${joined}`);            }),          ),        ),      );       it.effect("hides spark from codex models for unsupported chatgpt plans", () =>        Effect.gen(function* () {          yield* withTempCodexHome();          const status = yield* checkCodexProviderStatus(() =>            Effect.succeed({              type: "chatgpt" as const,              planType: "plus" as const,              sparkEnabled: false,            }),          );           assert.strictEqual(status.provider, "codex");          assert.strictEqual(status.status, "ready");          assert.strictEqual(status.auth.status, "authenticated");          assert.strictEqual(status.auth.type, "plus");          assert.strictEqual(status.auth.label, "ChatGPT Plus Subscription");          assert.deepStrictEqual(            status.models.some((model) => model.slug === "gpt-5.3-codex-spark"),            false,          );        }).pipe(          Effect.provide(            mockSpawnerLayer((args) => {              const joined = args.join(" ");              if (joined === "--version") return { stdout: "codex 1.0.0\n", stderr: "", code: 0 };              if (joined === "login status") return { stdout: "Logged in\n", stderr: "", code: 0 };              throw new Error(`Unexpected args: ${joined}`);            }),          ),        ),      );       it.effect("hides spark from codex models for non-pro chatgpt subscriptions", () =>        Effect.gen(function* () {          yield* withTempCodexHome();          const status = yield* checkCodexProviderStatus(() =>            Effect.succeed({              type: "chatgpt" as const,              planType: "team" as const,              sparkEnabled: false,            }),          );           assert.strictEqual(status.provider, "codex");          assert.strictEqual(status.auth.type, "team");          assert.strictEqual(status.auth.label, "ChatGPT Team Subscription");          assert.deepStrictEqual(            status.models.some((model) => model.slug === "gpt-5.3-codex-spark"),            false,          );        }).pipe(          Effect.provide(            mockSpawnerLayer((args) => {              const joined = args.join(" ");              if (joined === "--version") return { stdout: "codex 1.0.0\n", stderr: "", code: 0 };              if (joined === "login status") return { stdout: "Logged in\n", stderr: "", code: 0 };              throw new Error(`Unexpected args: ${joined}`);            }),          ),        ),      );       it.effect("returns an api key label for codex api key auth", () =>        Effect.gen(function* () {          yield* withTempCodexHome();          const status = yield* checkCodexProviderStatus(() =>            Effect.succeed({              type: "apiKey" as const,              planType: null,              sparkEnabled: false,            }),          );           assert.strictEqual(status.provider, "codex");          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");          assert.deepStrictEqual(            status.models.some((model) => model.slug === "gpt-5.3-codex-spark"),            false,          );        }).pipe(          Effect.provide(            mockSpawnerLayer((args) => {              const joined = args.join(" ");              if (joined === "--version") return { stdout: "codex 1.0.0\n", stderr: "", code: 0 };              if (joined === "login status") return { stdout: "Logged in\n", stderr: "", code: 0 };              throw new Error(`Unexpected args: ${joined}`);            }),          ),        ),      );       it.effect.skipIf(process.platform === "win32")(        "inherits PATH when launching the codex probe with a CODEX_HOME override",        () =>          Effect.gen(function* () {            const fileSystem = yield* FileSystem.FileSystem;            const path = yield* Path.Path;            const binDir = yield* fileSystem.makeTempDirectoryScoped({              prefix: "t3-test-codex-bin-",            });            const codexPath = path.join(binDir, "codex");            yield* fileSystem.writeFileString(              codexPath,              [                "#!/bin/sh",                'if [ "$1" = "--version" ]; then',                '  echo "codex-cli 1.0.0"',                "  exit 0",                "fi",                'if [ "$1" = "login" ] && [ "$2" = "status" ]; then',                '  echo "Logged in using ChatGPT"',                "  exit 0",                "fi",                'echo "unexpected args: $*" >&2',                "exit 1",                "",              ].join("\n"),            );            yield* fileSystem.chmod(codexPath, 0o755);            const customCodexHome = yield* fileSystem.makeTempDirectoryScoped({              prefix: "t3-test-codex-home-",            });            const previousPath = process.env.PATH;            process.env.PATH = binDir;             try {              const serverSettingsLayer = ServerSettingsService.layerTest({                providers: {                  codex: {                    homePath: customCodexHome,                  },                },              });               const status = yield* checkCodexProviderStatus().pipe(                Effect.provide(serverSettingsLayer),              );              assert.strictEqual(status.provider, "codex");              assert.strictEqual(status.installed, true);              assert.strictEqual(status.status, "ready");              assert.strictEqual(status.auth.status, "authenticated");            } finally {              process.env.PATH = previousPath;            }          }),      );       it.effect("returns unavailable when codex is missing", () =>        Effect.gen(function* () {          yield* withTempCodexHome();          const status = yield* checkCodexProviderStatus();          assert.strictEqual(status.provider, "codex");          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.",          );        }).pipe(Effect.provide(failingSpawnerLayer("spawn codex ENOENT"))),      );       it.effect("returns unavailable when codex is below the minimum supported version", () =>        Effect.gen(function* () {          yield* withTempCodexHome();          const status = yield* checkCodexProviderStatus();          assert.strictEqual(status.provider, "codex");          assert.strictEqual(status.status, "error");          assert.strictEqual(status.installed, true);          assert.strictEqual(status.auth.status, "unknown");          assert.strictEqual(            status.message,            "Codex CLI v0.36.0 is too old for T3 Code. Upgrade to v0.37.0 or newer and restart T3 Code.",          );        }).pipe(          Effect.provide(            mockSpawnerLayer((args) => {              const joined = args.join(" ");              if (joined === "--version") return { stdout: "codex 0.36.0\n", stderr: "", code: 0 };              throw new Error(`Unexpected args: ${joined}`);            }),          ),        ),      );       it.effect("returns unauthenticated when auth probe reports login required", () =>        Effect.gen(function* () {          yield* withTempCodexHome();          const status = yield* checkCodexProviderStatus();          assert.strictEqual(status.provider, "codex");          assert.strictEqual(status.status, "error");          assert.strictEqual(status.installed, true);          assert.strictEqual(status.auth.status, "unauthenticated");          assert.strictEqual(            status.message,            "Codex CLI is not authenticated. Run `codex login` and try again.",          );        }).pipe(          Effect.provide(            mockSpawnerLayer((args) => {              const joined = args.join(" ");              if (joined === "--version") return { stdout: "codex 1.0.0\n", stderr: "", code: 0 };              if (joined === "login status") {                return { stdout: "", stderr: "Not logged in. Run codex login.", code: 1 };              }              throw new Error(`Unexpected args: ${joined}`);            }),          ),        ),      );       it.effect("returns unauthenticated when login status output includes 'not logged in'", () =>        Effect.gen(function* () {          yield* withTempCodexHome();          const status = yield* checkCodexProviderStatus();          assert.strictEqual(status.provider, "codex");          assert.strictEqual(status.status, "error");          assert.strictEqual(status.installed, true);          assert.strictEqual(status.auth.status, "unauthenticated");          assert.strictEqual(            status.message,            "Codex CLI is not authenticated. Run `codex login` and try again.",          );        }).pipe(          Effect.provide(            mockSpawnerLayer((args) => {              const joined = args.join(" ");              if (joined === "--version") return { stdout: "codex 1.0.0\n", stderr: "", code: 0 };              if (joined === "login status")                return { stdout: "Not logged in\n", stderr: "", code: 1 };              throw new Error(`Unexpected args: ${joined}`);            }),          ),        ),      );       it.effect("returns warning when login status command is unsupported", () =>        Effect.gen(function* () {          yield* withTempCodexHome();          const status = yield* checkCodexProviderStatus();          assert.strictEqual(status.provider, "codex");          assert.strictEqual(status.status, "warning");          assert.strictEqual(status.installed, true);          assert.strictEqual(status.auth.status, "unknown");          assert.strictEqual(            status.message,            "Codex CLI authentication status command is unavailable in this Codex version.",          );        }).pipe(          Effect.provide(            mockSpawnerLayer((args) => {              const joined = args.join(" ");              if (joined === "--version") return { stdout: "codex 1.0.0\n", stderr: "", code: 0 };              if (joined === "login status") {                return { stdout: "", stderr: "error: unknown command 'login'", code: 2 };              }              throw new Error(`Unexpected args: ${joined}`);            }),          ),        ),      );    });     describe("ProviderRegistryLive", () => {      it("treats equal provider snapshots as unchanged", () => {        const providers = [          {            provider: "codex",            status: "ready",            enabled: true,            installed: true,            auth: { status: "authenticated" },            checkedAt: "2026-03-25T00:00:00.000Z",            version: "1.0.0",            models: [],

Lines 1–500 of 1059.