diff --git a/changelog.d/fixes/7812-release-regressions.md b/changelog.d/fixes/7812-release-regressions.md new file mode 100644 index 0000000000..69befb0b9a --- /dev/null +++ b/changelog.d/fixes/7812-release-regressions.md @@ -0,0 +1 @@ +- **fix(runtime):** Apply refreshed request-queue settings, restore inter-batch health-check jitter, and invalidate database read caches when the SQLite singleton resets; refresh stale CI baselines and fixtures exposed by clean release runs ([#7812](https://github.com/diegosouzapw/OmniRoute/pull/7812) by [@backryun](https://github.com/backryun)). diff --git a/open-sse/services/rateLimitManager.ts b/open-sse/services/rateLimitManager.ts index f2391f749d..f01d00a034 100644 --- a/open-sse/services/rateLimitManager.ts +++ b/open-sse/services/rateLimitManager.ts @@ -343,7 +343,10 @@ export async function initializeRateLimits() { try { const { getCachedProviderConnections, getSettings } = await import("@/lib/localDb"); - const [connections, settings] = await Promise.all([getCachedProviderConnections(), getSettings()]); + const [connections, settings] = await Promise.all([ + getCachedProviderConnections(), + getSettings(), + ]); const resilience = resolveResilienceSettings(settings); currentRequestQueueSettings = { ...resilience.requestQueue }; // #6846 Phase 1: operator overrides for header-less providers' static RPM @@ -383,6 +386,7 @@ export async function initializeRateLimits() { } export async function applyRequestQueueSettings(nextSettings: RequestQueueSettings) { + currentRequestQueueSettings = { ...nextSettings }; const { getCachedProviderConnections } = await import("@/lib/localDb"); const connections = await getCachedProviderConnections(); reconcileEnabledConnections(connections as unknown[], currentRequestQueueSettings); diff --git a/scripts/check/compression-budget-baseline.json b/scripts/check/compression-budget-baseline.json index ad57bae285..f948293623 100644 --- a/scripts/check/compression-budget-baseline.json +++ b/scripts/check/compression-budget-baseline.json @@ -51,8 +51,8 @@ "ccr": { "tasks": { "prose": 129, - "tool-output": 56, - "json": 14 + "tool-output": 81, + "json": 64 } } } diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index b71da370be..1aee10ba13 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -7243,6 +7243,7 @@ "restartServerWithNewPassword": "Restart the server - it will use the new password", "backToLogin": "Back to Login", "forgotPassword": "Forgot password?", + "continueWithOidc": "Continue with OIDC", "defaultPasswordHint": "Default password: CHANGEME (unless INITIAL_PASSWORD was set)", "Authorization": "Authorization", "Content-Disposition": "Content-Disposition", diff --git a/src/i18n/messages/pt-BR.json b/src/i18n/messages/pt-BR.json index 0b6e6f3106..c2620de998 100644 --- a/src/i18n/messages/pt-BR.json +++ b/src/i18n/messages/pt-BR.json @@ -7243,6 +7243,7 @@ "restartServerWithNewPassword": "Reinicie o servidor - ele usará a nova senha", "backToLogin": "Voltar para o Login", "forgotPassword": "Esqueceu a senha?", + "continueWithOidc": "Continuar com OIDC", "defaultPasswordHint": "Default password: CHANGEME (unless INITIAL_PASSWORD was set)", "Authorization": "Autorização", "Content-Disposition": "Disposição de conteúdo", diff --git a/src/i18n/messages/vi.json b/src/i18n/messages/vi.json index 3518e52fda..3ea2b92f35 100644 --- a/src/i18n/messages/vi.json +++ b/src/i18n/messages/vi.json @@ -7275,6 +7275,7 @@ "restartServerWithNewPassword": "Khởi động lại máy chủ - máy chủ sẽ sử dụng mật khẩu mới", "backToLogin": "Quay lại Đăng nhập", "forgotPassword": "Quên mật khẩu?", + "continueWithOidc": "Tiếp tục với OIDC", "defaultPasswordHint": "Mật khẩu mặc định: CHANGEME (trừ khi INITIAL_PASSWORD đã được đặt)", "Authorization": "Authorization", "Content-Disposition": "Content-Disposition", diff --git a/src/lib/db/core.ts b/src/lib/db/core.ts index 39e5397752..ed69172172 100644 --- a/src/lib/db/core.ts +++ b/src/lib/db/core.ts @@ -814,7 +814,6 @@ function offloadLegacyCallLogDetails(db: SqliteDatabase) { } } - function shouldRunStartupDbHealthCheck(): boolean { if (process.env.OMNIROUTE_FORCE_DB_HEALTHCHECK === "1") return true; return !isAutomatedTestProcess(); @@ -1313,6 +1312,10 @@ export function closeDbInstance(options?: { checkpointMode?: CheckpointMode | nu */ export function resetDbInstance() { closeDbInstance(); + // Read caches outlive the SQLite singleton. A reset swaps the backing + // database, so retaining cached rows can leak the previous database's + // connections/settings into the newly opened instance (tests and restore). + invalidateDbCache(); } // ──────────────── Runtime Driver Info ──────────────── diff --git a/src/lib/tokenHealthCheck.ts b/src/lib/tokenHealthCheck.ts index 561cc8ea02..f57c012d30 100644 --- a/src/lib/tokenHealthCheck.ts +++ b/src/lib/tokenHealthCheck.ts @@ -350,7 +350,10 @@ export async function sweep() { // prevent sustained bursting while reducing total sweep duration. if (batchEnd < total) { if (staggerMs > 0) { - await new Promise((resolve) => setTimeout(resolve, staggerMs)); + const jitterMin = parseInt(process.env.HEALTHCHECK_JITTER_MIN_MS || "500", 10); + const jitterMax = parseInt(process.env.HEALTHCHECK_JITTER_MAX_MS || "5000", 10); + const jitter = jitterMin + Math.random() * Math.max(0, jitterMax - jitterMin); + await new Promise((resolve) => setTimeout(resolve, staggerMs + jitter)); } // Yield a microtask so the event loop can service pending I/O // (DB contention, network responses) before the next batch starts. diff --git a/tests/snapshots/provider/translate-path.json b/tests/snapshots/provider/translate-path.json index 0656be2a47..fe232779cf 100644 --- a/tests/snapshots/provider/translate-path.json +++ b/tests/snapshots/provider/translate-path.json @@ -1876,6 +1876,54 @@ "stream": "https://gemini.google.com/app" } }, + "ghe-copilot": { + "format": "openai", + "headers": { + "apiKey": { + "Accept": "text/event-stream", + "Authorization": "Bearer ", + "Content-Type": "application/json", + "X-Initiator": "user", + "copilot-integration-id": "vscode-chat", + "editor-plugin-version": "copilot-chat/0.54.0", + "editor-version": "vscode/1.126.0", + "openai-intent": "conversation-panel", + "user-agent": "GitHubCopilotChat/0.54.0", + "x-github-api-version": "2026-06-01", + "x-vscode-user-agent-library-version": "electron-fetch" + }, + "nonStream": { + "Accept": "application/json", + "Authorization": "Bearer ", + "Content-Type": "application/json", + "X-Initiator": "user", + "copilot-integration-id": "vscode-chat", + "editor-plugin-version": "copilot-chat/0.54.0", + "editor-version": "vscode/1.126.0", + "openai-intent": "conversation-panel", + "user-agent": "GitHubCopilotChat/0.54.0", + "x-github-api-version": "2026-06-01", + "x-vscode-user-agent-library-version": "electron-fetch" + }, + "oauth": { + "Accept": "text/event-stream", + "Authorization": "Bearer ", + "Content-Type": "application/json", + "X-Initiator": "user", + "copilot-integration-id": "vscode-chat", + "editor-plugin-version": "copilot-chat/0.54.0", + "editor-version": "vscode/1.126.0", + "openai-intent": "conversation-panel", + "user-agent": "GitHubCopilotChat/0.54.0", + "x-github-api-version": "2026-06-01", + "x-vscode-user-agent-library-version": "electron-fetch" + } + }, + "url": { + "nonStream": "https://api.githubcopilot.com/chat/completions", + "stream": "https://api.githubcopilot.com/chat/completions" + } + }, "gigachat": { "format": "openai", "headers": { diff --git a/tests/unit/apikey-connection-health-check.test.ts b/tests/unit/apikey-connection-health-check.test.ts index 582f0ad50f..572e10b161 100644 --- a/tests/unit/apikey-connection-health-check.test.ts +++ b/tests/unit/apikey-connection-health-check.test.ts @@ -172,7 +172,7 @@ test("connection with both apiKey and refreshToken: refresh path is tried", asyn ); }); -test("sweep processes all connections with stagger + jitter delay", async () => { +test("sweep processes all connections with inter-batch stagger + jitter delay", async () => { await resetStorage(); // Create multiple connections; set isActive=false so checkConnection @@ -201,6 +201,16 @@ test("sweep processes all connections with stagger + jitter delay", async () => refreshToken: "test-rt", isActive: false, }); + for (let i = 4; i <= 21; i++) { + await providersDb.createProviderConnection({ + provider: "openai", + authType: "oauth", + name: `Stagger Test ${i}`, + email: `t${i}@example.com`, + refreshToken: "test-rt", + isActive: false, + }); + } // Clear any health-check skip config const origSetting = process.env.HEALTHCHECK_SKIP_PROVIDERS; @@ -219,13 +229,13 @@ test("sweep processes all connections with stagger + jitter delay", async () => await sweep(); const elapsed = Date.now() - start; - // 3 connections -> 2 gaps. Each gap waits + // 21 connections -> two batches (20 + 1) and one inter-batch gap. The gap waits // HEALTHCHECK_STAGGER_MS (1ms) + HEALTHCHECK_JITTER_MIN_MS (100ms) = 101ms. - // Without jitter: 2 * 1ms = ~2ms. With jitter: 2 * 101ms = ~202ms. + // Without jitter the only gap would be ~1ms. // The assert proves the jitter is actually applied. assert.ok( - elapsed >= 190, - `sweep took ${elapsed}ms — expected >= 190ms with jitter applied (jitter-free baseline would be ~2ms)` + elapsed >= 90, + `sweep took ${elapsed}ms — expected >= 90ms with jitter applied (jitter-free baseline would be ~1ms)` ); // Verify all connections still exist (sweep didn't error out mid-loop) diff --git a/tests/unit/combo-custom-provider-resolution.test.ts b/tests/unit/combo-custom-provider-resolution.test.ts index cf95a6934d..cf2108be9b 100644 --- a/tests/unit/combo-custom-provider-resolution.test.ts +++ b/tests/unit/combo-custom-provider-resolution.test.ts @@ -171,8 +171,8 @@ test("custom provider auth lookup search pool maps alias prefixes to internal pr ); assert.match( authSrc, - /getProviderNodes\(/, - "auth lookup should read provider_nodes to map custom prefixes like 78code/micu back to internal provider ids" + /getCachedProviderNodes\(/, + "auth lookup should read cached provider_nodes to map custom prefixes like 78code/micu back to internal provider ids" ); assert.match( authSrc, diff --git a/tests/unit/db-read-cache.test.ts b/tests/unit/db-read-cache.test.ts index a98bff5aeb..55181dba2e 100644 --- a/tests/unit/db-read-cache.test.ts +++ b/tests/unit/db-read-cache.test.ts @@ -142,6 +142,21 @@ test("getCachedProviderConnections caches only the unfiltered query", async () = assert.equal((await readCache.getCachedProviderConnections()).length, 2); }); +test("resetDbInstance invalidates provider connection read caches", async () => { + await providersDb.createProviderConnection({ + provider: "openai", + authType: "apikey", + name: "Reset Cache Test", + apiKey: "sk-reset-cache", + }); + + assert.equal((await providersDb.getProviderConnections()).length, 1); + + await resetStorage(); + + assert.equal((await providersDb.getProviderConnections()).length, 0); +}); + test("cached LKGP values refresh only after the specific key is invalidated", async () => { const readCache = await importFresh("src/lib/db/readCache.ts"); const db = core.getDbInstance(); diff --git a/tests/unit/mitm-targets-resolve.test.ts b/tests/unit/mitm-targets-resolve.test.ts index ea14b8b2bf..b9986b3936 100644 --- a/tests/unit/mitm-targets-resolve.test.ts +++ b/tests/unit/mitm-targets-resolve.test.ts @@ -32,13 +32,14 @@ test("resolveTarget — unknown host returns null", () => { assert.equal(resolveTarget(""), null); }); -test("ALL_TARGETS — registers exactly nine targets", () => { - assert.equal(ALL_TARGETS.length, 9); +test("ALL_TARGETS — registers exactly ten targets", () => { + assert.equal(ALL_TARGETS.length, 10); const ids = new Set(ALL_TARGETS.map((t) => t.id)); - assert.equal(ids.size, 9); + assert.equal(ids.size, 10); assert.ok(ids.has("antigravity")); assert.ok(ids.has("kiro")); assert.ok(ids.has("copilot")); + assert.ok(ids.has("ghe-copilot")); assert.ok(ids.has("codex")); assert.ok(ids.has("cursor")); assert.ok(ids.has("zed")); diff --git a/tests/unit/repro-7764-collapsed-quota-order.test.ts b/tests/unit/repro-7764-collapsed-quota-order.test.ts index 09df08f8e7..599435fe28 100644 --- a/tests/unit/repro-7764-collapsed-quota-order.test.ts +++ b/tests/unit/repro-7764-collapsed-quota-order.test.ts @@ -6,6 +6,8 @@ import { hasFixedQuotaOrder, } from "@/app/(dashboard)/dashboard/usage/components/ProviderLimits/quotaParsing"; +const quotaName = (quota: { name: string }) => quota.name; + test("#7764 sanity: codex has a fixed quota order (session, weekly)", () => { assert.equal(hasFixedQuotaOrder("codex"), true); }); @@ -25,16 +27,10 @@ test("#7764: topQuotas() (collapsed card order) respects hasFixedQuotaOrder inst }; const parsedA = parseQuotaData("codex", rawA); const parsedB = parseQuotaData("codex", rawB); - assert.deepEqual( - parsedA.map((q: any) => q.name), - ["session", "weekly"] - ); - assert.deepEqual( - parsedB.map((q: any) => q.name), - ["session", "weekly"] - ); - const renderedA = topQuotas(parsedA, 3, "codex").map((q: any) => q.name); - const renderedB = topQuotas(parsedB, 3, "codex").map((q: any) => q.name); + assert.deepEqual(parsedA.map(quotaName), ["session", "weekly"]); + assert.deepEqual(parsedB.map(quotaName), ["session", "weekly"]); + const renderedA = topQuotas(parsedA, 3, "codex").map(quotaName); + const renderedB = topQuotas(parsedB, 3, "codex").map(quotaName); assert.deepEqual(renderedA, ["session", "weekly"]); assert.deepEqual(renderedB, ["session", "weekly"]); }); @@ -45,6 +41,6 @@ test("#7764: providers WITHOUT a fixed order still sort worst-status-first (no r { name: "beta", used: 95, total: 100, remainingPercentage: 5 }, { name: "gamma", used: 50, total: 100, remainingPercentage: 50 }, ]; - const rendered = topQuotas(quotas, 3, "some-other-provider").map((q: any) => q.name); + const rendered = topQuotas(quotas, 3, "some-other-provider").map(quotaName); assert.deepEqual(rendered, ["beta", "gamma", "alpha"]); }); diff --git a/tests/unit/route-edge-coverage.test.ts b/tests/unit/route-edge-coverage.test.ts index 4f765cd84e..3788227f08 100644 --- a/tests/unit/route-edge-coverage.test.ts +++ b/tests/unit/route-edge-coverage.test.ts @@ -1202,7 +1202,7 @@ test("embeddings route handles responses provider nodes, invalid local nodes, an ); assert.equal(localResponse.status, 200); assert.equal(fetchCalls[0].url, "http://localhost:7790/v1/embeddings"); - + localDb.invalidateDbCache("nodes"); const remoteResponse = await withPrepareOverride( "SELECT * FROM provider_nodes", ({ statement }) => diff --git a/tests/unit/token-health-check-sweep.test.ts b/tests/unit/token-health-check-sweep.test.ts index 841d6d250b..87e2d548a4 100644 --- a/tests/unit/token-health-check-sweep.test.ts +++ b/tests/unit/token-health-check-sweep.test.ts @@ -21,7 +21,7 @@ * and tests/unit/rule12-error-sanitization-sweep.test.ts). Connections are * created with healthCheckInterval: 0 so checkConnection() is a fast no-op * (returns before any network/DB-write work), letting the test control - * timing purely via the inter-connection HEALTHCHECK_STAGGER_MS delay. + * timing purely via the inter-batch HEALTHCHECK_STAGGER_MS delay. */ import test from "node:test"; import assert from "node:assert/strict"; @@ -61,13 +61,15 @@ async function resetStorage() { test.after(async () => { core.resetDbInstance(); fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); + delete process.env.HEALTHCHECK_STAGGER_MS; + delete process.env.HEALTHCHECK_JITTER_MIN_MS; + delete process.env.HEALTHCHECK_JITTER_MAX_MS; }); /** Read the shared health-check global state's `sweeping` flag. */ function isSweeping(): boolean { const hc = (globalThis as Record).__omnirouteTokenHC as - | { sweeping?: boolean } - | undefined; + { sweeping?: boolean } | undefined; return hc?.sweeping ?? false; } @@ -87,10 +89,12 @@ async function createNoOpOauthConnections(count: number, namePrefix: string) { test("sweep() skips re-entrant calls while a previous sweep is still in flight", async () => { await resetStorage(); process.env.HEALTHCHECK_STAGGER_MS = "300"; - await createNoOpOauthConnections(4, "reentrancy"); + process.env.HEALTHCHECK_JITTER_MIN_MS = "0"; + process.env.HEALTHCHECK_JITTER_MAX_MS = "0"; + await createNoOpOauthConnections(21, "reentrancy"); const seeded = await providersDb.getProviderConnections({ authType: "oauth" }); - assert.equal(seeded.length, 4, "precondition: 4 oauth connections exist"); + assert.equal(seeded.length, 21, "precondition: 21 oauth connections exist"); assert.equal(isSweeping(), false, "precondition: no sweep in flight yet"); const first = sweep(); @@ -103,9 +107,9 @@ test("sweep() skips re-entrant calls while a previous sweep is still in flight", await sweep(); // second, concurrent call — must be skipped by the guard const elapsedMs = Date.now() - start; - // With 4 connections and a 300ms inter-connection stagger, a REAL second - // sweep would take >= 300ms just to clear a single stagger gap (and up to - // ~900ms for all three gaps). The guard must make this call return near- + // With 21 connections and a 300ms inter-batch stagger, a REAL second + // sweep would take >= 300ms to clear the single batch gap. The guard must + // make this call return near- // instantly instead of re-entering the loop. assert.ok( elapsedMs < 250,