mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-23 15:42:12 +03:00
⭐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.
93 lines
3.4 KiB
TypeScript
93 lines
3.4 KiB
TypeScript
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 200–260ms 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`
|
||
);
|
||
});
|