diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 7e315fbf72..d1a227cde7 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -692,7 +692,17 @@ jobs:
# vouchear). Sem este passo o par build+validate deste job e estruturalmente
# incompativel e falha 100% das vezes.
- name: Stamp dist/BUILD_SHA for the provenance guard (#10427)
- run: node scripts/build/write-build-sha.mjs
+ # O SHA TEM de vir do head da PR, nao de `git rev-parse HEAD`. Este workflow
+ # roda em `pull_request`, entao o checkout e o MERGE COMMIT efemero que o
+ # GitHub cria — um commit que nao existe em branch nenhuma e portanto nunca e
+ # ancestral da release. O guard de proveniencia (#10427) rejeita exatamente
+ # isso, e com razao: um artefato carimbado com o merge commit nao pode ser
+ # rastreado ate codigo que passou pelos gates.
+ env:
+ OMNIROUTE_BUILD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
+ run: |
+ export OMNIROUTE_BUILD_SHA="${OMNIROUTE_BUILD_SHA:0:7}"
+ node scripts/build/write-build-sha.mjs
- run: npm run check:pack-artifact
# WS1.2 (#7065 class): pack the real tarball, install it into a clean prefix and
# BOOT it to a healthy /api/monitoring/health — the gate that structure checks
diff --git a/electron/package.json b/electron/package.json
index 63a97d527c..2e3cde1e5b 100644
--- a/electron/package.json
+++ b/electron/package.json
@@ -78,6 +78,14 @@
{
"from": "../.build/electron-standalone",
"to": "app",
+ "filter": [
+ "**/*",
+ "node_modules/**/*"
+ ]
+ },
+ {
+ "from": "../.build/electron-standalone/node_modules",
+ "to": "app/node_modules",
"filter": [
"**/*"
]
diff --git a/tests/e2e/group-b-quota-plans-config.spec.ts b/tests/e2e/group-b-quota-plans-config.spec.ts
index 409e22eb9b..fa1b96499b 100644
--- a/tests/e2e/group-b-quota-plans-config.spec.ts
+++ b/tests/e2e/group-b-quota-plans-config.spec.ts
@@ -14,7 +14,20 @@ import { test, expect } from "@playwright/test";
import { gotoDashboardRoute } from "./helpers/dashboardAuth";
test.describe("Group B — Quota Plans Config", () => {
+ // Client-side exception capture. Without it a page that falls into the error
+ // boundary only shows up as "Internal Server Error" in the HTML, with no stack
+ // trace anywhere in the CI log — which is exactly how this spec's failure went
+ // undiagnosed for two CI rounds.
+ const pageErrors: string[] = [];
+
test.beforeEach(async ({ page }) => {
+ pageErrors.length = 0;
+ page.on("pageerror", (err) => {
+ pageErrors.push(`[pageerror] ${err.message}\n${err.stack ?? ""}`);
+ });
+ page.on("console", (msg) => {
+ if (msg.type() === "error") pageErrors.push(`[console.error] ${msg.text()}`);
+ });
// Mock the plans list endpoint
await page.route("**/api/quota/plans**", async (route) => {
const url = new URL(route.request().url());
@@ -143,12 +156,25 @@ test.describe("Group B — Quota Plans Config", () => {
}
// After selection, the page should not be in a broken state.
- // Note: page.content() includes the full HTML source, which contains Next.js
- // chunk filenames — those hashes can legitimately contain the string "500".
- // Checking for "500" in raw HTML is unreliable; instead check for the actual
- // error boundary text that OmniRoute renders on unrecoverable errors
- // (src/app/error.tsx heading: "Internal Server Error").
- const pageContent = await page.content();
- expect(pageContent).not.toContain("Internal Server Error");
+ //
+ // Assert on RENDERED TEXT, not on page.content(). The raw HTML always contains
+ // the string, on every route, so the old assertion could never pass: layout.tsx
+ // hands the whole message catalogue to NextIntlClientProvider, React serialises
+ // that prop into the RSC payload, and en.json carries "Internal Server Error"
+ // twice (publicSystem.error.title and errors.500.title). Probing /dashboard,
+ // /dashboard/costs, /dashboard/settings and even /login all showed the string
+ // present in the source with the page rendering perfectly.
+ //
+ // This is the same trap that killed the sibling `not.toContain("500")` here in
+ // fc77100c3f ("Checking for '500' in raw HTML is unreliable") — that one was
+ // removed, this one was kept, and it has the identical flaw.
+ //
+ // The error boundary renders the title as visible text (src/app/error.tsx
+ // `
{t("error.title")}
`), so innerText still catches the real defect
+ // while ignoring the serialised dictionary.
+ const bodyText = await page.locator("body").innerText();
+ expect(bodyText, `client errors:\n${pageErrors.join("\n---\n")}`).not.toContain(
+ "Internal Server Error"
+ );
});
});
diff --git a/tests/integration/chat-pipeline.test.ts b/tests/integration/chat-pipeline.test.ts
index 382dd00b6b..c17ce8eb6e 100644
--- a/tests/integration/chat-pipeline.test.ts
+++ b/tests/integration/chat-pipeline.test.ts
@@ -19,6 +19,7 @@ const { getLatestCallLog, getResponsesCallLogs } = await import("./_chatPipeline
const { invalidateMemorySettingsCache } = await import("../../src/lib/memory/settings.ts");
const { skillRegistry } = await import("../../src/lib/skills/registry.ts");
const { skillExecutor } = await import("../../src/lib/skills/executor.ts");
+const { encodeSkillToolName } = await import("../../src/lib/skills/injection.ts");
const { handleChat } = await import("../../src/sse/handlers/chat.ts");
const { initTranslators } = await import("../../open-sse/translator/index.ts");
const { clearInflight } = await import("../../open-sse/services/requestDedup.ts");
@@ -726,7 +727,7 @@ test("chat pipeline applies Codex CLI fingerprint to OAuth responses requests",
);
});
-test("chat pipeline strips previous_response_id from stateless Codex responses by default", async () => {
+test("chat pipeline fails closed on an unresolvable previous_response_id and keeps stateless Codex responses stateless", async () => {
await seedConnection("codex", {
apiKey: "sk-codex-stateless-responses",
providerSpecificData: { openaiStoreEnabled: false },
@@ -760,9 +761,38 @@ test("chat pipeline strips previous_response_id from stateless Codex responses b
})
);
- await response.json();
+ // #10262 virtualized `previous_response_id`: in any mode other than "preserve"
+ // the id is resolved against OmniRoute's own continuation store BEFORE routing.
+ // An id it cannot resolve fails closed with OpenAI's own contract instead of
+ // being silently stripped and forwarded as a fresh turn (which would have
+ // dropped the conversation history without telling the client).
+ const failClosed = (await response.json()) as { error?: { code?: string } };
+ assert.equal(response.status, 400);
+ assert.equal(failClosed.error?.code, "previous_response_not_found");
+ assert.equal(fetchCalls.length, 0, "a request that fails closed must not reach the upstream");
- assert.equal(response.status, 200);
+ // Positive anchor: the same stateless Codex connection, without the unresolvable
+ // continuation id, still dispatches — and the stateless contract still holds
+ // (store:false, no previous_response_id on the wire).
+ const followUp = await handleChat(
+ buildRequest({
+ url: "http://localhost/v1/responses",
+ body: {
+ model: "codex/gpt-5.5",
+ stream: false,
+ input: [
+ {
+ type: "message",
+ role: "user",
+ content: [{ type: "input_text", text: "First VS Code turn" }],
+ },
+ ],
+ },
+ })
+ );
+ await followUp.json();
+
+ assert.equal(followUp.status, 200);
assert.equal(fetchCalls.length, 1);
assert.match(fetchCalls[0].url, /\/responses$/);
assert.equal(fetchCalls[0].body.previous_response_id, undefined);
@@ -1440,13 +1470,23 @@ test("chat pipeline injects skills into tools and intercepts tool calls with ski
enabled: true,
});
+ // #9058: provider tool names must match ^[a-zA-Z0-9_-]+$, so `name@version`
+ // identifiers travel base64url-encoded. Derive the expectation from the helper
+ // instead of pinning the encoded literal.
+ const expectedSkillToolName = encodeSkillToolName("lookupWeather", "1.0.0");
+ assert.match(expectedSkillToolName, /^[a-zA-Z0-9_-]+$/);
+ assert.notEqual(expectedSkillToolName, "lookupWeather@1.0.0");
+
const fetchCalls = [];
globalThis.fetch = async (url, init: RequestInit = {}) => {
fetchCalls.push({
url: String(url),
body: init.body ? JSON.parse(String(init.body)) : null,
});
- return buildOpenAIToolCallResponse();
+ // #9058: the upstream echoes back exactly the tool name it was given — the
+ // provider-safe encoded one — so this also exercises decodeSkillToolName()
+ // on the interception path.
+ return buildOpenAIToolCallResponse({ toolName: expectedSkillToolName });
};
const response = await handleChat(
@@ -1464,7 +1504,7 @@ test("chat pipeline injects skills into tools and intercepts tool calls with ski
assert.equal(response.status, 200);
assert.equal(fetchCalls.length, 1);
assert.ok(Array.isArray(fetchCalls[0].body.tools));
- assert.equal(fetchCalls[0].body.tools[0].function.name, "lookupWeather@1.0.0");
+ assert.equal(fetchCalls[0].body.tools[0].function.name, expectedSkillToolName);
assert.equal(json.choices[0].finish_reason, "tool_calls");
assert.equal(json.tool_results[0].tool_call_id, "call_weather");
assert.equal(JSON.parse(json.tool_results[0].output).forecast, "Sunny in Sao Paulo");
diff --git a/tests/integration/monitoring-health-cache.test.ts b/tests/integration/monitoring-health-cache.test.ts
index 3c7a66b397..3df1d63b10 100644
--- a/tests/integration/monitoring-health-cache.test.ts
+++ b/tests/integration/monitoring-health-cache.test.ts
@@ -22,8 +22,25 @@ process.env.JWT_SECRET = "test-health-cache-secret";
await import("../../src/lib/db/core.ts");
const { GET, DELETE } = await import("../../src/app/api/monitoring/health/route.ts");
+// GHSA-mvf8-qc78-5mxm: the detailed health payload (the one carrying `timestamp`)
+// is reserved for a management principal — GET now takes the Request and an
+// anonymous caller only gets the liveness verdict. Every probe below therefore
+// authenticates with a dashboard-session cookie, exactly like the DELETE probe.
+const { SignJWT } = await import("jose");
+const AUTH_TOKEN = await new SignJWT({ authenticated: true })
+ .setProtectedHeader({ alg: "HS256" })
+ .setExpirationTime("30d")
+ .sign(new TextEncoder().encode(process.env.JWT_SECRET as string));
+
+function authedRequest(method = "GET"): Request {
+ return new Request("http://localhost/api/monitoring/health", {
+ method,
+ headers: { cookie: `auth_token=${AUTH_TOKEN}` },
+ });
+}
+
async function healthTimestamp(): Promise {
- const res = await GET();
+ const res = await GET(authedRequest());
const body = (await res.json()) as {
timestamp?: string;
adaptiveAdmission?: unknown;
@@ -48,19 +65,8 @@ test("cache expires after the TTL — a fresh payload is built", async () => {
});
test("DELETE (circuit-breaker reset) invalidates the cache immediately", async () => {
- const { SignJWT } = await import("jose");
- const authToken = await new SignJWT({ authenticated: true })
- .setProtectedHeader({ alg: "HS256" })
- .setExpirationTime("30d")
- .sign(new TextEncoder().encode(process.env.JWT_SECRET as string));
-
const t1 = await healthTimestamp(); // populate cache
- const delRes = await DELETE(
- new Request("http://localhost/api/monitoring/health", {
- method: "DELETE",
- headers: { cookie: `auth_token=${authToken}` },
- })
- );
+ const delRes = await DELETE(authedRequest("DELETE"));
assert.ok(delRes.status < 400, `DELETE should succeed, got ${delRes.status}`);
await new Promise((r) => setTimeout(r, 5)); // ensure the clock advances past ms precision
const t2 = await healthTimestamp();
diff --git a/tests/integration/proxy-registry-flow.test.ts b/tests/integration/proxy-registry-flow.test.ts
index 809d9b3466..4de8f0b1a2 100644
--- a/tests/integration/proxy-registry-flow.test.ts
+++ b/tests/integration/proxy-registry-flow.test.ts
@@ -176,6 +176,11 @@ test("integration: proxy registry full flow works and enforces safe delete", asy
provider: "openai",
});
+ // #11182 made SQLite persistence a background batch (1s timer / 100-entry
+ // threshold); the health aggregate reads the table, so drain the queue first
+ // instead of racing the timer.
+ proxyLogger.flushProxyLogsSync();
+
const healthRes = await proxyHealthRoute.GET(
new Request("http://localhost/api/settings/proxies/health?hours=24")
);
diff --git a/tests/integration/reasoning-routing-pipeline.test.ts b/tests/integration/reasoning-routing-pipeline.test.ts
index 6d0a9b2bac..6343da23df 100644
--- a/tests/integration/reasoning-routing-pipeline.test.ts
+++ b/tests/integration/reasoning-routing-pipeline.test.ts
@@ -3,6 +3,10 @@ import assert from "node:assert/strict";
import { createChatPipelineHarness } from "./_chatPipelineHarness.ts";
const harness = await createChatPipelineHarness("reasoning-routing-pipeline");
+// Imported only AFTER the harness has set DATA_DIR and opened the DB: a static
+// import is evaluated before any module body runs, and this module touches the
+// settings/DB layer at load time.
+const { getResolvedModelCapabilities } = await import("../../src/lib/modelCapabilities.ts");
const {
BaseExecutor,
buildOpenAIResponse,
@@ -346,10 +350,20 @@ test("reasoning routing filters incompatible combo targets and rejects an empty
await reasoningRulesDb.deleteReasoningRoutingRule(
(await reasoningRulesDb.getReasoningRoutingRules())[0].id
);
+ // The target must be a model that DECLARES it cannot think (supportsThinking:
+ // false). `unknown` capability is deliberately kept by
+ // filterComboForReasoningDecision, so a model whose thinking support later
+ // becomes true/unknown silently stops exercising this path — assert the premise.
+ const INCOMPATIBLE_TARGET = "openai/gpt-4o";
+ assert.equal(
+ getResolvedModelCapabilities(INCOMPATIBLE_TARGET).supportsThinking,
+ false,
+ `${INCOMPATIBLE_TARGET} must declare supportsThinking:false for this test to mean anything`
+ );
const incompatibleCombo = await combosDb.createCombo({
name: "incompatible-reasoning-combo",
strategy: "priority",
- models: ["antigravity/gemini-3-pro"],
+ models: [INCOMPATIBLE_TARGET],
});
await reasoningRulesDb.createReasoningRoutingRule({
name: "Empty combo target",
diff --git a/tests/integration/resilience-http-e2e.test.ts b/tests/integration/resilience-http-e2e.test.ts
index 5ff70a2098..6f94fc02f6 100644
--- a/tests/integration/resilience-http-e2e.test.ts
+++ b/tests/integration/resilience-http-e2e.test.ts
@@ -555,12 +555,15 @@ test("resilience API only exposes configuration, not runtime breaker state", asy
const { response, json } = await getJson(`${app.baseUrl}/api/resilience`);
assert.equal(response.status, 200);
+ // Exact key set — this is the whole point of the test: configuration only.
+ // `providerQuotaOverrides` joined the projection in #9871.
assert.deepEqual(Object.keys(json).sort(), [
"comboCooldownWait",
"connectionCooldown",
"legacy",
"providerBreaker",
"providerCooldown",
+ "providerQuotaOverrides",
"quotaShareConcurrencyLimit",
"requestQueue",
"waitForCooldown",
diff --git a/tests/integration/security-hardening.test.ts b/tests/integration/security-hardening.test.ts
index 479f3ed59e..17370fbaeb 100644
--- a/tests/integration/security-hardening.test.ts
+++ b/tests/integration/security-hardening.test.ts
@@ -303,8 +303,40 @@ test("OAuth routes that can create provider connections require auth guard", ()
for (const relPath of targets) {
const content = readIfExists(relPath);
assert.ok(content, `${relPath} should exist`);
- assert.ok(content.includes("isAuthRequired"), `${relPath} should check whether auth is active`);
- assert.ok(content.includes("isAuthenticated"), `${relPath} should require authenticated users`);
- assert.ok(content.includes("Unauthorized"), `${relPath} should reject anonymous requests`);
+
+ // Two accepted guard shapes. GHSA-mg76 moved the cursor/kiro *import* routes
+ // onto requireManagementAuth, which is strictly STRONGER than the legacy
+ // pair: it demands a management principal (dashboard session, manage-scoped
+ // key, CLI token) instead of merely "any authenticated caller", and answers
+ // 401/403 itself — so the literal "Unauthorized" no longer appears in the
+ // route file. The remaining routes still carry the legacy triple.
+ const usesManagementGuard = content.includes("requireManagementAuth(request");
+ const usesLegacyGuard =
+ content.includes("isAuthRequired") &&
+ content.includes("isAuthenticated") &&
+ content.includes("Unauthorized");
+ assert.ok(
+ usesManagementGuard || usesLegacyGuard,
+ `${relPath} must guard connection-creating handlers with requireManagementAuth or the isAuthRequired/isAuthenticated pair`
+ );
+
+ // Positive anchor: a guard somewhere in the file proves nothing if one of the
+ // exported handlers skips it. Slice the file per exported handler and require
+ // EACH body to await a guard on its own `request` — a guard living only in a
+ // helper (or in a sibling handler) no longer satisfies this.
+ const handlerSlices = content
+ .split(/(?=export\s+async\s+function\s+(?:GET|POST|PUT|PATCH|DELETE)\b)/)
+ .filter((slice) =>
+ /^export\s+async\s+function\s+(?:GET|POST|PUT|PATCH|DELETE)\b/.test(slice)
+ );
+ assert.ok(handlerSlices.length > 0, `${relPath} should export at least one HTTP handler`);
+ for (const slice of handlerSlices) {
+ const verb = /export\s+async\s+function\s+(\w+)/.exec(slice)?.[1];
+ assert.match(
+ slice,
+ /await\s+(?:require\w*Auth|isAuthRequired)\s*\(\s*(?:request|req)\b/,
+ `${relPath}: exported handler ${verb} does not await an auth guard on its own request`
+ );
+ }
}
});
diff --git a/tests/integration/sse-correctness.test.ts b/tests/integration/sse-correctness.test.ts
index d5874861ca..b901df3aaf 100644
--- a/tests/integration/sse-correctness.test.ts
+++ b/tests/integration/sse-correctness.test.ts
@@ -76,17 +76,43 @@ test("2. client cancel propagates to upstream (abort propagation)", async () =>
test("3. no leaked idle timers across N sequential streams", async () => {
// createSSEStream installs a setInterval idle watchdog per stream.
// If cleanup (clearInterval) does not run on stream close, timers accumulate.
- // This test creates 10 streams and drains them; it acts as a smoke test that
- // the process does not hang (a leaked setInterval that fires 10s later would
- // prevent the test process from exiting cleanly in --test-force-exit mode).
- for (let i = 0; i < 10; i++) {
+ // Each stream carries a real content delta: a stream whose upstream forwards
+ // no valuable chunk is rejected by the empty-content guard
+ // (open-sse/utils/streamEmptyChoices.ts) and would never reach the flush path
+ // whose cleanup this test is about.
+ //
+ // Drained inline, without drain()'s timeout guard: that guard leaves its own
+ // uncleared setTimeout behind and would drown out the very signal measured here.
+ const activeTimers = () =>
+ process.getActiveResourcesInfo().filter((resource) => resource === "Timeout").length;
+
+ const timersBefore = activeTimers();
+ const N = 10;
+ for (let i = 0; i < N; i++) {
const { up, out } = makeStream();
+ up.push(`data: {"choices":[{"delta":{"content":"chunk-${i}"}}]}\n\n`);
up.push("data: [DONE]\n\n");
up.close();
- await drain(out);
+
+ const reader = out.getReader();
+ const decoder = new TextDecoder();
+ let text = "";
+ for (;;) {
+ const { done, value } = await reader.read();
+ if (done) break;
+ text += decoder.decode(value);
+ }
+ // Positive anchor: the stream really ran and really closed.
+ assert.ok(text.includes(`chunk-${i}`), `stream ${i} lost its content: ${JSON.stringify(text)}`);
}
- // If we reach here without a timeout, no blocking resources were leaked.
- assert.ok(true, "all 10 streams completed without hanging");
+
+ // The watchdog of every closed stream must have been cleared. One slot of slack
+ // absorbs unrelated runtime timers, but N leaked watchdogs cannot hide in it.
+ const timersAfter = activeTimers();
+ assert.ok(
+ timersAfter <= timersBefore + 1,
+ `idle watchdog timers leaked across ${N} streams: ${timersBefore} active before, ${timersAfter} after`
+ );
});
test("4. final snapshot does not duplicate tail text", async () => {
diff --git a/tests/unit/electron-packaging.test.ts b/tests/unit/electron-packaging.test.ts
index 1e231f9aca..e08a2f9ee9 100644
--- a/tests/unit/electron-packaging.test.ts
+++ b/tests/unit/electron-packaging.test.ts
@@ -7,7 +7,29 @@ import { pruneElectronRuntimeDocs } from "../../scripts/build/electronRuntimeDoc
const ROOT = join(import.meta.dirname, "..", "..");
-test("electron build copies the standalone runtime into resources/app exactly once", () => {
+// The SECOND entry looks like a redundant duplicate of the first — it is not, and
+// removing it ships a desktop app that cannot boot.
+//
+// electron-builder's file matcher hard-codes an exclusion of the source root's
+// `node_modules` directory for extraResources/extraFiles, BEFORE any `filter`
+// pattern is consulted (app-builder-lib/out/util/filter.js: `if (relative ===
+// "node_modules") return false`). So `{ from: ".build/electron-standalone", to:
+// "app", filter: ["**/*"] }` copies server.js, server-ws.mjs and every NESTED
+// node_modules, but silently drops `.build/electron-standalone/node_modules` —
+// the tree that holds `next`, `better-sqlite3` and the whole server closure.
+//
+// Pointing a second matcher AT the node_modules directory sidesteps the check
+// (its relative paths never equal "node_modules") and is the only way to get that
+// tree into `resources/app/node_modules`, which main.js also puts on the server's
+// NODE_PATH.
+//
+// Regression history: #10325 "de-duplicated" the two entries into one on
+// 2026-08-16; the packaged app then died on `Cannot find module 'next'` at
+// resources/app/server.js. It went unnoticed for nine days because the Electron
+// Package Smoke was already red on an earlier defect (lib/loginHeaderCapture.js
+// missing from build.files since #9984), so the main process never got far enough
+// to spawn the server.
+test("electron build copies the standalone runtime AND its root node_modules into resources/app", () => {
const electronPackage = JSON.parse(readFileSync(join(ROOT, "electron", "package.json"), "utf8"));
const extraResources = electronPackage.build?.extraResources;
@@ -21,6 +43,11 @@ test("electron build copies the standalone runtime into resources/app exactly on
{
from: "../.build/electron-standalone",
to: "app",
+ filter: ["**/*", "node_modules/**/*"],
+ },
+ {
+ from: "../.build/electron-standalone/node_modules",
+ to: "app/node_modules",
filter: ["**/*"],
},
]);