Files
OmniRoute/tests/unit/9147-catalog-eventloop-yield.test.ts
Ravi Tharuma 137e49e393 feat(search): first-class X Search via SuperGrok x_search (#10988)
5 — Provider x-search de primeira classe (SuperGrok/xAI x_search) em POST /v1/search e MCP omniroute_x_search. Fallback de credenciais xai-oauth→xao→xai; distinto de web search e do X Developer MCP.

Reconciliado com o release tip (que já incluía #10981 "skip catalog-default SearXNG" deste mesmo lote): merge trouxe 5 conflitos reais de contagem gerada (llm.txt/README.md/AGENTS.md/PROVIDER_REFERENCE.md/SVGs/46 mirrors i18n, todos verificados como bump puro 347→348, sem perda de conteúdo do HEAD) + 1 conflito real de mergeable=CONFLICTING.

Durante a validação, os 3 testes novos de SearXNG expuseram um bug real de interação com #10981: `isUnconfiguredLoopbackSearchProvider()` checava o baseUrl ESTÁTICO do catálogo em vez do baseUrl efetivo (após override de `provider_options.baseUrl` ou `providerSpecificData.baseUrl` da conexão), então QUALQUER request a searxng-search — mesmo com override customizado — era rejeitado como se fosse o default não-configurado. Corrigido em `open-sse/handlers/search.ts` (resolve o baseUrl efetivo via `resolveSearchBaseUrl()` antes do skip-check, tanto para o provider primário quanto o alternate). Um teste do próprio #10988 que assumia o comportamento pré-#10981 (default localhost:8888 sempre atendido) foi atualizado para refletir o comportamento já mesclado e intencional (503 quando não configurado).

Validação completa: typecheck limpo, 70/70 testes unit (search-route/search-registry/x-search-provider/searxng-loopback-default), 24/24 vitest MCP, 14/14 integration (search-providers-catalog), lint limpo nos arquivos tocados, docs-counts-sync OK (2 drifts soft pré-existentes, não relacionados), gates estáticos (file-size/complexity/cognitive/dead-code/changelog) todos OK.
2026-08-21 14:16:42 -03:00

93 lines
3.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-9147-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "catalog-9147-test-secret";
const core = await import("../../src/lib/db/core.ts");
const apiKeysDb = await import("../../src/lib/db/apiKeys.ts");
const v1ModelsCatalog = await import("../../src/app/api/v1/models/catalog.ts");
const CONNECTION_COUNT = 60;
const MODELS_PER_CONNECTION = 12; // ~720 synced models total
async function resetStorage() {
core.resetDbInstance();
apiKeysDb.resetApiKeyState();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
v1ModelsCatalog.__resetCatalogBuilderRunsForTest();
}
async function seedCatalogScaleDataset() {
const db = core.getDbInstance();
const now = new Date().toISOString();
const insertConn = db.prepare(
`INSERT INTO provider_connections (id, provider, auth_type, name, priority, is_active, api_key, created_at, updated_at)
VALUES (?, 'openai-compatible', 'apikey', ?, ?, 1, ?, ?, ?)`
);
const insertModels = db.prepare(
`INSERT INTO key_value (namespace, key, value) VALUES ('syncedAvailableModels', ?, ?)`
);
const seedTx = db.transaction(() => {
for (let i = 0; i < CONNECTION_COUNT; i++) {
const id = `probe-conn-${i}`;
insertConn.run(id, `probe-connection-${i}`, i, `sk-probe-${i}`, now, now);
const models = Array.from({ length: MODELS_PER_CONNECTION }, (_, m) => ({
id: `probe-model-${i}-${m}`,
name: `Probe Model ${i}-${m}`,
contextLength: 128000,
}));
insertModels.run(`openai-compatible:${id}`, JSON.stringify(models));
}
});
seedTx();
}
test.beforeEach(async () => {
await resetStorage();
});
test.after(async () => {
core.resetDbInstance();
apiKeysDb.resetApiKeyState();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
test("#9147 — catalog build at catalog-scale must not pin the event loop for a long stretch", async () => {
await seedCatalogScaleDataset();
const req = new Request("http://localhost/v1/models");
let settled = false;
const buildPromise = v1ModelsCatalog.getUnifiedModelsResponse(req).then((res) => {
settled = true;
return res;
});
let lastTick = performance.now();
let maxGapMs = 0;
let ticks = 0;
while (!settled) {
await new Promise((resolve) => setTimeout(resolve, 0));
const now = performance.now();
maxGapMs = Math.max(maxGapMs, now - lastTick);
lastTick = now;
ticks++;
if (ticks > 20000) break;
}
const res = await buildPromise;
assert.equal(res.status, 200);
// 150ms is tight on GitHub-hosted unit shards (`--test-concurrency=4`):
// sibling tests share the event loop, so a healthy yielding builder still
// records 200260ms gaps. 400ms still fails a true pin (seconds) while
// absorbing shard contention. Observed CI: 252.5ms on run 32494847431.
assert.ok(
maxGapMs < 400,
`event loop was blocked for ${maxGapMs.toFixed(1)}ms in a single stretch while building the ` +
`catalog for ${CONNECTION_COUNT} connections / ${CONNECTION_COUNT * MODELS_PER_CONNECTION} models ` +
`(${ticks} interleaved ticks observed) — the builder is not yielding to the event loop`
);
});