Files
OmniRoute/tests/unit/cli-route-unavailable-fallback-10081.test.ts
Praveen K Palaniswamy 65e81158ab fix(ollama): route models by advertised capability (#11088)
Landed with the design call resolved per the owner's pick — **option 1**: the synced store is now endpoint-agnostic (persistDiscoveredModels and managedModelImport no longer drop non-chat models at write time), and chat selectability moved to read time (auto-pool expansion in autoStrategy applies filterChatSelectableModels; the models-route projection already had its chatOnly filter). Your discovery test now passes end-to-end (3/3): /api/show capabilities persist per connection and image/embedding requests route through the advertising host.

Reconciliation notes: conflicted areas merged onto the current tip (adobe discovery import, requestedModel preflight signature, resolvedProvider fast-path coexists with the synced-route override — explicit resolution wins); carried base-red drains (#10055 memoization, #11071 test variants) dropped as already-landed; the managed-model-import exclusion test was propagated to the new contract (image/video models persist; the read filter still hides them from chat pickers — pinned by a new assertion). Full battery: 205/206 focused (the one red is a confirmed periodic-timer timing flake on the loaded devbox — 20/20 isolated), autoCombo vitest 30/30, combo suites 46/46, gates + typecheck clean.

Thank you @yourspraveen — the capability probe + routing design was right; it just needed the store contract opened up. Fixes #11087.
2026-08-23 11:45:01 -03:00

77 lines
3.6 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
// #10081 — CLI commands swallowed a non-2xx into a benign-looking outcome:
// * `keys add` aborted on any 4xx, so a missing route (404) stranded the user
// even though the local SQLite fallback right below would have worked.
// * `providers test-all` reported OAuth connections as FAILED (and persisted
// that verdict) because getProviderApiKey() throws for them by design.
// * `combo list` printed "No combos configured" when the HTTP call failed,
// which is indistinguishable from genuine emptiness.
const { isRouteUnavailableStatus } = await import("../../bin/cli/api.mjs");
test("isRouteUnavailableStatus separates 'route missing' from real client errors", () => {
// Server does not serve this route -> the caller may fall back locally.
for (const status of [404, 405, 501]) {
assert.equal(isRouteUnavailableStatus(status), true, `${status} should be route-unavailable`);
}
// Genuine client errors must stay fatal — retrying locally would hide a real
// problem (bad credential, insufficient scope, conflict, invalid payload).
for (const status of [400, 401, 403, 409, 422, 429]) {
assert.equal(isRouteUnavailableStatus(status), false, `${status} should stay fatal`);
}
// Server-side failures are handled by the existing retry path, not here.
for (const status of [500, 502, 503]) {
assert.equal(isRouteUnavailableStatus(status), false, `${status} should stay fatal`);
}
});
test("keys add falls through to the local DB only for route-unavailable statuses", async () => {
const source = await import("node:fs").then((fs) =>
fs.readFileSync(new URL("../../bin/cli/commands/keys.mjs", import.meta.url), "utf8")
);
// The guard must exclude route-unavailable statuses, otherwise the
// openOmniRouteDb() fallback below it is unreachable while the server is up.
assert.match(
source,
/res\.status >= 400 && res\.status < 500 && !isRouteUnavailableStatus\(res\.status\)/,
"keys add should not abort on a missing route"
);
});
test("providers test-all skips connections it cannot probe instead of failing them", async () => {
const source = await import("node:fs").then((fs) =>
fs.readFileSync(new URL("../../bin/cli/commands/providers.mjs", import.meta.url), "utf8")
);
// Non-apikey connections return early as skipped, before getProviderApiKey().
const nonApiKeyGuard = source.indexOf('if (connection.authType !== "apikey")');
const getKeyCall = source.indexOf("const apiKey = getProviderApiKey(connection);");
assert.ok(nonApiKeyGuard > -1, "expected a non-apikey guard in runProviderTest");
assert.ok(
nonApiKeyGuard < getKeyCall,
"the guard must run before getProviderApiKey(), which throws for OAuth"
);
// An "unsupported" probe must not overwrite a good test_status with a failure.
assert.match(source, /if \(result\.unsupported\) \{[\s\S]*?skipped: true/);
});
test("combo list reports a transport failure instead of 'No combos configured'", async () => {
const source = await import("node:fs").then((fs) =>
fs.readFileSync(new URL("../../bin/cli/commands/combo.mjs", import.meta.url), "utf8")
);
assert.match(source, /listError = listRes\.status/, "a failed list must be recorded");
// The error branch has to be checked before the empty-list branch, otherwise
// the misleading "no combos" message still wins.
const errorBranch = source.indexOf("if (listError) {");
const emptyBranch = source.indexOf('console.log(t("combo.noCombos"))');
assert.ok(errorBranch > -1 && emptyBranch > -1);
assert.ok(errorBranch < emptyBranch, "listError must be handled before the empty-list message");
});