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>
This commit is contained in:
Markus Hartung
2026-08-18 15:51:03 +02:00
committed by GitHub
parent 4c5535be4e
commit 02a987003d
4 changed files with 73 additions and 2 deletions

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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;
}
});

View File

@@ -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 = "";