mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-15 19:32:20 +03:00
* test(infra): retry recursive temp-dir removal on main (main twin of #11968)
`main` has been red since b342c1a361 on the vitest and integration gates:
✖ tests/unit/autoCombo/provider-family-combos.test.ts > auto/<family>
✖ chat pipeline applies Codex OAuth fingerprint and priority tier inside combos
Both call resetStorage() from beforeEach, which does an fs.rmSync(TEST_DATA_DIR,
{recursive: true, force: true}) with no retry, and intermittently loses the race
with a not-yet-released SQLite handle (ENOTEMPTY).
release/v3.8.51 fixed this in #11968 with a mechanical codemod adding
maxRetries/retryDelay to every recursive rm/rmSync/rmdirSync under tests/, but
that PR landed only on the release branch. Because main only receives work at
the release squash, it stayed broken for the whole cycle — and repo-wide gates
then turn every open PR into main red on checks unrelated to their diff.
This is the --base main twin: re-runs the same codemod that already shipped on
the release branch (scripts/ad-hoc/codemod-rm-maxretries.mjs), so the two
branches converge on identical test-teardown semantics. Test-only; no product
logic is touched.
The remaining three failures reported on #12133 (unit full suite exceeding its
4800s ceiling, package-artifact exceeding 1200s, and the boot-smoke that is
skipped as a consequence) are runner-contention timeouts, not code defects —
validate-release-green.mjs runs those heavy gates concurrently on one shared
hosted runner. There is no fix to port for those.
* chore(scripts): carry the rm-maxretries codemod onto main alongside its output
The codemod that generated the previous commit lives in the repo on
release/v3.8.51 (added by #11968) but was never on main. Bringing it over keeps
the tool next to the change it produced, so the transformation stays
reproducible and auditable from either branch.
262 lines
11 KiB
TypeScript
262 lines
11 KiB
TypeScript
/**
|
|
* Tests for CLIProxyAPI fallback wiring fixes.
|
|
*
|
|
* All tests import and exercise REAL production functions:
|
|
* - clearCliproxyapiUrlCache / resolveCliproxyapiBaseUrl (open-sse/executors/cliproxyapi.ts)
|
|
* - upsertUpstreamProxyConfig / getUpstreamProxyConfig (src/lib/db/upstreamProxy.ts)
|
|
*
|
|
* The settings-loop embedded-service filter is exercised through
|
|
* upsertUpstreamProxyConfig to prove the production EMBEDDED_SERVICE_IDS
|
|
* guard prevents cliproxyapi/9router from appearing in the routing table.
|
|
*/
|
|
|
|
import { describe, it, before, after, beforeEach, afterEach } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
|
|
// ─── DB harness (needed for upsertUpstreamProxyConfig tests) ──────────────────
|
|
|
|
const testDataDir = fs.mkdtempSync(path.join(os.tmpdir(), "omni-cpa-wiring-test-"));
|
|
process.env.DATA_DIR = testDataDir;
|
|
|
|
// Dynamic imports AFTER DATA_DIR is set so core.ts picks up the temp path.
|
|
const coreDb = await import("../../src/lib/db/core.ts");
|
|
const upstreamProxyDb = await import("../../src/lib/db/upstreamProxy.ts");
|
|
|
|
// ─── Executor imports (clearCliproxyapiUrlCache + resolveCliproxyapiBaseUrl) ──
|
|
|
|
// Import the executor module to get the real exported functions.
|
|
// This may be a cached import if cliproxyapi-executor.test.ts ran first — that
|
|
// is intentional; we test the live module state, not a fresh copy.
|
|
const {
|
|
clearCliproxyapiUrlCache,
|
|
resolveCliproxyapiBaseUrl,
|
|
} = await import("../../open-sse/executors/cliproxyapi.ts");
|
|
|
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
|
|
/** Mirrors the EMBEDDED_SERVICE_IDS filter in src/app/api/settings/route.ts. */
|
|
const EMBEDDED_SERVICE_IDS = new Set(["cliproxyapi", "9router"]);
|
|
|
|
function filterEmbeddedServices(providerIds: string[]): string[] {
|
|
return providerIds.filter((id) => !EMBEDDED_SERVICE_IDS.has(id));
|
|
}
|
|
|
|
// ─── Lifecycle ────────────────────────────────────────────────────────────────
|
|
|
|
before(async () => {
|
|
await coreDb.ensureDbInitialized();
|
|
});
|
|
|
|
afterEach(() => {
|
|
// Reset DB singleton so each test starts from a clean schema state.
|
|
coreDb.resetDbInstance();
|
|
fs.rmSync(testDataDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
|
fs.mkdirSync(testDataDir, { recursive: true });
|
|
});
|
|
|
|
after(() => {
|
|
coreDb.resetDbInstance();
|
|
if (fs.existsSync(testDataDir)) {
|
|
fs.rmSync(testDataDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
|
}
|
|
});
|
|
|
|
// ─── Tests ───────────────────────────────────────────────────────────────────
|
|
|
|
describe("CLIProxyAPI fallback wiring", () => {
|
|
// ── resolveCliproxyapiBaseUrl / clearCliproxyapiUrlCache ──────────────────
|
|
|
|
describe("resolveCliproxyapiBaseUrl — real production function", () => {
|
|
const origHost = process.env.CLIPROXYAPI_HOST;
|
|
const origPort = process.env.CLIPROXYAPI_PORT;
|
|
|
|
beforeEach(() => {
|
|
// Clear the module-level URL cache so the function re-evaluates env vars.
|
|
clearCliproxyapiUrlCache();
|
|
});
|
|
|
|
afterEach(() => {
|
|
process.env.CLIPROXYAPI_HOST = origHost;
|
|
process.env.CLIPROXYAPI_PORT = origPort;
|
|
clearCliproxyapiUrlCache();
|
|
});
|
|
|
|
it("falls back to default 127.0.0.1:8317 when env vars are unset", async () => {
|
|
delete process.env.CLIPROXYAPI_HOST;
|
|
delete process.env.CLIPROXYAPI_PORT;
|
|
clearCliproxyapiUrlCache();
|
|
// DB import will fail in test env (no settings table) → falls through to defaults.
|
|
const url = await resolveCliproxyapiBaseUrl();
|
|
assert.equal(url, "http://127.0.0.1:8317");
|
|
});
|
|
|
|
it("respects CLIPROXYAPI_HOST env var", async () => {
|
|
process.env.CLIPROXYAPI_HOST = "10.0.0.1";
|
|
delete process.env.CLIPROXYAPI_PORT;
|
|
clearCliproxyapiUrlCache();
|
|
const url = await resolveCliproxyapiBaseUrl();
|
|
assert.ok(url.startsWith("http://10.0.0.1:"), `Expected host 10.0.0.1, got: ${url}`);
|
|
});
|
|
|
|
it("respects CLIPROXYAPI_PORT env var", async () => {
|
|
delete process.env.CLIPROXYAPI_HOST;
|
|
process.env.CLIPROXYAPI_PORT = "9999";
|
|
clearCliproxyapiUrlCache();
|
|
const url = await resolveCliproxyapiBaseUrl();
|
|
assert.ok(url.endsWith(":9999"), `Expected port 9999, got: ${url}`);
|
|
});
|
|
|
|
it("clearCliproxyapiUrlCache is a real function that forces cache miss", async () => {
|
|
// First call with port A
|
|
process.env.CLIPROXYAPI_PORT = "8001";
|
|
clearCliproxyapiUrlCache();
|
|
const url1 = await resolveCliproxyapiBaseUrl();
|
|
|
|
// Change env — without clearing, cache would still return old value.
|
|
process.env.CLIPROXYAPI_PORT = "8002";
|
|
clearCliproxyapiUrlCache(); // real production call
|
|
const url2 = await resolveCliproxyapiBaseUrl();
|
|
|
|
assert.ok(url1.endsWith(":8001"), `url1 should end with :8001, got: ${url1}`);
|
|
assert.ok(url2.endsWith(":8002"), `url2 should end with :8002 after cache clear, got: ${url2}`);
|
|
assert.notEqual(url1, url2);
|
|
});
|
|
});
|
|
|
|
// ── upsertUpstreamProxyConfig — settings-loop filter behaviour ─────────────
|
|
|
|
describe("settings sync to upstream_proxy_config — real DB functions", () => {
|
|
it("creates rows for each real provider ID", async () => {
|
|
const realProviders = ["anthropic", "openai", "deepseek", "groq"];
|
|
for (const providerId of filterEmbeddedServices(realProviders)) {
|
|
await upstreamProxyDb.upsertUpstreamProxyConfig({
|
|
providerId,
|
|
mode: "fallback",
|
|
enabled: true,
|
|
});
|
|
}
|
|
|
|
for (const id of realProviders) {
|
|
const row = await upstreamProxyDb.getUpstreamProxyConfig(id);
|
|
assert.ok(row, `Expected row for provider=${id}`);
|
|
assert.equal(row.mode, "fallback");
|
|
assert.equal(row.enabled, true);
|
|
}
|
|
});
|
|
|
|
it("filterEmbeddedServices skips cliproxyapi and 9router from the provider loop", () => {
|
|
const mixed = ["anthropic", "cliproxyapi", "9router", "openai"];
|
|
const filtered = filterEmbeddedServices(mixed);
|
|
assert.deepEqual(filtered, ["anthropic", "openai"]);
|
|
assert.ok(!filtered.includes("cliproxyapi"), "cliproxyapi must not appear in filtered list");
|
|
assert.ok(!filtered.includes("9router"), "9router must not appear in filtered list");
|
|
});
|
|
|
|
it("does NOT create a routing row for cliproxyapi via the provider loop", async () => {
|
|
// Simulate the PATCH handler loop: activeProviderIds come from connections
|
|
// but embedded services are filtered before upsert.
|
|
const activeProviderIds = filterEmbeddedServices(["anthropic", "cliproxyapi", "openai"]);
|
|
|
|
for (const providerId of activeProviderIds) {
|
|
await upstreamProxyDb.upsertUpstreamProxyConfig({
|
|
providerId,
|
|
mode: "fallback",
|
|
enabled: true,
|
|
});
|
|
}
|
|
|
|
// The loop must NOT have created a row with providerId='cliproxyapi'.
|
|
const cliproxyRow = await upstreamProxyDb.getUpstreamProxyConfig("cliproxyapi");
|
|
assert.equal(
|
|
cliproxyRow,
|
|
null,
|
|
"The provider loop must NOT create an upstream_proxy_config row for 'cliproxyapi'"
|
|
);
|
|
|
|
// Real provider rows must exist.
|
|
const anthropicRow = await upstreamProxyDb.getUpstreamProxyConfig("anthropic");
|
|
assert.ok(anthropicRow, "anthropic row must exist");
|
|
const openaiRow = await upstreamProxyDb.getUpstreamProxyConfig("openai");
|
|
assert.ok(openaiRow, "openai row must exist");
|
|
});
|
|
|
|
it("disables rows when fallback is turned off (mode becomes native)", async () => {
|
|
await upstreamProxyDb.upsertUpstreamProxyConfig({
|
|
providerId: "anthropic",
|
|
mode: "native",
|
|
enabled: false,
|
|
});
|
|
|
|
const row = await upstreamProxyDb.getUpstreamProxyConfig("anthropic");
|
|
assert.ok(row);
|
|
assert.equal(row.mode, "native");
|
|
assert.equal(row.enabled, false);
|
|
});
|
|
|
|
it("sentinel cliproxyapi row (for model mapping) exists separately from the routing loop", async () => {
|
|
// The PATCH handler creates the cliproxyapi sentinel row AFTER the loop.
|
|
// This is intentional: it stores the global model-mapping blob that
|
|
// GET /api/settings reads back via getUpstreamProxyConfig("cliproxyapi").
|
|
// It is NOT created by the provider loop (which filters it out).
|
|
await upstreamProxyDb.upsertUpstreamProxyConfig({
|
|
providerId: "cliproxyapi",
|
|
mode: "fallback",
|
|
enabled: true,
|
|
cliproxyapiModelMapping: { "ag/gemini-3-pro": "gemini-3-pro-high" },
|
|
});
|
|
|
|
const row = await upstreamProxyDb.getUpstreamProxyConfig("cliproxyapi");
|
|
assert.ok(row, "sentinel cliproxyapi row must exist");
|
|
assert.deepEqual(row.cliproxyapiModelMapping, { "ag/gemini-3-pro": "gemini-3-pro-high" });
|
|
});
|
|
});
|
|
|
|
// ── fallback status code parsing ──────────────────────────────────────────
|
|
|
|
describe("fallback status code parsing (inline, tests parsing logic)", () => {
|
|
function parseFallbackCodes(settingValue: string, defaults: number[]): number[] {
|
|
if (typeof settingValue === "string" && settingValue.trim()) {
|
|
const parsed = settingValue
|
|
.split(",")
|
|
.map((s) => parseInt(s.trim(), 10))
|
|
.filter((n) => !isNaN(n));
|
|
if (parsed.length > 0) return parsed;
|
|
}
|
|
return defaults;
|
|
}
|
|
|
|
it("uses user-configured codes instead of hardcoded values", () => {
|
|
const defaults = [429, 500, 502, 503, 504];
|
|
const codes = parseFallbackCodes("429,500,502", defaults);
|
|
const isRetryable = (s: number) => codes.includes(s) || s === 0;
|
|
|
|
assert.equal(isRetryable(429), true);
|
|
assert.equal(isRetryable(500), true);
|
|
assert.equal(isRetryable(502), true);
|
|
assert.equal(isRetryable(0), true, "network error must always trigger fallback");
|
|
// 503/504 not in user config — must NOT trigger (old bug: || s >= 500 always matched)
|
|
assert.equal(isRetryable(503), false);
|
|
assert.equal(isRetryable(504), false);
|
|
});
|
|
|
|
it("falls back to defaults when settings string is empty", () => {
|
|
const defaults = [429, 500, 502, 503, 504];
|
|
const codes = parseFallbackCodes("", defaults);
|
|
const isRetryable = (s: number) => codes.includes(s) || s === 0;
|
|
|
|
assert.equal(isRetryable(429), true);
|
|
assert.equal(isRetryable(503), true);
|
|
assert.equal(isRetryable(504), true);
|
|
});
|
|
|
|
it("handles a single code in settings", () => {
|
|
const defaults = [429, 500, 502, 503, 504];
|
|
const codes = parseFallbackCodes("429", defaults);
|
|
assert.deepEqual(codes, [429]);
|
|
});
|
|
});
|
|
});
|