| ... | ... | @@ -1,10 +1,15 @@ |
| 1 | | import assert from "node:assert/strict"; |
| 2 | | import { it } from "@effect/vitest"; |
| 3 | | import { Effect, Layer, Sink, Stream } from "effect"; |
| 1 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 2 | import { describe, it, assert } from "@effect/vitest"; |
| 3 | import { Effect, FileSystem, Layer, Path, Sink, Stream } from "effect"; |
| 4 | 4 | import * as PlatformError from "effect/PlatformError"; |
| 5 | 5 | import { ChildProcessSpawner } from "effect/unstable/process"; |
| 6 | 6 | |
| 7 | | import { checkCodexProviderStatus, parseAuthStatusFromOutput } from "./ProviderHealth"; |
| 7 | import { |
| 8 | checkCodexProviderStatus, |
| 9 | hasCustomModelProvider, |
| 10 | parseAuthStatusFromOutput, |
| 11 | readCodexConfigModelProvider, |
| 12 | } from "./ProviderHealth"; |
| 8 | 13 | |
| 9 | 14 | // ── Test helpers ──────────────────────────────────────────────────── |
| 10 | 15 | |
| ... | ... | @@ -53,157 +58,410 @@ function failingSpawnerLayer(description: string) { |
| 53 | 58 | ); |
| 54 | 59 | } |
| 55 | 60 | |
| 56 | | // ── Tests ─────────────────────────────────────────────────────────── |
| 57 | | |
| 58 | | it.effect("returns ready when codex is installed and authenticated", () => |
| 59 | | Effect.gen(function* () { |
| 60 | | const status = yield* checkCodexProviderStatus; |
| 61 | | assert.strictEqual(status.provider, "codex"); |
| 62 | | assert.strictEqual(status.status, "ready"); |
| 63 | | assert.strictEqual(status.available, true); |
| 64 | | assert.strictEqual(status.authStatus, "authenticated"); |
| 65 | | }).pipe( |
| 66 | | Effect.provide( |
| 67 | | mockSpawnerLayer((args) => { |
| 68 | | const joined = args.join(" "); |
| 69 | | if (joined === "--version") return { stdout: "codex 1.0.0\n", stderr: "", code: 0 }; |
| 70 | | if (joined === "login status") return { stdout: "Logged in\n", stderr: "", code: 0 }; |
| 71 | | throw new Error(`Unexpected args: ${joined}`); |
| 61 | /** |
| 62 | * Create a temporary CODEX_HOME scoped to the current Effect test. |
| 63 | * Cleanup is registered in the test scope rather than via Vitest hooks. |
| 64 | */ |
| 65 | function withTempCodexHome(configContent?: string) { |
| 66 | return Effect.gen(function* () { |
| 67 | const fileSystem = yield* FileSystem.FileSystem; |
| 68 | const path = yield* Path.Path; |
| 69 | const tmpDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-test-codex-" }); |
| 70 | |
| 71 | yield* Effect.acquireRelease( |
| 72 | Effect.sync(() => { |
| 73 | const originalCodexHome = process.env.CODEX_HOME; |
| 74 | process.env.CODEX_HOME = tmpDir; |
| 75 | return originalCodexHome; |
| 72 | 76 | }), |
| 73 | | ), |
| 74 | | ), |
| 75 | | ); |
| 76 | | |
| 77 | | it.effect("returns unavailable when codex is missing", () => |
| 78 | | Effect.gen(function* () { |
| 79 | | const status = yield* checkCodexProviderStatus; |
| 80 | | assert.strictEqual(status.provider, "codex"); |
| 81 | | assert.strictEqual(status.status, "error"); |
| 82 | | assert.strictEqual(status.available, false); |
| 83 | | assert.strictEqual(status.authStatus, "unknown"); |
| 84 | | assert.strictEqual(status.message, "Codex CLI (`codex`) is not installed or not on PATH."); |
| 85 | | }).pipe(Effect.provide(failingSpawnerLayer("spawn codex ENOENT"))), |
| 86 | | ); |
| 87 | | |
| 88 | | it.effect("returns unavailable when codex is below the minimum supported version", () => |
| 89 | | Effect.gen(function* () { |
| 90 | | const status = yield* checkCodexProviderStatus; |
| 91 | | assert.strictEqual(status.provider, "codex"); |
| 92 | | assert.strictEqual(status.status, "error"); |
| 93 | | assert.strictEqual(status.available, false); |
| 94 | | assert.strictEqual(status.authStatus, "unknown"); |
| 95 | | assert.strictEqual( |
| 96 | | status.message, |
| 97 | | "Codex CLI v0.36.0 is too old for T3 Code. Upgrade to v0.37.0 or newer and restart T3 Code.", |
| 98 | | ); |
| 99 | | }).pipe( |
| 100 | | Effect.provide( |
| 101 | | mockSpawnerLayer((args) => { |
| 102 | | const joined = args.join(" "); |
| 103 | | if (joined === "--version") return { stdout: "codex 0.36.0\n", stderr: "", code: 0 }; |
| 104 | | throw new Error(`Unexpected args: ${joined}`); |
| 77 | (originalCodexHome) => |
| 78 | Effect.sync(() => { |
| 79 | if (originalCodexHome !== undefined) { |
| 80 | process.env.CODEX_HOME = originalCodexHome; |
| 81 | } else { |
| 82 | delete process.env.CODEX_HOME; |
| 83 | } |
| 84 | }), |
| 85 | ); |
| 86 | |
| 87 | if (configContent !== undefined) { |
| 88 | yield* fileSystem.writeFileString(path.join(tmpDir, "config.toml"), configContent); |
| 89 | } |
| 90 | |
| 91 | return { tmpDir } as const; |
| 92 | }); |
| 93 | } |
| 94 | |
| 95 | it.layer(NodeServices.layer)("ProviderHealth", (it) => { |
| 96 | // ── checkCodexProviderStatus tests ──────────────────────────────── |
| 97 | // |
| 98 | // These tests control CODEX_HOME to ensure the custom-provider detection |
| 99 | // in hasCustomModelProvider() does not interfere with the auth-probe |
| 100 | // path being tested. |
| 101 | |
| 102 | describe("checkCodexProviderStatus", () => { |
| 103 | it.effect("returns ready when codex is installed and authenticated", () => |
| 104 | Effect.gen(function* () { |
| 105 | // Point CODEX_HOME at an empty tmp dir (no config.toml) so the |
| 106 | // default code path (OpenAI provider, auth probe runs) is exercised. |
| 107 | yield* withTempCodexHome(); |
| 108 | const status = yield* checkCodexProviderStatus; |
| 109 | assert.strictEqual(status.provider, "codex"); |
| 110 | assert.strictEqual(status.status, "ready"); |
| 111 | assert.strictEqual(status.available, true); |
| 112 | assert.strictEqual(status.authStatus, "authenticated"); |
| 113 | }).pipe( |
| 114 | Effect.provide( |
| 115 | mockSpawnerLayer((args) => { |
| 116 | const joined = args.join(" "); |
| 117 | if (joined === "--version") return { stdout: "codex 1.0.0\n", stderr: "", code: 0 }; |
| 118 | if (joined === "login status") return { stdout: "Logged in\n", stderr: "", code: 0 }; |
| 119 | throw new Error(`Unexpected args: ${joined}`); |
| 120 | }), |
| 121 | ), |
| 122 | ), |
| 123 | ); |
| 124 | |
| 125 | it.effect("returns unavailable when codex is missing", () => |
| 126 | Effect.gen(function* () { |
| 127 | yield* withTempCodexHome(); |
| 128 | const status = yield* checkCodexProviderStatus; |
| 129 | assert.strictEqual(status.provider, "codex"); |
| 130 | assert.strictEqual(status.status, "error"); |
| 131 | assert.strictEqual(status.available, false); |
| 132 | assert.strictEqual(status.authStatus, "unknown"); |
| 133 | assert.strictEqual(status.message, "Codex CLI (`codex`) is not installed or not on PATH."); |
| 134 | }).pipe(Effect.provide(failingSpawnerLayer("spawn codex ENOENT"))), |
| 135 | ); |
| 136 | |
| 137 | it.effect("returns unavailable when codex is below the minimum supported version", () => |
| 138 | Effect.gen(function* () { |
| 139 | yield* withTempCodexHome(); |
| 140 | const status = yield* checkCodexProviderStatus; |
| 141 | assert.strictEqual(status.provider, "codex"); |
| 142 | assert.strictEqual(status.status, "error"); |
| 143 | assert.strictEqual(status.available, false); |
| 144 | assert.strictEqual(status.authStatus, "unknown"); |
| 145 | assert.strictEqual( |
| 146 | status.message, |
| 147 | "Codex CLI v0.36.0 is too old for T3 Code. Upgrade to v0.37.0 or newer and restart T3 Code.", |
| 148 | ); |
| 149 | }).pipe( |
| 150 | Effect.provide( |
| 151 | mockSpawnerLayer((args) => { |
| 152 | const joined = args.join(" "); |
| 153 | if (joined === "--version") return { stdout: "codex 0.36.0\n", stderr: "", code: 0 }; |
| 154 | throw new Error(`Unexpected args: ${joined}`); |
| 155 | }), |
| 156 | ), |
| 157 | ), |
| 158 | ); |
| 159 | |
| 160 | it.effect("returns unauthenticated when auth probe reports login required", () => |
| 161 | Effect.gen(function* () { |
| 162 | yield* withTempCodexHome(); |
| 163 | const status = yield* checkCodexProviderStatus; |
| 164 | assert.strictEqual(status.provider, "codex"); |
| 165 | assert.strictEqual(status.status, "error"); |
| 166 | assert.strictEqual(status.available, true); |
| 167 | assert.strictEqual(status.authStatus, "unauthenticated"); |
| 168 | assert.strictEqual( |
| 169 | status.message, |
| 170 | "Codex CLI is not authenticated. Run `codex login` and try again.", |
| 171 | ); |
| 172 | }).pipe( |
| 173 | Effect.provide( |
| 174 | mockSpawnerLayer((args) => { |
| 175 | const joined = args.join(" "); |
| 176 | if (joined === "--version") return { stdout: "codex 1.0.0\n", stderr: "", code: 0 }; |
| 177 | if (joined === "login status") { |
| 178 | return { stdout: "", stderr: "Not logged in. Run codex login.", code: 1 }; |
| 179 | } |
| 180 | throw new Error(`Unexpected args: ${joined}`); |
| 181 | }), |
| 182 | ), |
| 183 | ), |
| 184 | ); |
| 185 | |
| 186 | it.effect("returns unauthenticated when login status output includes 'not logged in'", () => |
| 187 | Effect.gen(function* () { |
| 188 | yield* withTempCodexHome(); |
| 189 | const status = yield* checkCodexProviderStatus; |
| 190 | assert.strictEqual(status.provider, "codex"); |
| 191 | assert.strictEqual(status.status, "error"); |
| 192 | assert.strictEqual(status.available, true); |
| 193 | assert.strictEqual(status.authStatus, "unauthenticated"); |
| 194 | assert.strictEqual( |
| 195 | status.message, |
| 196 | "Codex CLI is not authenticated. Run `codex login` and try again.", |
| 197 | ); |
| 198 | }).pipe( |
| 199 | Effect.provide( |
| 200 | mockSpawnerLayer((args) => { |
| 201 | const joined = args.join(" "); |
| 202 | if (joined === "--version") return { stdout: "codex 1.0.0\n", stderr: "", code: 0 }; |
| 203 | if (joined === "login status") |
| 204 | return { stdout: "Not logged in\n", stderr: "", code: 1 }; |
| 205 | throw new Error(`Unexpected args: ${joined}`); |
| 206 | }), |
| 207 | ), |
| 208 | ), |
| 209 | ); |
| 210 | |
| 211 | it.effect("returns warning when login status command is unsupported", () => |
| 212 | Effect.gen(function* () { |
| 213 | yield* withTempCodexHome(); |
| 214 | const status = yield* checkCodexProviderStatus; |
| 215 | assert.strictEqual(status.provider, "codex"); |
| 216 | assert.strictEqual(status.status, "warning"); |
| 217 | assert.strictEqual(status.available, true); |
| 218 | assert.strictEqual(status.authStatus, "unknown"); |
| 219 | assert.strictEqual( |
| 220 | status.message, |
| 221 | "Codex CLI authentication status command is unavailable in this Codex version.", |
| 222 | ); |
| 223 | }).pipe( |
| 224 | Effect.provide( |
| 225 | mockSpawnerLayer((args) => { |
| 226 | const joined = args.join(" "); |
| 227 | if (joined === "--version") return { stdout: "codex 1.0.0\n", stderr: "", code: 0 }; |
| 228 | if (joined === "login status") { |
| 229 | return { stdout: "", stderr: "error: unknown command 'login'", code: 2 }; |
| 230 | } |
| 231 | throw new Error(`Unexpected args: ${joined}`); |
| 232 | }), |
| 233 | ), |
| 234 | ), |
| 235 | ); |
| 236 | }); |
| 237 | |
| 238 | // ── Custom model provider: checkCodexProviderStatus integration ─── |
| 239 | |
| 240 | describe("checkCodexProviderStatus with custom model provider", () => { |
| 241 | it.effect("skips auth probe and returns ready when a custom model provider is configured", () => |
| 242 | Effect.gen(function* () { |
| 243 | yield* withTempCodexHome( |
| 244 | [ |
| 245 | 'model_provider = "portkey"', |
| 246 | "", |
| 247 | "[model_providers.portkey]", |
| 248 | 'base_url = "https://api.portkey.ai/v1"', |
| 249 | 'env_key = "PORTKEY_API_KEY"', |
| 250 | ].join("\n"), |
| 251 | ); |
| 252 | const status = yield* checkCodexProviderStatus; |
| 253 | assert.strictEqual(status.provider, "codex"); |
| 254 | assert.strictEqual(status.status, "ready"); |
| 255 | assert.strictEqual(status.available, true); |
| 256 | assert.strictEqual(status.authStatus, "unknown"); |
| 257 | assert.strictEqual( |
| 258 | status.message, |
| 259 | "Using a custom Codex model provider; OpenAI login check skipped.", |
| 260 | ); |
| 261 | }).pipe( |
| 262 | Effect.provide( |
| 263 | // The spawner only handles --version; if the test attempts |
| 264 | // "login status" the throw proves the auth probe was NOT skipped. |
| 265 | mockSpawnerLayer((args) => { |
| 266 | const joined = args.join(" "); |
| 267 | if (joined === "--version") return { stdout: "codex 1.0.0\n", stderr: "", code: 0 }; |
| 268 | throw new Error(`Auth probe should have been skipped but got args: ${joined}`); |
| 269 | }), |
| 270 | ), |
| 271 | ), |
| 272 | ); |
| 273 | |
| 274 | it.effect("still reports error when codex CLI is missing even with custom provider", () => |
| 275 | Effect.gen(function* () { |
| 276 | yield* withTempCodexHome( |
| 277 | [ |
| 278 | 'model_provider = "portkey"', |
| 279 | "", |
| 280 | "[model_providers.portkey]", |
| 281 | 'base_url = "https://api.portkey.ai/v1"', |
| 282 | 'env_key = "PORTKEY_API_KEY"', |
| 283 | ].join("\n"), |
| 284 | ); |
| 285 | const status = yield* checkCodexProviderStatus; |
| 286 | assert.strictEqual(status.status, "error"); |
| 287 | assert.strictEqual(status.available, false); |
| 288 | }).pipe(Effect.provide(failingSpawnerLayer("spawn codex ENOENT"))), |
| 289 | ); |
| 290 | }); |
| 291 | |
| 292 | describe("checkCodexProviderStatus with openai model provider", () => { |
| 293 | it.effect("still runs auth probe when model_provider is openai", () => |
| 294 | Effect.gen(function* () { |
| 295 | yield* withTempCodexHome('model_provider = "openai"\n'); |
| 296 | const status = yield* checkCodexProviderStatus; |
| 297 | // The auth probe runs and sees "not logged in" → error |
| 298 | assert.strictEqual(status.status, "error"); |
| 299 | assert.strictEqual(status.authStatus, "unauthenticated"); |
| 300 | }).pipe( |
| 301 | Effect.provide( |
| 302 | mockSpawnerLayer((args) => { |
| 303 | const joined = args.join(" "); |
| 304 | if (joined === "--version") return { stdout: "codex 1.0.0\n", stderr: "", code: 0 }; |
| 305 | if (joined === "login status") |
| 306 | return { stdout: "Not logged in\n", stderr: "", code: 1 }; |
| 307 | throw new Error(`Unexpected args: ${joined}`); |
| 308 | }), |
| 309 | ), |
| 310 | ), |
| 311 | ); |
| 312 | }); |
| 313 | |
| 314 | // ── parseAuthStatusFromOutput pure tests ────────────────────────── |
| 315 | |
| 316 | describe("parseAuthStatusFromOutput", () => { |
| 317 | it("exit code 0 with no auth markers is ready", () => { |
| 318 | const parsed = parseAuthStatusFromOutput({ stdout: "OK\n", stderr: "", code: 0 }); |
| 319 | assert.strictEqual(parsed.status, "ready"); |
| 320 | assert.strictEqual(parsed.authStatus, "authenticated"); |
| 321 | }); |
| 322 | |
| 323 | it("JSON with authenticated=false is unauthenticated", () => { |
| 324 | const parsed = parseAuthStatusFromOutput({ |
| 325 | stdout: '[{"authenticated":false}]\n', |
| 326 | stderr: "", |
| 327 | code: 0, |
| 328 | }); |
| 329 | assert.strictEqual(parsed.status, "error"); |
| 330 | assert.strictEqual(parsed.authStatus, "unauthenticated"); |
| 331 | }); |
| 332 | |
| 333 | it("JSON without auth marker is warning", () => { |
| 334 | const parsed = parseAuthStatusFromOutput({ |
| 335 | stdout: '[{"ok":true}]\n', |
| 336 | stderr: "", |
| 337 | code: 0, |
| 338 | }); |
| 339 | assert.strictEqual(parsed.status, "warning"); |
| 340 | assert.strictEqual(parsed.authStatus, "unknown"); |
| 341 | }); |
| 342 | }); |
| 343 | |
| 344 | // ── readCodexConfigModelProvider tests ───────────────────────────── |
| 345 | |
| 346 | describe("readCodexConfigModelProvider", () => { |
| 347 | it.effect("returns undefined when config file does not exist", () => |
| 348 | Effect.gen(function* () { |
| 349 | yield* withTempCodexHome(); |
| 350 | assert.strictEqual(yield* readCodexConfigModelProvider, undefined); |
| 105 | 351 | }), |
| 106 | | ), |
| 107 | | ), |
| 108 | | ); |
| 109 | | |
| 110 | | it.effect("returns unauthenticated when auth probe reports login required", () => |
| 111 | | Effect.gen(function* () { |
| 112 | | const status = yield* checkCodexProviderStatus; |
| 113 | | assert.strictEqual(status.provider, "codex"); |
| 114 | | assert.strictEqual(status.status, "error"); |
| 115 | | assert.strictEqual(status.available, true); |
| 116 | | assert.strictEqual(status.authStatus, "unauthenticated"); |
| 117 | | assert.strictEqual( |
| 118 | | status.message, |
| 119 | | "Codex CLI is not authenticated. Run `codex login` and try again.", |
| 120 | | ); |
| 121 | | }).pipe( |
| 122 | | Effect.provide( |
| 123 | | mockSpawnerLayer((args) => { |
| 124 | | const joined = args.join(" "); |
| 125 | | if (joined === "--version") return { stdout: "codex 1.0.0\n", stderr: "", code: 0 }; |
| 126 | | if (joined === "login status") { |
| 127 | | return { stdout: "", stderr: "Not logged in. Run codex login.", code: 1 }; |
| 128 | | } |
| 129 | | throw new Error(`Unexpected args: ${joined}`); |
| 352 | ); |
| 353 | |
| 354 | it.effect("returns undefined when config has no model_provider key", () => |
| 355 | Effect.gen(function* () { |
| 356 | yield* withTempCodexHome('model = "gpt-5-codex"\n'); |
| 357 | assert.strictEqual(yield* readCodexConfigModelProvider, undefined); |
| 130 | 358 | }), |
| 131 | | ), |
| 132 | | ), |
| 133 | | ); |
| 134 | | |
| 135 | | it.effect("returns unauthenticated when login status output includes 'not logged in'", () => |
| 136 | | Effect.gen(function* () { |
| 137 | | const status = yield* checkCodexProviderStatus; |
| 138 | | assert.strictEqual(status.provider, "codex"); |
| 139 | | assert.strictEqual(status.status, "error"); |
| 140 | | assert.strictEqual(status.available, true); |
| 141 | | assert.strictEqual(status.authStatus, "unauthenticated"); |
| 142 | | assert.strictEqual( |
| 143 | | status.message, |
| 144 | | "Codex CLI is not authenticated. Run `codex login` and try again.", |
| 145 | | ); |
| 146 | | }).pipe( |
| 147 | | Effect.provide( |
| 148 | | mockSpawnerLayer((args) => { |
| 149 | | const joined = args.join(" "); |
| 150 | | if (joined === "--version") return { stdout: "codex 1.0.0\n", stderr: "", code: 0 }; |
| 151 | | if (joined === "login status") return { stdout: "Not logged in\n", stderr: "", code: 1 }; |
| 152 | | throw new Error(`Unexpected args: ${joined}`); |
| 359 | ); |
| 360 | |
| 361 | it.effect("returns the provider when model_provider is set at top level", () => |
| 362 | Effect.gen(function* () { |
| 363 | yield* withTempCodexHome('model = "gpt-5-codex"\nmodel_provider = "portkey"\n'); |
| 364 | assert.strictEqual(yield* readCodexConfigModelProvider, "portkey"); |
| 153 | 365 | }), |
| 154 | | ), |
| 155 | | ), |
| 156 | | ); |
| 157 | | |
| 158 | | it.effect("returns warning when login status command is unsupported", () => |
| 159 | | Effect.gen(function* () { |
| 160 | | const status = yield* checkCodexProviderStatus; |
| 161 | | assert.strictEqual(status.provider, "codex"); |
| 162 | | assert.strictEqual(status.status, "warning"); |
| 163 | | assert.strictEqual(status.available, true); |
| 164 | | assert.strictEqual(status.authStatus, "unknown"); |
| 165 | | assert.strictEqual( |
| 166 | | status.message, |
| 167 | | "Codex CLI authentication status command is unavailable in this Codex version.", |
| 168 | | ); |
| 169 | | }).pipe( |
| 170 | | Effect.provide( |
| 171 | | mockSpawnerLayer((args) => { |
| 172 | | const joined = args.join(" "); |
| 173 | | if (joined === "--version") return { stdout: "codex 1.0.0\n", stderr: "", code: 0 }; |
| 174 | | if (joined === "login status") { |
| 175 | | return { stdout: "", stderr: "error: unknown command 'login'", code: 2 }; |
| 176 | | } |
| 177 | | throw new Error(`Unexpected args: ${joined}`); |
| 366 | ); |
| 367 | |
| 368 | it.effect("returns openai when model_provider is openai", () => |
| 369 | Effect.gen(function* () { |
| 370 | yield* withTempCodexHome('model_provider = "openai"\n'); |
| 371 | assert.strictEqual(yield* readCodexConfigModelProvider, "openai"); |
| 178 | 372 | }), |
| 179 | | ), |
| 180 | | ), |
| 181 | | ); |
| 373 | ); |
| 182 | 374 | |
| 183 | | // ── Pure function tests ───────────────────────────────────────────── |
| 375 | it.effect("ignores model_provider inside section headers", () => |
| 376 | Effect.gen(function* () { |
| 377 | yield* withTempCodexHome( |
| 378 | [ |
| 379 | 'model = "gpt-5-codex"', |
| 380 | "", |
| 381 | "[model_providers.portkey]", |
| 382 | 'base_url = "https://api.portkey.ai/v1"', |
| 383 | 'model_provider = "should-be-ignored"', |
| 384 | "", |
| 385 | ].join("\n"), |
| 386 | ); |
| 387 | assert.strictEqual(yield* readCodexConfigModelProvider, undefined); |
| 388 | }), |
| 389 | ); |
| 184 | 390 | |
| 185 | | it("parseAuthStatusFromOutput: exit code 0 with no auth markers is ready", () => { |
| 186 | | const parsed = parseAuthStatusFromOutput({ stdout: "OK\n", stderr: "", code: 0 }); |
| 187 | | assert.strictEqual(parsed.status, "ready"); |
| 188 | | assert.strictEqual(parsed.authStatus, "authenticated"); |
| 189 | | }); |
| 391 | it.effect("handles comments and whitespace", () => |
| 392 | Effect.gen(function* () { |
| 393 | yield* withTempCodexHome( |
| 394 | [ |
| 395 | "# This is a comment", |
| 396 | "", |
| 397 | ' model_provider = "azure" ', |
| 398 | "", |
| 399 | "[profiles.deep-review]", |
| 400 | 'model = "gpt-5-pro"', |
| 401 | ].join("\n"), |
| 402 | ); |
| 403 | assert.strictEqual(yield* readCodexConfigModelProvider, "azure"); |
| 404 | }), |
| 405 | ); |
| 190 | 406 | |
| 191 | | it("parseAuthStatusFromOutput: JSON with authenticated=false is unauthenticated", () => { |
| 192 | | const parsed = parseAuthStatusFromOutput({ |
| 193 | | stdout: '[{"authenticated":false}]\n', |
| 194 | | stderr: "", |
| 195 | | code: 0, |
| 407 | it.effect("handles single-quoted values in TOML", () => |
| 408 | Effect.gen(function* () { |
| 409 | yield* withTempCodexHome("model_provider = 'mistral'\n"); |
| 410 | assert.strictEqual(yield* readCodexConfigModelProvider, "mistral"); |
| 411 | }), |
| 412 | ); |
| 196 | 413 | }); |
| 197 | | assert.strictEqual(parsed.status, "error"); |
| 198 | | assert.strictEqual(parsed.authStatus, "unauthenticated"); |
| 199 | | }); |
| 200 | 414 | |
| 201 | | it("parseAuthStatusFromOutput: JSON without auth marker is warning", () => { |
| 202 | | const parsed = parseAuthStatusFromOutput({ |
| 203 | | stdout: '[{"ok":true}]\n', |
| 204 | | stderr: "", |
| 205 | | code: 0, |
| 415 | // ── hasCustomModelProvider tests ─────────────────────────────────── |
| 416 | |
| 417 | describe("hasCustomModelProvider", () => { |
| 418 | it.effect("returns false when no config file exists", () => |
| 419 | Effect.gen(function* () { |
| 420 | yield* withTempCodexHome(); |
| 421 | assert.strictEqual(yield* hasCustomModelProvider, false); |
| 422 | }), |
| 423 | ); |
| 424 | |
| 425 | it.effect("returns false when model_provider is not set", () => |
| 426 | Effect.gen(function* () { |
| 427 | yield* withTempCodexHome('model = "gpt-5-codex"\n'); |
| 428 | assert.strictEqual(yield* hasCustomModelProvider, false); |
| 429 | }), |
| 430 | ); |
| 431 | |
| 432 | it.effect("returns false when model_provider is openai", () => |
| 433 | Effect.gen(function* () { |
| 434 | yield* withTempCodexHome('model_provider = "openai"\n'); |
| 435 | assert.strictEqual(yield* hasCustomModelProvider, false); |
| 436 | }), |
| 437 | ); |
| 438 | |
| 439 | it.effect("returns true when model_provider is portkey", () => |
| 440 | Effect.gen(function* () { |
| 441 | yield* withTempCodexHome('model_provider = "portkey"\n'); |
| 442 | assert.strictEqual(yield* hasCustomModelProvider, true); |
| 443 | }), |
| 444 | ); |
| 445 | |
| 446 | it.effect("returns true when model_provider is azure", () => |
| 447 | Effect.gen(function* () { |
| 448 | yield* withTempCodexHome('model_provider = "azure"\n'); |
| 449 | assert.strictEqual(yield* hasCustomModelProvider, true); |
| 450 | }), |
| 451 | ); |
| 452 | |
| 453 | it.effect("returns true when model_provider is ollama", () => |
| 454 | Effect.gen(function* () { |
| 455 | yield* withTempCodexHome('model_provider = "ollama"\n'); |
| 456 | assert.strictEqual(yield* hasCustomModelProvider, true); |
| 457 | }), |
| 458 | ); |
| 459 | |
| 460 | it.effect("returns true when model_provider is a custom proxy", () => |
| 461 | Effect.gen(function* () { |
| 462 | yield* withTempCodexHome('model_provider = "my-company-proxy"\n'); |
| 463 | assert.strictEqual(yield* hasCustomModelProvider, true); |
| 464 | }), |
| 465 | ); |
| 206 | 466 | }); |
| 207 | | assert.strictEqual(parsed.status, "warning"); |
| 208 | | assert.strictEqual(parsed.authStatus, "unknown"); |
| 209 | 467 | }); |