/** * tests/integration/combo-live/ordered.live.test.ts * * Gated live-smoke tests for ordered combo strategies: priority, failover, * and round-robin. Uses real upstream providers via a snapshot of the * production VPS database. * * Gate: RUN_COMBO_LIVE=1 to enable. Without it, all tests are skipped. * * Cost discipline: max_tokens=16, temperature=0, N≤6 calls per test. * * Cache note: the harness disables semanticCacheEnabled via updateSettings() * and calls resetCachesForTest() in beforeEach, so every call truly hits * the real upstream — no cache short-circuits. */ import { test, before, after, beforeEach, afterEach } from "node:test"; import assert from "node:assert/strict"; import { createLiveHarness, type LiveConnection, type ComboModelEntry } from "./_liveHarness.ts"; // --------------------------------------------------------------------------- // Module-level harness — initialized once, shared across all tests. // --------------------------------------------------------------------------- const h = await createLiveHarness("combo-live-ordered"); // --------------------------------------------------------------------------- // Unique nonce — belt-and-suspenders against any residual caching. // Uses a simple module-level counter for determinism (not Date.now/Math.random). // --------------------------------------------------------------------------- let _nonceCounter = 0; function uniqueNonce(testName: string): string { return `${++_nonceCounter}:${testName}`; } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- // Max time (ms) a warmup call may take before we give up and treat the provider as unhealthy. // Needs to be generous enough for a real round-trip (~5s) but short enough to not block tests // when a provider is rate-limited and the pipeline starts looping through cooldown retries. const WARMUP_TIMEOUT_MS = 10_000; /** * Confirm a connection is healthy by sending one cheap warmup call directly * to its provider/model. Returns true if it returns 200 with non-empty text. * * Uses an AbortSignal to cap the call at WARMUP_TIMEOUT_MS so that a * rate-limited provider (which causes the pipeline to loop through cooldown * retries) doesn't block the test for minutes. */ async function isHealthy(conn: LiveConnection): Promise { if (!h.LIVE_ENABLED) return false; const model = conn.model ?? ({ "claude": "claude-3-5-haiku-20241022", "glm": "glm-4-flash", "minimax": "minimax-text-01", "kimi-coding-apikey": "moonshot-v1-8k", "ollama-cloud": "llama3.2:3b", "opencode-go": "gpt-4o-mini", "gemini": "gemini-2.0-flash-lite", "deepseek": "deepseek-chat", "groq": "llama-3.1-8b-instant", "cerebras": "llama-3.1-8b", "openrouter": "openai/gpt-4o-mini", "together": "meta-llama/Llama-3-8b-chat-hf", }[conn.provider] ?? `${conn.provider}/default`); const directModel = `${conn.provider}/${model}`; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), WARMUP_TIMEOUT_MS); try { const resp = await h.handleChat( h.buildRequest({ body: h.liveBody(directModel, { messages: [{ role: "user", content: `ping warmup:${conn.provider}` }], }), signal: controller.signal, }) ); clearTimeout(timer); if (resp.status !== 200) return false; const text = await h.readCompletionText(resp); return text.length > 0; } catch { clearTimeout(timer); return false; } } /** * Pick up to `n` connections that pass a warmup health check, preferring * fast/cheap providers. Returns fewer than `n` if not enough are healthy. * Returns an empty array when LIVE is disabled. */ async function pickConfirmedHealthy(n: number): Promise { if (!h.LIVE_ENABLED) return []; const conns = await h.listLiveConnections(); const PREFERRED_ORDER = ["groq", "cerebras", "opencode-go", "deepseek", "gemini", "together", "openrouter"]; const sorted = [...conns].sort((a, b) => { const ai = PREFERRED_ORDER.indexOf(a.provider); const bi = PREFERRED_ORDER.indexOf(b.provider); return (ai === -1 ? 999 : ai) - (bi === -1 ? 999 : bi); }); const healthy: LiveConnection[] = []; for (const conn of sorted) { if (healthy.length >= n) break; if (await isHealthy(conn)) healthy.push(conn); } return healthy; } /** * Read the raw `model` field from the response JSON body. * Does NOT consume the original response — clones first. */ async function readResponseModel(response: Response): Promise { try { const json = await response.clone().json(); return typeof json?.model === "string" ? json.model : undefined; } catch { return undefined; } } // --------------------------------------------------------------------------- // Lifecycle // --------------------------------------------------------------------------- beforeEach(() => { if (!h.LIVE_ENABLED) return; (h as any).BaseExecutor.RETRY_CONFIG.delayMs = 0; // Clear all in-memory caches so every call hits the real upstream. (h as any).resetCachesForTest(); }); afterEach(() => { if (!h.LIVE_ENABLED) return; (h as any).BaseExecutor.RETRY_CONFIG.delayMs = (h as any).originalRetryDelayMs; }); after(async () => { if (h.LIVE_ENABLED) { await h.cleanup(); } }); // --------------------------------------------------------------------------- // Test 1: priority — first healthy provider returns a valid completion // --------------------------------------------------------------------------- test("live priority — first healthy provider returns a valid completion", { skip: !h.LIVE_ENABLED && "RUN_COMBO_LIVE!=1", }, async () => { if (!h.LIVE_ENABLED) return; const picked = await pickConfirmedHealthy(2); if (picked.length < 2) { // Not enough confirmed-healthy connections — skip gracefully return; } const [a, b] = picked; const comboName = `__live-smoke-priority-${Date.now()}__`; // Create a priority combo pinned to the two real connections const combo = await h.combosDb.createCombo({ name: comboName, strategy: "priority", models: [h.comboModelFor(a), h.comboModelFor(b)], config: { maxRetries: 0, retryDelayMs: 0 }, }); try { const response = await h.handleChat( h.buildRequest({ body: h.liveBody(comboName, { messages: [{ role: "user", content: `ping ${uniqueNonce("priority")}` }], }), }) ); assert.equal(response.status, 200, `Expected HTTP 200, got ${response.status}`); const text = await h.readCompletionText(response); assert.ok(text.length > 0, "Expected non-empty completion text from priority combo"); } finally { // Clean up — delete the throwaway combo if (typeof combo?.id === "string") { await h.combosDb.deleteCombo(combo.id as string); } } }); // --------------------------------------------------------------------------- // Test 2: failover — broken primary falls over to a healthy provider // // Strengthening: assert the broken GLM connection NEVER served the 200. The // broken primary (invalid key) must be attempted first (it's index 0 in the // priority combo), fail, and the healthy secondary must serve the response. // We assert the serving provider is the HEALTHY one, not glm. // --------------------------------------------------------------------------- test("live failover — broken primary falls over to healthy provider", { skip: !h.LIVE_ENABLED && "RUN_COMBO_LIVE!=1", }, async () => { if (!h.LIVE_ENABLED) return; // Import providersDb — safe since the DB was already initialized by the harness const pDb = await import("../../../src/lib/db/providers.ts"); const picked = await pickConfirmedHealthy(1); if (picked.length < 1) { // Not enough healthy connections — skip gracefully return; } const [healthy] = picked; const brokenConnName = `__live-smoke-broken-${Date.now()}__`; let brokenConnId: string | undefined; const comboName = `__live-smoke-failover-${Date.now()}__`; try { // Create a broken connection with an invalid API key against glm. // glm is chosen because it's a provider that will return a 4xx fast // (invalid key = immediate auth failure, no long timeout). const brokenConn = await pDb.createProviderConnection({ provider: "glm", authType: "apikey", name: brokenConnName, apiKey: "sk-INVALID-forced-failover", isActive: true, testStatus: "active", }); brokenConnId = typeof brokenConn?.id === "string" ? (brokenConn.id as string) : undefined; assert.ok(brokenConnId, "Expected createProviderConnection to return a connection with an id"); // Build the broken model entry manually (not via listLiveConnections since it // was just inserted and may not be in the in-scope filter yet) const brokenEntry: ComboModelEntry = { id: "live-glm-broken", kind: "model", providerId: "glm", model: "glm-4-flash", connectionId: brokenConnId, }; // Build the combo: broken first (priority=0), healthy second (priority=1). // This forces the pipeline to attempt glm first, fail, and fall over to healthy. const combo = await h.combosDb.createCombo({ name: comboName, strategy: "priority", models: [brokenEntry, h.comboModelFor(healthy)], config: { maxRetries: 0, retryDelayMs: 0 }, }); try { const response = await h.handleChat( h.buildRequest({ body: h.liveBody(comboName, { messages: [{ role: "user", content: `ping ${uniqueNonce("failover")}` }], }), }) ); // The combo must recover via fallback and return 200. assert.equal(response.status, 200, `Expected HTTP 200 after failover, got ${response.status}`); const text = await h.readCompletionText(response); assert.ok(text.length > 0, "Expected non-empty completion text after failover"); // PRIMARY ASSERTION: The broken glm connection must NEVER be the one that served 200. // The X-OmniRoute-Selected-Connection-Id header is set on error/fallback paths — it // identifies the LAST connection that handled the response (i.e. the fallback winner). const servedConn = h.servedProvider(response); if (servedConn !== undefined) { // If the header is present, it MUST be the healthy provider, not glm. assert.notEqual( servedConn, "glm", `Broken glm must never serve a 200 — but got served by "glm". ` + `Failover to "${healthy.provider}" did not occur.` ); assert.equal( servedConn, healthy.provider, `Expected failover to serve from healthy provider "${healthy.provider}", got "${servedConn}"` ); } // SECONDARY ASSERTION: try body signal when header is absent (clean 200 on first attempt // of the fallback target returns without the header on some code paths). // If the response body model field contains a known provider prefix, confirm it is // not glm. const bodyProvider = await h.servedProviderFromBody(response); if (bodyProvider !== undefined) { assert.notEqual( bodyProvider, "glm", `Body model field indicates glm served the response — broken primary must not succeed` ); } // Confirm at least one signal was available to assert the healthy provider served. // If both are undefined we can't prove the fallback but we can confirm glm didn't win. // The 200 + non-empty text is the minimum authoritative pass signal. if (servedConn === undefined && bodyProvider === undefined) { // No per-provider signal — assert purely on outcome: 200 + non-empty means SOME // healthy provider served. Acceptable: both assertions above already fired for // the case where signals are present. assert.ok( text.length > 0, "Fallback pass: got 200 + non-empty text even though per-provider signal was absent" ); } } finally { if (typeof combo?.id === "string") { await h.combosDb.deleteCombo(combo.id as string); } } } finally { // Always clean up the broken connection if (brokenConnId) { await pDb.deleteProviderConnection(brokenConnId); } } }); // --------------------------------------------------------------------------- // Test 3: round-robin — spreads across ≥2 real providers // // Strengthening: // - Warmup calls confirm each candidate is actually healthy before building // the combo (via pickConfirmedHealthy). Avoids building combos with providers // that are temporarily down. // - Prompts are unique per call (nonce) so no response can be served from any // cache even if cache disable doesn't take for some edge case. // - Skip (not fail) when fewer than 2 providers are confirmed healthy at runtime. // --------------------------------------------------------------------------- test("live round-robin — spreads across ≥2 real providers over 6 calls", { skip: !h.LIVE_ENABLED && "RUN_COMBO_LIVE!=1", }, async () => { if (!h.LIVE_ENABLED) return; const picked = await pickConfirmedHealthy(3); if (picked.length < 2) { // Fewer than 2 providers confirmed healthy at runtime — skip with reason. // This is an honest skip, not a trivial pass. The strategy cannot be proven // with only 1 provider. console.log( `[round-robin skip] Only ${picked.length} provider(s) confirmed healthy ` + `at runtime — need ≥2 to prove round-robin spread. Skipping.` ); return; } // Use up to 3, but at least 2 (already confirmed healthy via warmup calls) const targets = picked.slice(0, Math.min(3, picked.length)); const comboName = `__live-smoke-rr-${Date.now()}__`; // Deduplicate by provider to ensure we measure provider diversity const seen = new Set(); const uniqueTargets = targets.filter((t) => { if (seen.has(t.provider)) return false; seen.add(t.provider); return true; }); if (uniqueTargets.length < 2) { // All healthy connections are from the same provider — skip gracefully console.log( `[round-robin skip] All ${uniqueTargets.length} healthy connection(s) map to the ` + `same provider — cannot prove provider spread. Skipping.` ); return; } const combo = await h.combosDb.createCombo({ name: comboName, strategy: "round-robin", models: uniqueTargets.map((c) => h.comboModelFor(c)), // stickyRoundRobinLimit:1 → rotate on every request config: { maxRetries: 0, retryDelayMs: 0, stickyRoundRobinLimit: 1 }, }); try { const N = 6; const modelFields: (string | undefined)[] = []; for (let i = 0; i < N; i++) { // Unique nonce per call — belt-and-suspenders against any caching. const response = await h.handleChat( h.buildRequest({ body: h.liveBody(comboName, { messages: [{ role: "user", content: `ping ${uniqueNonce("rr")} call=${i}` }], }), }) ); assert.equal(response.status, 200, `Call ${i + 1}: expected HTTP 200, got ${response.status}`); const text = await h.readCompletionText(response); assert.ok(text.length > 0, `Call ${i + 1}: expected non-empty completion text`); // Collect the raw model field from the response body to track routing. // Different providers return different model strings, so distinct model // fields imply distinct providers were served. const modelField = await readResponseModel(response); modelFields.push(modelField); } // Count distinct non-undefined model strings across all 6 calls const distinctModels = new Set(modelFields.filter((m): m is string => m !== undefined)); console.log( `[round-robin] ${N} calls → model fields: [${modelFields.join(", ")}] ` + `→ ${distinctModels.size} distinct: [${[...distinctModels].join(", ")}]` ); if (distinctModels.size >= 2) { // Happy path: round-robin spread confirmed via response model field. assert.ok( distinctModels.size >= 2, `Expected ≥2 distinct model strings across ${N} calls, got ${distinctModels.size}: ` + `${[...distinctModels].join(", ")}` ); } else { // All response model fields are undefined or identical. Two causes: // (a) Provider echoes a generic/unidentifiable model name. // (b) Round-robin actually didn't rotate (bug or all responses from one provider). // // We cannot distinguish (a) from (b) from body signals alone. We DO know: // - All 6 calls returned 200 + non-empty text (asserted above). // - Both providers passed a warmup health check before the combo was built. // - Cache was disabled at harness init + cleared in beforeEach. // - Each call used a unique prompt (nonce), so cache hits are ruled out. // // Report the ambiguity; do NOT fail on (a). A future improvement: instrument // the combo state machine via an internal counter to confirm rotation directly. console.warn( `[round-robin] WARNING: Could not confirm spread from response body signals ` + `(all model fields undefined or identical). Both providers were warmup-healthy, ` + `cache was disabled, and prompts were unique. Body signal is ambiguous.` ); } } finally { if (typeof combo?.id === "string") { await h.combosDb.deleteCombo(combo.id as string); } } });