mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
fix(ci): repair release regressions exposed by clean runs (#7812)
This commit is contained in:
1
changelog.d/fixes/7812-release-regressions.md
Normal file
1
changelog.d/fixes/7812-release-regressions.md
Normal file
@@ -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)).
|
||||
@@ -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);
|
||||
|
||||
@@ -51,8 +51,8 @@
|
||||
"ccr": {
|
||||
"tasks": {
|
||||
"prose": 129,
|
||||
"tool-output": 56,
|
||||
"json": 14
|
||||
"tool-output": 81,
|
||||
"json": 64
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 ────────────────
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1876,6 +1876,54 @@
|
||||
"stream": "https://gemini.google.com/app"
|
||||
}
|
||||
},
|
||||
"ghe-copilot": {
|
||||
"format": "openai",
|
||||
"headers": {
|
||||
"apiKey": {
|
||||
"Accept": "text/event-stream",
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"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 <TOK>",
|
||||
"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 <TOK>",
|
||||
"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": {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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"]);
|
||||
});
|
||||
|
||||
@@ -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 }) =>
|
||||
|
||||
@@ -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<string, unknown>).__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,
|
||||
|
||||
Reference in New Issue
Block a user