| author | |
| committer | |
| log | 9db54c9b19b136526baa97677e244b4b0071ee5b |
| tree | 6a3e97c6303020c54ad122bdaa977fe08b3e09eb |
| parent | 34430e3514041935898aba5282392fac605afa71 |
- Parse nested/array JSON auth status output from `codex auth status --json`
- Return `unauthenticated` when JSON contains `authenticated: false`
- Return warning when JSON is parseable but lacks an auth marker2 files changed, 85 insertions(+), 11 deletions(-)
apps/server/src/provider/Layers/ProviderHealth.test.ts+50| ... | ... | @@ -79,6 +79,56 @@ describe("checkProviderStatusesOnStartup", () => { |
| 79 | 79 | ]); |
| 80 | 80 | }); |
| 81 | 81 | |
| 82 | it("returns unauthenticated when auth probe JSON array includes authenticated=false", async () => { | |
| 83 | const status = await checkProviderStatusesOnStartup(async (_command, args) => { | |
| 84 | if (args.join(" ") === "--version") { | |
| 85 | return result({ stdout: "codex 1.0.0\n" }); | |
| 86 | } | |
| 87 | if (args.join(" ") === "auth status --json") { | |
| 88 | return result({ | |
| 89 | stdout: '[{"authenticated":false}]\n', | |
| 90 | }); | |
| 91 | } | |
| 92 | throw new Error(`Unexpected command: ${args.join(" ")}`); | |
| 93 | }); | |
| 94 | ||
| 95 | expect(status).toEqual([ | |
| 96 | expect.objectContaining({ | |
| 97 | provider: "codex", | |
| 98 | status: "error", | |
| 99 | available: true, | |
| 100 | authStatus: "unauthenticated", | |
| 101 | message: "Codex CLI is not authenticated. Run `codex login` and try again.", | |
| 102 | }), | |
| 103 | ]); | |
| 104 | }); | |
| 105 | ||
| 106 | it("returns warning when auth probe JSON array has no auth marker", async () => { | |
| 107 | const status = await checkProviderStatusesOnStartup(async (_command, args) => { | |
| 108 | if (args.join(" ") === "--version") { | |
| 109 | return result({ stdout: "codex 1.0.0\n" }); | |
| 110 | } | |
| 111 | if (args.join(" ") === "auth status --json") { | |
| 112 | return result({ | |
| 113 | code: 0, | |
| 114 | stdout: '[{"ok":true}]\n', | |
| 115 | }); | |
| 116 | } | |
| 117 | throw new Error(`Unexpected command: ${args.join(" ")}`); | |
| 118 | }); | |
| 119 | ||
| 120 | expect(status).toEqual([ | |
| 121 | expect.objectContaining({ | |
| 122 | provider: "codex", | |
| 123 | status: "warning", | |
| 124 | available: true, | |
| 125 | authStatus: "unknown", | |
| 126 | message: | |
| 127 | "Could not verify Codex authentication status from JSON output (missing auth marker).", | |
| 128 | }), | |
| 129 | ]); | |
| 130 | }); | |
| 131 | ||
| 82 | 132 | it("returns warning when auth status command is unsupported", async () => { |
| 83 | 133 | const status = await checkProviderStatusesOnStartup(async (_command, args) => { |
| 84 | 134 | if (args.join(" ") === "--version") { |
apps/server/src/provider/Layers/ProviderHealth.ts+35-11| ... | ... | @@ -54,7 +54,17 @@ function detailFromResult(result: ProcessRunResult): string | undefined { |
| 54 | 54 | } |
| 55 | 55 | |
| 56 | 56 | function extractAuthBoolean(value: unknown): boolean | undefined { |
| 57 | if (!value || typeof value !== "object" || Array.isArray(value)) { | |
| 57 | if (Array.isArray(value)) { | |
| 58 | for (const entry of value) { | |
| 59 | const nested = extractAuthBoolean(entry); | |
| 60 | if (nested !== undefined) { | |
| 61 | return nested; | |
| 62 | } | |
| 63 | } | |
| 64 | return undefined; | |
| 65 | } | |
| 66 | ||
| 67 | if (!value || typeof value !== "object") { | |
| 58 | 68 | return undefined; |
| 59 | 69 | } |
| 60 | 70 | |
| ... | ... | @@ -122,19 +132,28 @@ function parseAuthStatusFromOutput( |
| 122 | 132 | (() => { |
| 123 | 133 | const trimmed = result.stdout.trim(); |
| 124 | 134 | if (!trimmed || (!trimmed.startsWith("{") && !trimmed.startsWith("["))) { |
| 125 | return undefined; | |
| 135 | return { | |
| 136 | attemptedJsonParse: false as const, | |
| 137 | auth: undefined as boolean | undefined, | |
| 138 | }; | |
| 126 | 139 | } |
| 127 | 140 | try { |
| 128 | return extractAuthBoolean(JSON.parse(trimmed)); | |
| 141 | return { | |
| 142 | attemptedJsonParse: true as const, | |
| 143 | auth: extractAuthBoolean(JSON.parse(trimmed)), | |
| 144 | }; | |
| 129 | 145 | } catch { |
| 130 | return undefined; | |
| 146 | return { | |
| 147 | attemptedJsonParse: false as const, | |
| 148 | auth: undefined as boolean | undefined, | |
| 149 | }; | |
| 131 | 150 | } |
| 132 | })() ?? undefined; | |
| 151 | })(); | |
| 133 | 152 | |
| 134 | if (parsedAuth === true) { | |
| 153 | if (parsedAuth.auth === true) { | |
| 135 | 154 | return { status: "ready", authStatus: "authenticated" }; |
| 136 | 155 | } |
| 137 | if (parsedAuth === false) { | |
| 156 | if (parsedAuth.auth === false) { | |
| 138 | 157 | return { |
| 139 | 158 | status: "error", |
| 140 | 159 | authStatus: "unauthenticated", |
| ... | ... | @@ -142,6 +161,15 @@ function parseAuthStatusFromOutput( |
| 142 | 161 | }; |
| 143 | 162 | } |
| 144 | 163 | |
| 164 | if (parsedAuth.attemptedJsonParse) { | |
| 165 | return { | |
| 166 | status: "warning", | |
| 167 | authStatus: "unknown", | |
| 168 | message: | |
| 169 | "Could not verify Codex authentication status from JSON output (missing auth marker).", | |
| 170 | }; | |
| 171 | } | |
| 172 | ||
| 145 | 173 | if (result.code === 0) { |
| 146 | 174 | // Successful command with no explicit auth marker is still a pass. |
| 147 | 175 | return { status: "ready", authStatus: "authenticated" }; |
| ... | ... | @@ -240,7 +268,3 @@ const makeProviderHealth = (run: CommandRunner = runProcess) => |
| 240 | 268 | }); |
| 241 | 269 | |
| 242 | 270 | export const ProviderHealthLive = Layer.effect(ProviderHealth, makeProviderHealth()); |
| 243 | ||
| 244 | export function makeProviderHealthLive(run?: CommandRunner) { | |
| 245 | return Layer.effect(ProviderHealth, makeProviderHealth(run)); | |
| 246 | } |