From 9dc1b8153b04eaf6bfadde173fdf5d8c0243f630 Mon Sep 17 00:00:00 2001 From: BinBandit Date: Mon, 9 Mar 2026 20:44:39 +1100 Subject: [PATCH 1/4] fix(server): skip auth check when Codex CLI uses a custom model provider When the Codex CLI is configured with a custom model_provider in ~/.codex/config.toml (e.g. Portkey, Azure OpenAI proxy), authentication is handled via provider-specific environment variables rather than `codex login`. The `codex login status` probe would report 'not logged in' and t3code would treat this as a blocking error, even though the CLI works perfectly fine. This change reads the model_provider key from the Codex CLI config file at startup. When a non-OpenAI provider is detected, the auth probe is skipped and the provider health check returns ready with authStatus 'unknown' instead of erroring out. Fixes #644 --- .../provider/Layers/ProviderHealth.test.ts | 497 +++++++++++++----- .../src/provider/Layers/ProviderHealth.ts | 83 +++ 2 files changed, 455 insertions(+), 125 deletions(-) diff --git a/apps/server/src/provider/Layers/ProviderHealth.test.ts b/apps/server/src/provider/Layers/ProviderHealth.test.ts index 90df9b691f..bfbbc3d471 100644 --- a/apps/server/src/provider/Layers/ProviderHealth.test.ts +++ b/apps/server/src/provider/Layers/ProviderHealth.test.ts @@ -1,10 +1,19 @@ import assert from "node:assert/strict"; +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; import { it } from "@effect/vitest"; import { Effect, Layer, Sink, Stream } from "effect"; import * as PlatformError from "effect/PlatformError"; import { ChildProcessSpawner } from "effect/unstable/process"; +import { afterEach, beforeEach, describe } from "vitest"; -import { checkCodexProviderStatus, parseAuthStatusFromOutput } from "./ProviderHealth"; +import { + checkCodexProviderStatus, + hasCustomModelProvider, + parseAuthStatusFromOutput, + readCodexConfigModelProvider, +} from "./ProviderHealth"; // ── Test helpers ──────────────────────────────────────────────────── @@ -53,88 +62,99 @@ function failingSpawnerLayer(description: string) { ); } -// ── Tests ─────────────────────────────────────────────────────────── - -it.effect("returns ready when codex is installed and authenticated", () => - Effect.gen(function* () { - const status = yield* checkCodexProviderStatus; - assert.strictEqual(status.provider, "codex"); - assert.strictEqual(status.status, "ready"); - assert.strictEqual(status.available, true); - assert.strictEqual(status.authStatus, "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 unavailable when codex is missing", () => - Effect.gen(function* () { - const status = yield* checkCodexProviderStatus; - assert.strictEqual(status.provider, "codex"); - assert.strictEqual(status.status, "error"); - assert.strictEqual(status.available, false); - assert.strictEqual(status.authStatus, "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* () { - const status = yield* checkCodexProviderStatus; - assert.strictEqual(status.provider, "codex"); - assert.strictEqual(status.status, "error"); - assert.strictEqual(status.available, false); - assert.strictEqual(status.authStatus, "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}`); - }), +/** + * Create a temporary CODEX_HOME with an optional config.toml content. + * Returns a cleanup function that restores the original env var. + */ +function withTempCodexHome(configContent?: string): { tmpDir: string; cleanup: () => void } { + const originalCodexHome = process.env.CODEX_HOME; + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "t3-test-codex-")); + process.env.CODEX_HOME = tmpDir; + if (configContent !== undefined) { + fs.writeFileSync(path.join(tmpDir, "config.toml"), configContent); + } + return { + tmpDir, + cleanup: () => { + if (originalCodexHome !== undefined) { + process.env.CODEX_HOME = originalCodexHome; + } else { + delete process.env.CODEX_HOME; + } + fs.rmSync(tmpDir, { recursive: true, force: true }); + }, + }; +} + +// ── 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", () => { + let cleanup: () => void; + + // Point CODEX_HOME at an empty tmp dir (no config.toml) so the + // default code path (OpenAI provider, auth probe runs) is exercised. + beforeEach(() => { + ({ cleanup } = withTempCodexHome()); + }); + afterEach(() => cleanup()); + + it.effect("returns ready when codex is installed and authenticated", () => + Effect.gen(function* () { + const status = yield* checkCodexProviderStatus; + assert.strictEqual(status.provider, "codex"); + assert.strictEqual(status.status, "ready"); + assert.strictEqual(status.available, true); + assert.strictEqual(status.authStatus, "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 unauthenticated when auth probe reports login required", () => - Effect.gen(function* () { - const status = yield* checkCodexProviderStatus; - assert.strictEqual(status.provider, "codex"); - assert.strictEqual(status.status, "error"); - assert.strictEqual(status.available, true); - assert.strictEqual(status.authStatus, "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 unavailable when codex is missing", () => + Effect.gen(function* () { + const status = yield* checkCodexProviderStatus; + assert.strictEqual(status.provider, "codex"); + assert.strictEqual(status.status, "error"); + assert.strictEqual(status.available, false); + assert.strictEqual(status.authStatus, "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* () { + const status = yield* checkCodexProviderStatus; + assert.strictEqual(status.provider, "codex"); + assert.strictEqual(status.status, "error"); + assert.strictEqual(status.available, false); + assert.strictEqual(status.authStatus, "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 login status output includes 'not logged in'", - () => + it.effect("returns unauthenticated when auth probe reports login required", () => Effect.gen(function* () { const status = yield* checkCodexProviderStatus; assert.strictEqual(status.provider, "codex"); @@ -145,6 +165,137 @@ it.effect( 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* () { + const status = yield* checkCodexProviderStatus; + assert.strictEqual(status.provider, "codex"); + assert.strictEqual(status.status, "error"); + assert.strictEqual(status.available, true); + assert.strictEqual(status.authStatus, "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* () { + const status = yield* checkCodexProviderStatus; + assert.strictEqual(status.provider, "codex"); + assert.strictEqual(status.status, "warning"); + assert.strictEqual(status.available, true); + assert.strictEqual(status.authStatus, "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}`); + }), + ), + ), + ); +}); + +// ── Custom model provider: checkCodexProviderStatus integration ───── + +describe("checkCodexProviderStatus with custom model provider", () => { + let cleanup: () => void; + + beforeEach(() => { + ({ cleanup } = withTempCodexHome( + ['model_provider = "portkey"', "", "[model_providers.portkey]", 'base_url = "https://api.portkey.ai/v1"', 'env_key = "PORTKEY_API_KEY"'].join( + "\n", + ), + )); + }); + afterEach(() => cleanup()); + + it.effect( + "skips auth probe and returns ready when a custom model provider is configured", + () => + Effect.gen(function* () { + const status = yield* checkCodexProviderStatus; + assert.strictEqual(status.provider, "codex"); + assert.strictEqual(status.status, "ready"); + assert.strictEqual(status.available, true); + assert.strictEqual(status.authStatus, "unknown"); + assert.strictEqual( + status.message, + "Using a custom Codex model provider; OpenAI login check skipped.", + ); + }).pipe( + Effect.provide( + // The spawner only handles --version; if the test attempts + // "login status" the throw proves the auth probe was NOT skipped. + mockSpawnerLayer((args) => { + const joined = args.join(" "); + if (joined === "--version") return { stdout: "codex 1.0.0\n", stderr: "", code: 0 }; + throw new Error(`Auth probe should have been skipped but got args: ${joined}`); + }), + ), + ), + ); + + it.effect( + "still reports error when codex CLI is missing even with custom provider", + () => + Effect.gen(function* () { + const status = yield* checkCodexProviderStatus; + assert.strictEqual(status.status, "error"); + assert.strictEqual(status.available, false); + }).pipe(Effect.provide(failingSpawnerLayer("spawn codex ENOENT"))), + ); +}); + +describe("checkCodexProviderStatus with openai model provider", () => { + let cleanup: () => void; + + beforeEach(() => { + ({ cleanup } = withTempCodexHome('model_provider = "openai"\n')); + }); + afterEach(() => cleanup()); + + it.effect("still runs auth probe when model_provider is openai", () => + Effect.gen(function* () { + const status = yield* checkCodexProviderStatus; + // The auth probe runs and sees "not logged in" → error + assert.strictEqual(status.status, "error"); + assert.strictEqual(status.authStatus, "unauthenticated"); }).pipe( Effect.provide( mockSpawnerLayer((args) => { @@ -156,57 +307,153 @@ it.effect( }), ), ), -); - -it.effect("returns warning when login status command is unsupported", () => - Effect.gen(function* () { - const status = yield* checkCodexProviderStatus; - assert.strictEqual(status.provider, "codex"); - assert.strictEqual(status.status, "warning"); - assert.strictEqual(status.available, true); - assert.strictEqual(status.authStatus, "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}`); - }), - ), - ), -); + ); +}); + +// ── parseAuthStatusFromOutput pure tests ──────────────────────────── + +describe("parseAuthStatusFromOutput", () => { + it("exit code 0 with no auth markers is ready", () => { + const parsed = parseAuthStatusFromOutput({ stdout: "OK\n", stderr: "", code: 0 }); + assert.strictEqual(parsed.status, "ready"); + assert.strictEqual(parsed.authStatus, "authenticated"); + }); -// ── Pure function tests ───────────────────────────────────────────── + it("JSON with authenticated=false is unauthenticated", () => { + const parsed = parseAuthStatusFromOutput({ + stdout: '[{"authenticated":false}]\n', + stderr: "", + code: 0, + }); + assert.strictEqual(parsed.status, "error"); + assert.strictEqual(parsed.authStatus, "unauthenticated"); + }); -it("parseAuthStatusFromOutput: exit code 0 with no auth markers is ready", () => { - const parsed = parseAuthStatusFromOutput({ stdout: "OK\n", stderr: "", code: 0 }); - assert.strictEqual(parsed.status, "ready"); - assert.strictEqual(parsed.authStatus, "authenticated"); + it("JSON without auth marker is warning", () => { + const parsed = parseAuthStatusFromOutput({ + stdout: '[{"ok":true}]\n', + stderr: "", + code: 0, + }); + assert.strictEqual(parsed.status, "warning"); + assert.strictEqual(parsed.authStatus, "unknown"); + }); }); -it("parseAuthStatusFromOutput: JSON with authenticated=false is unauthenticated", () => { - const parsed = parseAuthStatusFromOutput({ - stdout: '[{"authenticated":false}]\n', - stderr: "", - code: 0, +// ── readCodexConfigModelProvider tests ─────────────────────────────── + +describe("readCodexConfigModelProvider", () => { + let cleanup: () => void; + let tmpDir: string; + + beforeEach(() => { + ({ tmpDir, cleanup } = withTempCodexHome()); + }); + afterEach(() => cleanup()); + + it("returns undefined when config file does not exist", () => { + assert.strictEqual(readCodexConfigModelProvider(), undefined); + }); + + it("returns undefined when config has no model_provider key", () => { + fs.writeFileSync(path.join(tmpDir, "config.toml"), 'model = "gpt-5-codex"\n'); + assert.strictEqual(readCodexConfigModelProvider(), undefined); + }); + + it("returns the provider when model_provider is set at top level", () => { + fs.writeFileSync( + path.join(tmpDir, "config.toml"), + 'model = "gpt-5-codex"\nmodel_provider = "portkey"\n', + ); + assert.strictEqual(readCodexConfigModelProvider(), "portkey"); + }); + + it("returns openai when model_provider is openai", () => { + fs.writeFileSync(path.join(tmpDir, "config.toml"), 'model_provider = "openai"\n'); + assert.strictEqual(readCodexConfigModelProvider(), "openai"); + }); + + it("ignores model_provider inside section headers", () => { + fs.writeFileSync( + path.join(tmpDir, "config.toml"), + [ + 'model = "gpt-5-codex"', + "", + "[model_providers.portkey]", + 'base_url = "https://api.portkey.ai/v1"', + 'model_provider = "should-be-ignored"', + "", + ].join("\n"), + ); + assert.strictEqual(readCodexConfigModelProvider(), undefined); + }); + + it("handles comments and whitespace", () => { + fs.writeFileSync( + path.join(tmpDir, "config.toml"), + [ + "# This is a comment", + "", + ' model_provider = "azure" ', + "", + "[profiles.deep-review]", + 'model = "gpt-5-pro"', + ].join("\n"), + ); + assert.strictEqual(readCodexConfigModelProvider(), "azure"); + }); + + it("handles single-quoted values in TOML", () => { + fs.writeFileSync( + path.join(tmpDir, "config.toml"), + "model_provider = 'mistral'\n", + ); + assert.strictEqual(readCodexConfigModelProvider(), "mistral"); }); - assert.strictEqual(parsed.status, "error"); - assert.strictEqual(parsed.authStatus, "unauthenticated"); }); -it("parseAuthStatusFromOutput: JSON without auth marker is warning", () => { - const parsed = parseAuthStatusFromOutput({ - stdout: '[{"ok":true}]\n', - stderr: "", - code: 0, +// ── hasCustomModelProvider tests ───────────────────────────────────── + +describe("hasCustomModelProvider", () => { + let cleanup: () => void; + let tmpDir: string; + + beforeEach(() => { + ({ tmpDir, cleanup } = withTempCodexHome()); + }); + afterEach(() => cleanup()); + + it("returns false when no config file exists", () => { + assert.strictEqual(hasCustomModelProvider(), false); + }); + + it("returns false when model_provider is not set", () => { + fs.writeFileSync(path.join(tmpDir, "config.toml"), 'model = "gpt-5-codex"\n'); + assert.strictEqual(hasCustomModelProvider(), false); + }); + + it("returns false when model_provider is openai", () => { + fs.writeFileSync(path.join(tmpDir, "config.toml"), 'model_provider = "openai"\n'); + assert.strictEqual(hasCustomModelProvider(), false); + }); + + it("returns true when model_provider is portkey", () => { + fs.writeFileSync(path.join(tmpDir, "config.toml"), 'model_provider = "portkey"\n'); + assert.strictEqual(hasCustomModelProvider(), true); + }); + + it("returns true when model_provider is azure", () => { + fs.writeFileSync(path.join(tmpDir, "config.toml"), 'model_provider = "azure"\n'); + assert.strictEqual(hasCustomModelProvider(), true); + }); + + it("returns true when model_provider is ollama", () => { + fs.writeFileSync(path.join(tmpDir, "config.toml"), 'model_provider = "ollama"\n'); + assert.strictEqual(hasCustomModelProvider(), true); + }); + + it("returns true when model_provider is a custom proxy", () => { + fs.writeFileSync(path.join(tmpDir, "config.toml"), 'model_provider = "my-company-proxy"\n'); + assert.strictEqual(hasCustomModelProvider(), true); }); - assert.strictEqual(parsed.status, "warning"); - assert.strictEqual(parsed.authStatus, "unknown"); }); diff --git a/apps/server/src/provider/Layers/ProviderHealth.ts b/apps/server/src/provider/Layers/ProviderHealth.ts index 59f41edf81..bd90108acb 100644 --- a/apps/server/src/provider/Layers/ProviderHealth.ts +++ b/apps/server/src/provider/Layers/ProviderHealth.ts @@ -8,6 +8,9 @@ * * @module ProviderHealthLive */ +import * as fs from "node:fs"; +import * as OS from "node:os"; +import * as NodePath from "node:path"; import type { ServerProviderAuthStatus, ServerProviderStatus, @@ -167,6 +170,70 @@ export function parseAuthStatusFromOutput(result: CommandResult): { }; } +// ── Codex CLI config detection ────────────────────────────────────── + +/** + * Providers that use OpenAI-native authentication via `codex login`. + * When the configured `model_provider` is one of these, the `codex login + * status` probe still runs. For any other provider value the auth probe + * is skipped because authentication is handled externally (e.g. via + * environment variables like `PORTKEY_API_KEY` or `AZURE_API_KEY`). + */ +const OPENAI_AUTH_PROVIDERS = new Set(["openai"]); + +/** + * Read the `model_provider` value from the Codex CLI config file. + * + * Looks for the file at `$CODEX_HOME/config.toml` (falls back to + * `~/.codex/config.toml`). Uses a simple line-by-line scan rather than + * a full TOML parser to avoid adding a dependency for a single key. + * + * Returns `undefined` when the file does not exist or does not set + * `model_provider`. + */ +export function readCodexConfigModelProvider(): string | undefined { + const codexHome = process.env.CODEX_HOME || NodePath.join(OS.homedir(), ".codex"); + const configPath = NodePath.join(codexHome, "config.toml"); + + let content: string; + try { + content = fs.readFileSync(configPath, "utf8"); + } catch { + return undefined; + } + + // We need to find `model_provider = "..."` at the top level of the + // TOML file (i.e. before any `[section]` header). Lines inside + // `[profiles.*]`, `[model_providers.*]`, etc. are ignored. + let inTopLevel = true; + for (const line of content.split("\n")) { + const trimmed = line.trim(); + // Skip comments and empty lines. + if (!trimmed || trimmed.startsWith("#")) continue; + // Detect section headers — once we leave the top level, stop. + if (trimmed.startsWith("[")) { + inTopLevel = false; + continue; + } + if (!inTopLevel) continue; + + const match = trimmed.match(/^model_provider\s*=\s*["']([^"']+)["']/); + if (match) return match[1]; + } + return undefined; +} + +/** + * Returns `true` when the Codex CLI is configured with a custom + * (non-OpenAI) model provider, meaning `codex login` auth is not + * required because authentication is handled through provider-specific + * environment variables. + */ +export function hasCustomModelProvider(): boolean { + const provider = readCodexConfigModelProvider(); + return provider !== undefined && !OPENAI_AUTH_PROVIDERS.has(provider); +} + // ── Effect-native command execution ───────────────────────────────── const collectStreamAsString = (stream: Stream.Stream): Effect.Effect => @@ -265,6 +332,22 @@ export const checkCodexProviderStatus: Effect.Effect< } // Probe 2: `codex login status` — is the user authenticated? + // + // Custom model providers (e.g. Portkey, Azure OpenAI proxy) handle + // authentication through their own environment variables, so `codex + // login status` will report "not logged in" even when the CLI works + // fine. Skip the auth probe entirely for non-OpenAI providers. + if (hasCustomModelProvider()) { + return { + provider: CODEX_PROVIDER, + status: "ready" as const, + available: true, + authStatus: "unknown" as const, + checkedAt, + message: "Using a custom Codex model provider; OpenAI login check skipped.", + } satisfies ServerProviderStatus; + } + const authProbe = yield* runCodexCommand(["login", "status"]).pipe( Effect.timeoutOption(DEFAULT_TIMEOUT_MS), Effect.result, From d874deac2ca5e479d0f7ba41fbd4ceab9af79d43 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 10 Mar 2026 09:53:51 -0700 Subject: [PATCH 2/4] fmt --- .../provider/Layers/ProviderHealth.test.ts | 66 ++++++++----------- 1 file changed, 29 insertions(+), 37 deletions(-) diff --git a/apps/server/src/provider/Layers/ProviderHealth.test.ts b/apps/server/src/provider/Layers/ProviderHealth.test.ts index 7fafefb6b1..1ba2ec78ad 100644 --- a/apps/server/src/provider/Layers/ProviderHealth.test.ts +++ b/apps/server/src/provider/Layers/ProviderHealth.test.ts @@ -246,40 +246,36 @@ describe("checkCodexProviderStatus with custom model provider", () => { }); afterEach(() => cleanup()); - it.effect( - "skips auth probe and returns ready when a custom model provider is configured", - () => - Effect.gen(function* () { - const status = yield* checkCodexProviderStatus; - assert.strictEqual(status.provider, "codex"); - assert.strictEqual(status.status, "ready"); - assert.strictEqual(status.available, true); - assert.strictEqual(status.authStatus, "unknown"); - assert.strictEqual( - status.message, - "Using a custom Codex model provider; OpenAI login check skipped.", - ); - }).pipe( - Effect.provide( - // The spawner only handles --version; if the test attempts - // "login status" the throw proves the auth probe was NOT skipped. - mockSpawnerLayer((args) => { - const joined = args.join(" "); - if (joined === "--version") return { stdout: "codex 1.0.0\n", stderr: "", code: 0 }; - throw new Error(`Auth probe should have been skipped but got args: ${joined}`); - }), - ), + it.effect("skips auth probe and returns ready when a custom model provider is configured", () => + Effect.gen(function* () { + const status = yield* checkCodexProviderStatus; + assert.strictEqual(status.provider, "codex"); + assert.strictEqual(status.status, "ready"); + assert.strictEqual(status.available, true); + assert.strictEqual(status.authStatus, "unknown"); + assert.strictEqual( + status.message, + "Using a custom Codex model provider; OpenAI login check skipped.", + ); + }).pipe( + Effect.provide( + // The spawner only handles --version; if the test attempts + // "login status" the throw proves the auth probe was NOT skipped. + mockSpawnerLayer((args) => { + const joined = args.join(" "); + if (joined === "--version") return { stdout: "codex 1.0.0\n", stderr: "", code: 0 }; + throw new Error(`Auth probe should have been skipped but got args: ${joined}`); + }), ), + ), ); - it.effect( - "still reports error when codex CLI is missing even with custom provider", - () => - Effect.gen(function* () { - const status = yield* checkCodexProviderStatus; - assert.strictEqual(status.status, "error"); - assert.strictEqual(status.available, false); - }).pipe(Effect.provide(failingSpawnerLayer("spawn codex ENOENT"))), + it.effect("still reports error when codex CLI is missing even with custom provider", () => + Effect.gen(function* () { + const status = yield* checkCodexProviderStatus; + assert.strictEqual(status.status, "error"); + assert.strictEqual(status.available, false); + }).pipe(Effect.provide(failingSpawnerLayer("spawn codex ENOENT"))), ); }); @@ -302,8 +298,7 @@ describe("checkCodexProviderStatus with openai model provider", () => { 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 }; + if (joined === "login status") return { stdout: "Not logged in\n", stderr: "", code: 1 }; throw new Error(`Unexpected args: ${joined}`); }), ), @@ -405,10 +400,7 @@ describe("readCodexConfigModelProvider", () => { }); it("handles single-quoted values in TOML", () => { - fs.writeFileSync( - path.join(tmpDir, "config.toml"), - "model_provider = 'mistral'\n", - ); + fs.writeFileSync(path.join(tmpDir, "config.toml"), "model_provider = 'mistral'\n"); assert.strictEqual(readCodexConfigModelProvider(), "mistral"); }); }); From 119dce9942aebd3073a0be9ead48434d3c8a7961 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 10 Mar 2026 10:10:26 -0700 Subject: [PATCH 3/4] Use Effect FileSystem for Codex provider config checks - make `readCodexConfigModelProvider` and `hasCustomModelProvider` Effect-based - skip `codex login status` via Effect flow when custom model providers are configured - refactor ProviderHealth tests to use scoped Effect Node services and temp `CODEX_HOME` --- .../provider/Layers/ProviderHealth.test.ts | 706 +++++++++--------- .../src/provider/Layers/ProviderHealth.ts | 36 +- 2 files changed, 379 insertions(+), 363 deletions(-) diff --git a/apps/server/src/provider/Layers/ProviderHealth.test.ts b/apps/server/src/provider/Layers/ProviderHealth.test.ts index 1ba2ec78ad..8d566aaa9d 100644 --- a/apps/server/src/provider/Layers/ProviderHealth.test.ts +++ b/apps/server/src/provider/Layers/ProviderHealth.test.ts @@ -1,12 +1,9 @@ import assert from "node:assert/strict"; -import * as fs from "node:fs"; -import * as os from "node:os"; -import * as path from "node:path"; -import { it } from "@effect/vitest"; -import { Effect, Layer, Sink, Stream } from "effect"; +import * as NodeServices from "@effect/platform-node/NodeServices"; +import { describe, it } from "@effect/vitest"; +import { Effect, FileSystem, Layer, Path, Sink, Stream } from "effect"; import * as PlatformError from "effect/PlatformError"; import { ChildProcessSpawner } from "effect/unstable/process"; -import { afterEach, beforeEach, describe } from "vitest"; import { checkCodexProviderStatus, @@ -63,390 +60,409 @@ function failingSpawnerLayer(description: string) { } /** - * Create a temporary CODEX_HOME with an optional config.toml content. - * Returns a cleanup function that restores the original env var. + * 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): { tmpDir: string; cleanup: () => void } { - const originalCodexHome = process.env.CODEX_HOME; - const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "t3-test-codex-")); - process.env.CODEX_HOME = tmpDir; - if (configContent !== undefined) { - fs.writeFileSync(path.join(tmpDir, "config.toml"), configContent); - } - return { - tmpDir, - cleanup: () => { - if (originalCodexHome !== undefined) { - process.env.CODEX_HOME = originalCodexHome; - } else { - delete process.env.CODEX_HOME; - } - fs.rmSync(tmpDir, { recursive: true, force: true }); - }, - }; -} - -// ── 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. +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; + } + }), + ); -describe("checkCodexProviderStatus", () => { - let cleanup: () => void; + if (configContent !== undefined) { + yield* fileSystem.writeFileString(path.join(tmpDir, "config.toml"), configContent); + } - // Point CODEX_HOME at an empty tmp dir (no config.toml) so the - // default code path (OpenAI provider, auth probe runs) is exercised. - beforeEach(() => { - ({ cleanup } = withTempCodexHome()); + return { tmpDir } as const; }); - afterEach(() => cleanup()); - - it.effect("returns ready when codex is installed and authenticated", () => - Effect.gen(function* () { - const status = yield* checkCodexProviderStatus; - assert.strictEqual(status.provider, "codex"); - assert.strictEqual(status.status, "ready"); - assert.strictEqual(status.available, true); - assert.strictEqual(status.authStatus, "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.layer(NodeServices.layer)("ProviderHealth", (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.available, true); + assert.strictEqual(status.authStatus, "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 unavailable when codex is missing", () => - Effect.gen(function* () { - const status = yield* checkCodexProviderStatus; - assert.strictEqual(status.provider, "codex"); - assert.strictEqual(status.status, "error"); - assert.strictEqual(status.available, false); - assert.strictEqual(status.authStatus, "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 missing", () => + Effect.gen(function* () { + yield* withTempCodexHome(); + const status = yield* checkCodexProviderStatus; + assert.strictEqual(status.provider, "codex"); + assert.strictEqual(status.status, "error"); + assert.strictEqual(status.available, false); + assert.strictEqual(status.authStatus, "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* () { - const status = yield* checkCodexProviderStatus; - assert.strictEqual(status.provider, "codex"); - assert.strictEqual(status.status, "error"); - assert.strictEqual(status.available, false); - assert.strictEqual(status.authStatus, "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 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.available, false); + assert.strictEqual(status.authStatus, "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* () { - const status = yield* checkCodexProviderStatus; - assert.strictEqual(status.provider, "codex"); - assert.strictEqual(status.status, "error"); - assert.strictEqual(status.available, true); - assert.strictEqual(status.authStatus, "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 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.available, true); + assert.strictEqual(status.authStatus, "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* () { - const status = yield* checkCodexProviderStatus; - assert.strictEqual(status.provider, "codex"); - assert.strictEqual(status.status, "error"); - assert.strictEqual(status.available, true); - assert.strictEqual(status.authStatus, "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 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.available, true); + assert.strictEqual(status.authStatus, "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* () { - const status = yield* checkCodexProviderStatus; - assert.strictEqual(status.provider, "codex"); - assert.strictEqual(status.status, "warning"); - assert.strictEqual(status.available, true); - assert.strictEqual(status.authStatus, "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}`); - }), + 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.available, true); + assert.strictEqual(status.authStatus, "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}`); + }), + ), ), - ), - ); -}); - -// ── Custom model provider: checkCodexProviderStatus integration ───── - -describe("checkCodexProviderStatus with custom model provider", () => { - let cleanup: () => void; - - beforeEach(() => { - ({ cleanup } = withTempCodexHome( - [ - 'model_provider = "portkey"', - "", - "[model_providers.portkey]", - 'base_url = "https://api.portkey.ai/v1"', - 'env_key = "PORTKEY_API_KEY"', - ].join("\n"), - )); + ); }); - afterEach(() => cleanup()); - - it.effect("skips auth probe and returns ready when a custom model provider is configured", () => - Effect.gen(function* () { - const status = yield* checkCodexProviderStatus; - assert.strictEqual(status.provider, "codex"); - assert.strictEqual(status.status, "ready"); - assert.strictEqual(status.available, true); - assert.strictEqual(status.authStatus, "unknown"); - assert.strictEqual( - status.message, - "Using a custom Codex model provider; OpenAI login check skipped.", - ); - }).pipe( - Effect.provide( - // The spawner only handles --version; if the test attempts - // "login status" the throw proves the auth probe was NOT skipped. - mockSpawnerLayer((args) => { - const joined = args.join(" "); - if (joined === "--version") return { stdout: "codex 1.0.0\n", stderr: "", code: 0 }; - throw new Error(`Auth probe should have been skipped but got args: ${joined}`); - }), - ), - ), - ); - it.effect("still reports error when codex CLI is missing even with custom provider", () => - Effect.gen(function* () { - const status = yield* checkCodexProviderStatus; - assert.strictEqual(status.status, "error"); - assert.strictEqual(status.available, false); - }).pipe(Effect.provide(failingSpawnerLayer("spawn codex ENOENT"))), - ); -}); - -describe("checkCodexProviderStatus with openai model provider", () => { - let cleanup: () => void; + // ── Custom model provider: checkCodexProviderStatus integration ─── + + describe("checkCodexProviderStatus with custom model provider", () => { + it.effect("skips auth probe and returns ready when a custom model provider is configured", () => + Effect.gen(function* () { + yield* withTempCodexHome( + [ + 'model_provider = "portkey"', + "", + "[model_providers.portkey]", + 'base_url = "https://api.portkey.ai/v1"', + 'env_key = "PORTKEY_API_KEY"', + ].join("\n"), + ); + const status = yield* checkCodexProviderStatus; + assert.strictEqual(status.provider, "codex"); + assert.strictEqual(status.status, "ready"); + assert.strictEqual(status.available, true); + assert.strictEqual(status.authStatus, "unknown"); + assert.strictEqual( + status.message, + "Using a custom Codex model provider; OpenAI login check skipped.", + ); + }).pipe( + Effect.provide( + // The spawner only handles --version; if the test attempts + // "login status" the throw proves the auth probe was NOT skipped. + mockSpawnerLayer((args) => { + const joined = args.join(" "); + if (joined === "--version") return { stdout: "codex 1.0.0\n", stderr: "", code: 0 }; + throw new Error(`Auth probe should have been skipped but got args: ${joined}`); + }), + ), + ), + ); - beforeEach(() => { - ({ cleanup } = withTempCodexHome('model_provider = "openai"\n')); + it.effect("still reports error when codex CLI is missing even with custom provider", () => + Effect.gen(function* () { + yield* withTempCodexHome( + [ + 'model_provider = "portkey"', + "", + "[model_providers.portkey]", + 'base_url = "https://api.portkey.ai/v1"', + 'env_key = "PORTKEY_API_KEY"', + ].join("\n"), + ); + const status = yield* checkCodexProviderStatus; + assert.strictEqual(status.status, "error"); + assert.strictEqual(status.available, false); + }).pipe(Effect.provide(failingSpawnerLayer("spawn codex ENOENT"))), + ); }); - afterEach(() => cleanup()); - - it.effect("still runs auth probe when model_provider is openai", () => - Effect.gen(function* () { - const status = yield* checkCodexProviderStatus; - // The auth probe runs and sees "not logged in" → error - assert.strictEqual(status.status, "error"); - assert.strictEqual(status.authStatus, "unauthenticated"); - }).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}`); - }), + + describe("checkCodexProviderStatus with openai model provider", () => { + it.effect("still runs auth probe when model_provider is openai", () => + Effect.gen(function* () { + yield* withTempCodexHome('model_provider = "openai"\n'); + const status = yield* checkCodexProviderStatus; + // The auth probe runs and sees "not logged in" → error + assert.strictEqual(status.status, "error"); + assert.strictEqual(status.authStatus, "unauthenticated"); + }).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}`); + }), + ), ), - ), - ); -}); + ); + }); -// ── parseAuthStatusFromOutput pure tests ──────────────────────────── + // ── parseAuthStatusFromOutput pure tests ────────────────────────── -describe("parseAuthStatusFromOutput", () => { - it("exit code 0 with no auth markers is ready", () => { - const parsed = parseAuthStatusFromOutput({ stdout: "OK\n", stderr: "", code: 0 }); - assert.strictEqual(parsed.status, "ready"); - assert.strictEqual(parsed.authStatus, "authenticated"); - }); + describe("parseAuthStatusFromOutput", () => { + it("exit code 0 with no auth markers is ready", () => { + const parsed = parseAuthStatusFromOutput({ stdout: "OK\n", stderr: "", code: 0 }); + assert.strictEqual(parsed.status, "ready"); + assert.strictEqual(parsed.authStatus, "authenticated"); + }); - it("JSON with authenticated=false is unauthenticated", () => { - const parsed = parseAuthStatusFromOutput({ - stdout: '[{"authenticated":false}]\n', - stderr: "", - code: 0, + it("JSON with authenticated=false is unauthenticated", () => { + const parsed = parseAuthStatusFromOutput({ + stdout: '[{"authenticated":false}]\n', + stderr: "", + code: 0, + }); + assert.strictEqual(parsed.status, "error"); + assert.strictEqual(parsed.authStatus, "unauthenticated"); }); - assert.strictEqual(parsed.status, "error"); - assert.strictEqual(parsed.authStatus, "unauthenticated"); - }); - it("JSON without auth marker is warning", () => { - const parsed = parseAuthStatusFromOutput({ - stdout: '[{"ok":true}]\n', - stderr: "", - code: 0, + it("JSON without auth marker is warning", () => { + const parsed = parseAuthStatusFromOutput({ + stdout: '[{"ok":true}]\n', + stderr: "", + code: 0, + }); + assert.strictEqual(parsed.status, "warning"); + assert.strictEqual(parsed.authStatus, "unknown"); }); - assert.strictEqual(parsed.status, "warning"); - assert.strictEqual(parsed.authStatus, "unknown"); }); -}); - -// ── readCodexConfigModelProvider tests ─────────────────────────────── -describe("readCodexConfigModelProvider", () => { - let cleanup: () => void; - let tmpDir: string; + // ── readCodexConfigModelProvider tests ───────────────────────────── - beforeEach(() => { - ({ tmpDir, cleanup } = withTempCodexHome()); - }); - afterEach(() => cleanup()); - - it("returns undefined when config file does not exist", () => { - assert.strictEqual(readCodexConfigModelProvider(), undefined); - }); + describe("readCodexConfigModelProvider", () => { + it.effect("returns undefined when config file does not exist", () => + Effect.gen(function* () { + yield* withTempCodexHome(); + assert.strictEqual(yield* readCodexConfigModelProvider, undefined); + }), + ); - it("returns undefined when config has no model_provider key", () => { - fs.writeFileSync(path.join(tmpDir, "config.toml"), 'model = "gpt-5-codex"\n'); - assert.strictEqual(readCodexConfigModelProvider(), undefined); - }); + it.effect("returns undefined when config has no model_provider key", () => + Effect.gen(function* () { + yield* withTempCodexHome('model = "gpt-5-codex"\n'); + assert.strictEqual(yield* readCodexConfigModelProvider, undefined); + }), + ); - it("returns the provider when model_provider is set at top level", () => { - fs.writeFileSync( - path.join(tmpDir, "config.toml"), - 'model = "gpt-5-codex"\nmodel_provider = "portkey"\n', + it.effect("returns the provider when model_provider is set at top level", () => + Effect.gen(function* () { + yield* withTempCodexHome('model = "gpt-5-codex"\nmodel_provider = "portkey"\n'); + assert.strictEqual(yield* readCodexConfigModelProvider, "portkey"); + }), ); - assert.strictEqual(readCodexConfigModelProvider(), "portkey"); - }); - it("returns openai when model_provider is openai", () => { - fs.writeFileSync(path.join(tmpDir, "config.toml"), 'model_provider = "openai"\n'); - assert.strictEqual(readCodexConfigModelProvider(), "openai"); - }); + it.effect("returns openai when model_provider is openai", () => + Effect.gen(function* () { + yield* withTempCodexHome('model_provider = "openai"\n'); + assert.strictEqual(yield* readCodexConfigModelProvider, "openai"); + }), + ); - it("ignores model_provider inside section headers", () => { - fs.writeFileSync( - path.join(tmpDir, "config.toml"), - [ - 'model = "gpt-5-codex"', - "", - "[model_providers.portkey]", - 'base_url = "https://api.portkey.ai/v1"', - 'model_provider = "should-be-ignored"', - "", - ].join("\n"), + it.effect("ignores model_provider inside section headers", () => + Effect.gen(function* () { + yield* withTempCodexHome( + [ + 'model = "gpt-5-codex"', + "", + "[model_providers.portkey]", + 'base_url = "https://api.portkey.ai/v1"', + 'model_provider = "should-be-ignored"', + "", + ].join("\n"), + ); + assert.strictEqual(yield* readCodexConfigModelProvider, undefined); + }), ); - assert.strictEqual(readCodexConfigModelProvider(), undefined); - }); - it("handles comments and whitespace", () => { - fs.writeFileSync( - path.join(tmpDir, "config.toml"), - [ - "# This is a comment", - "", - ' model_provider = "azure" ', - "", - "[profiles.deep-review]", - 'model = "gpt-5-pro"', - ].join("\n"), + it.effect("handles comments and whitespace", () => + Effect.gen(function* () { + yield* withTempCodexHome( + [ + "# This is a comment", + "", + ' model_provider = "azure" ', + "", + "[profiles.deep-review]", + 'model = "gpt-5-pro"', + ].join("\n"), + ); + assert.strictEqual(yield* readCodexConfigModelProvider, "azure"); + }), ); - assert.strictEqual(readCodexConfigModelProvider(), "azure"); - }); - it("handles single-quoted values in TOML", () => { - fs.writeFileSync(path.join(tmpDir, "config.toml"), "model_provider = 'mistral'\n"); - assert.strictEqual(readCodexConfigModelProvider(), "mistral"); + it.effect("handles single-quoted values in TOML", () => + Effect.gen(function* () { + yield* withTempCodexHome("model_provider = 'mistral'\n"); + assert.strictEqual(yield* readCodexConfigModelProvider, "mistral"); + }), + ); }); -}); - -// ── hasCustomModelProvider tests ───────────────────────────────────── -describe("hasCustomModelProvider", () => { - let cleanup: () => void; - let tmpDir: string; + // ── hasCustomModelProvider tests ─────────────────────────────────── - beforeEach(() => { - ({ tmpDir, cleanup } = withTempCodexHome()); - }); - afterEach(() => cleanup()); - - it("returns false when no config file exists", () => { - assert.strictEqual(hasCustomModelProvider(), false); - }); + describe("hasCustomModelProvider", () => { + it.effect("returns false when no config file exists", () => + Effect.gen(function* () { + yield* withTempCodexHome(); + assert.strictEqual(yield* hasCustomModelProvider, false); + }), + ); - it("returns false when model_provider is not set", () => { - fs.writeFileSync(path.join(tmpDir, "config.toml"), 'model = "gpt-5-codex"\n'); - assert.strictEqual(hasCustomModelProvider(), false); - }); + it.effect("returns false when model_provider is not set", () => + Effect.gen(function* () { + yield* withTempCodexHome('model = "gpt-5-codex"\n'); + assert.strictEqual(yield* hasCustomModelProvider, false); + }), + ); - it("returns false when model_provider is openai", () => { - fs.writeFileSync(path.join(tmpDir, "config.toml"), 'model_provider = "openai"\n'); - assert.strictEqual(hasCustomModelProvider(), false); - }); + it.effect("returns false when model_provider is openai", () => + Effect.gen(function* () { + yield* withTempCodexHome('model_provider = "openai"\n'); + assert.strictEqual(yield* hasCustomModelProvider, false); + }), + ); - it("returns true when model_provider is portkey", () => { - fs.writeFileSync(path.join(tmpDir, "config.toml"), 'model_provider = "portkey"\n'); - assert.strictEqual(hasCustomModelProvider(), true); - }); + it.effect("returns true when model_provider is portkey", () => + Effect.gen(function* () { + yield* withTempCodexHome('model_provider = "portkey"\n'); + assert.strictEqual(yield* hasCustomModelProvider, true); + }), + ); - it("returns true when model_provider is azure", () => { - fs.writeFileSync(path.join(tmpDir, "config.toml"), 'model_provider = "azure"\n'); - assert.strictEqual(hasCustomModelProvider(), true); - }); + it.effect("returns true when model_provider is azure", () => + Effect.gen(function* () { + yield* withTempCodexHome('model_provider = "azure"\n'); + assert.strictEqual(yield* hasCustomModelProvider, true); + }), + ); - it("returns true when model_provider is ollama", () => { - fs.writeFileSync(path.join(tmpDir, "config.toml"), 'model_provider = "ollama"\n'); - assert.strictEqual(hasCustomModelProvider(), true); - }); + it.effect("returns true when model_provider is ollama", () => + Effect.gen(function* () { + yield* withTempCodexHome('model_provider = "ollama"\n'); + assert.strictEqual(yield* hasCustomModelProvider, true); + }), + ); - it("returns true when model_provider is a custom proxy", () => { - fs.writeFileSync(path.join(tmpDir, "config.toml"), 'model_provider = "my-company-proxy"\n'); - assert.strictEqual(hasCustomModelProvider(), true); + it.effect("returns true when model_provider is a custom proxy", () => + Effect.gen(function* () { + yield* withTempCodexHome('model_provider = "my-company-proxy"\n'); + assert.strictEqual(yield* hasCustomModelProvider, true); + }), + ); }); }); diff --git a/apps/server/src/provider/Layers/ProviderHealth.ts b/apps/server/src/provider/Layers/ProviderHealth.ts index 98168e1f3c..1fed0597a2 100644 --- a/apps/server/src/provider/Layers/ProviderHealth.ts +++ b/apps/server/src/provider/Layers/ProviderHealth.ts @@ -8,15 +8,13 @@ * * @module ProviderHealthLive */ -import * as fs from "node:fs"; import * as OS from "node:os"; -import * as NodePath from "node:path"; import type { ServerProviderAuthStatus, ServerProviderStatus, ServerProviderStatusState, } from "@t3tools/contracts"; -import { Array, Effect, Fiber, Layer, Option, Result, Stream } from "effect"; +import { Array, Effect, Fiber, FileSystem, Layer, Option, Path, Result, Stream } from "effect"; import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; import { @@ -191,14 +189,16 @@ const OPENAI_AUTH_PROVIDERS = new Set(["openai"]); * Returns `undefined` when the file does not exist or does not set * `model_provider`. */ -export function readCodexConfigModelProvider(): string | undefined { - const codexHome = process.env.CODEX_HOME || NodePath.join(OS.homedir(), ".codex"); - const configPath = NodePath.join(codexHome, "config.toml"); - - let content: string; - try { - content = fs.readFileSync(configPath, "utf8"); - } catch { +export const readCodexConfigModelProvider = Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const codexHome = process.env.CODEX_HOME || path.join(OS.homedir(), ".codex"); + const configPath = path.join(codexHome, "config.toml"); + + const content = yield* fileSystem + .readFileString(configPath) + .pipe(Effect.orElseSucceed(() => undefined)); + if (content === undefined) { return undefined; } @@ -221,7 +221,7 @@ export function readCodexConfigModelProvider(): string | undefined { if (match) return match[1]; } return undefined; -} +}); /** * Returns `true` when the Codex CLI is configured with a custom @@ -229,10 +229,10 @@ export function readCodexConfigModelProvider(): string | undefined { * required because authentication is handled through provider-specific * environment variables. */ -export function hasCustomModelProvider(): boolean { - const provider = readCodexConfigModelProvider(); - return provider !== undefined && !OPENAI_AUTH_PROVIDERS.has(provider); -} +export const hasCustomModelProvider = Effect.map( + readCodexConfigModelProvider, + (provider) => provider !== undefined && !OPENAI_AUTH_PROVIDERS.has(provider), +); // ── Effect-native command execution ───────────────────────────────── @@ -269,7 +269,7 @@ const runCodexCommand = (args: ReadonlyArray) => export const checkCodexProviderStatus: Effect.Effect< ServerProviderStatus, never, - ChildProcessSpawner.ChildProcessSpawner + ChildProcessSpawner.ChildProcessSpawner | FileSystem.FileSystem | Path.Path > = Effect.gen(function* () { const checkedAt = new Date().toISOString(); @@ -337,7 +337,7 @@ export const checkCodexProviderStatus: Effect.Effect< // authentication through their own environment variables, so `codex // login status` will report "not logged in" even when the CLI works // fine. Skip the auth probe entirely for non-OpenAI providers. - if (hasCustomModelProvider()) { + if (yield* hasCustomModelProvider) { return { provider: CODEX_PROVIDER, status: "ready" as const, From 70bcd06c0ad8ebb48eafb4d7218c9da02de3d378 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 10 Mar 2026 10:11:09 -0700 Subject: [PATCH 4/4] Use @effect/vitest assert in ProviderHealth test - Replace Node assert import with `assert` from `@effect/vitest` - Keep test assertions aligned with the Effect Vitest test stack --- apps/server/src/provider/Layers/ProviderHealth.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/server/src/provider/Layers/ProviderHealth.test.ts b/apps/server/src/provider/Layers/ProviderHealth.test.ts index 8d566aaa9d..10bd12a7cf 100644 --- a/apps/server/src/provider/Layers/ProviderHealth.test.ts +++ b/apps/server/src/provider/Layers/ProviderHealth.test.ts @@ -1,6 +1,5 @@ -import assert from "node:assert/strict"; import * as NodeServices from "@effect/platform-node/NodeServices"; -import { describe, it } from "@effect/vitest"; +import { describe, it, assert } from "@effect/vitest"; import { Effect, FileSystem, Layer, Path, Sink, Stream } from "effect"; import * as PlatformError from "effect/PlatformError"; import { ChildProcessSpawner } from "effect/unstable/process";