From 02a987003db43f0755162b7ae226c3eb5e271809 Mon Sep 17 00:00:00 2001 From: Markus Hartung Date: Tue, 18 Aug 2026 15:51:03 +0200 Subject: [PATCH] fix(cli): recognize {connections} envelope from /api/providers (#10491) * fix(cli): recognize {connections} envelope from /api/providers GET /api/providers returns {connections, total} (src/app/api/providers/ route.ts:78), but `omniroute test --all-providers` and `omniroute oauth providers` both parsed the response as `data.providers ?? data.items ?? data` -- an object, not an array -- so `.filter` threw "(data.providers ?? data.items ?? data).filter is not a function" on every call. keys.mjs already had the correct fallback chain (`data.keys || data.connections || data.items || data`); apply the same `connections` field to both remaining call sites. * test(cli): cover provider connections envelope Add regression coverage for both CLI consumers of the /api/providers connections envelope. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --- bin/cli/commands/oauth.mjs | 2 +- bin/cli/commands/test-provider.mjs | 2 +- tests/unit/cli-expanded-commands.test.ts | 51 ++++++++++++++++++++++++ tests/unit/cli-oauth-commands.test.ts | 20 ++++++++++ 4 files changed, 73 insertions(+), 2 deletions(-) diff --git a/bin/cli/commands/oauth.mjs b/bin/cli/commands/oauth.mjs index decff7e343..8bf547b2c0 100644 --- a/bin/cli/commands/oauth.mjs +++ b/bin/cli/commands/oauth.mjs @@ -302,7 +302,7 @@ export async function runOAuthStatus(opts, cmd) { process.exit(1); } const data = await res.json(); - const connections = (data.providers ?? data.items ?? data).filter( + const connections = (data.connections ?? data.providers ?? data.items ?? data).filter( (c) => c.authType === "oauth" || c.authType === "oauth2" ); emit(connections, globalOpts, connectionSchema); diff --git a/bin/cli/commands/test-provider.mjs b/bin/cli/commands/test-provider.mjs index 4c45e81f6a..8802f75cd1 100644 --- a/bin/cli/commands/test-provider.mjs +++ b/bin/cli/commands/test-provider.mjs @@ -80,7 +80,7 @@ async function _runAllProviders(opts) { return 1; } const data = await res.json(); - const connections = (data.providers ?? data.items ?? data).filter( + const connections = (data.connections ?? data.providers ?? data.items ?? data).filter( (c) => c.authType === "apikey" || c.testStatus !== "unavailable" ); if (connections.length === 0) { diff --git a/tests/unit/cli-expanded-commands.test.ts b/tests/unit/cli-expanded-commands.test.ts index 74bfa2711e..1327351555 100644 --- a/tests/unit/cli-expanded-commands.test.ts +++ b/tests/unit/cli-expanded-commands.test.ts @@ -305,3 +305,54 @@ test("test-provider — compare requer pelo menos dois modelos sem server retorn } assert.ok(code === 0 || code === 1); }); + +test("test-provider --all-providers consumes the connections envelope", async () => { + const origFetch = globalThis.fetch; + const connections = [ + { id: "conn1", provider: "anthropic", defaultModel: "claude", authType: "apikey" }, + { id: "conn2", provider: "gemini", defaultModel: "gemini", authType: "oauth" }, + ]; + const requests: string[] = []; + globalThis.fetch = ((url: string) => { + requests.push(url); + if (url.includes("/api/health")) return Promise.resolve(new Response("{}", { status: 200 })); + if (url.includes("/api/providers?limit=200")) { + return Promise.resolve(new Response(JSON.stringify({ connections }), { status: 200 })); + } + if (url.includes("/api/v1/providers/test")) { + return Promise.resolve(new Response(JSON.stringify({ success: true }), { status: 201 })); + } + throw new Error(`unexpected URL: ${url}`); + }) as typeof fetch; + + try { + const { runTestProviderCommand } = await import("../../bin/cli/commands/test-provider.mjs"); + const output: string[] = []; + const origWrite = process.stdout.write.bind(process.stdout); + process.stdout.write = ((chunk: string | Uint8Array) => { + if (typeof chunk === "string") output.push(chunk); + return true; + }) as typeof process.stdout.write; + try { + const code = await runTestProviderCommand(undefined, undefined, { + allProviders: true, + json: true, + }); + assert.equal(code, 0); + } finally { + process.stdout.write = origWrite; + } + assert.ok(requests.some((url) => url.includes("/api/providers?limit=200"))); + const parsed = JSON.parse(output.join("")); + assert.deepEqual( + parsed.map(({ provider, model }: { provider: string; model: string }) => ({ provider, model })), + [ + { provider: "anthropic", model: "claude" }, + { provider: "gemini", model: "gemini" }, + ], + ); + assert.ok(parsed.every(({ success }: { success: boolean }) => success)); + } finally { + globalThis.fetch = origFetch; + } +}); diff --git a/tests/unit/cli-oauth-commands.test.ts b/tests/unit/cli-oauth-commands.test.ts index ffc17b68f3..63301f818f 100644 --- a/tests/unit/cli-oauth-commands.test.ts +++ b/tests/unit/cli-oauth-commands.test.ts @@ -95,6 +95,26 @@ test("runOAuthStatus filtra por provider", async () => { assert.ok(capturedUrl.includes("provider=gemini")); }); +test("runOAuthStatus consumes the connections envelope", async () => { + const origFetch = globalThis.fetch; + globalThis.fetch = ((url: string) => { + assert.ok(url.includes("/api/providers")); + return Promise.resolve(makeResp({ connections: CONNECTIONS })); + }) as any; + + try { + const { runOAuthStatus } = await import("../../bin/cli/commands/oauth.mjs"); + const out = await captureStdout(() => runOAuthStatus({}, makeCmd() as any)); + const parsed = JSON.parse(out); + assert.deepEqual( + parsed.map((connection: { id: string }) => connection.id), + ["conn1", "conn2"], + ); + } finally { + globalThis.fetch = origFetch; + } +}); + test("runOAuthRevoke com --yes chama endpoint de revogação", async () => { let capturedUrl = ""; let capturedMethod = "";