Files
OmniRoute/tests/integration/proxy-registry-flow.test.ts
Xiangzhe c51d74213e fix(ci): drain the electron packaging regression, the models-catalog e2e assertion and 10 integration reds
Electron Package Smoke — a packaging defect that had been hidden behind another
packaging defect for nine days. Once the loginHeaderCapture fix let the main process
start, the server underneath died on 'Cannot find module next': resources/app/server.js
shipped without resources/app/node_modules.

electron-builder discards the ROOT node_modules in code, not by configuration —
app-builder-lib/out/util/filter.js:42 has a hard-coded `if (relative === "node_modules")
return false` that runs before any filter pattern. The second extraResources entry
pointing INTO ../.build/electron-standalone/node_modules is what sidesteps it, because
those relative paths are never equal to "node_modules". #10325 removed that entry as an
apparent duplicate and flipped the test to assert "exactly once", freezing the
regression as if it were the contract. Restored, and the unit guard now pins both
entries — proven by mutation: reverting package.json to the post-#10325 shape fails the
guard 3/4, restoring it passes 4/4.

group-b-quota-plans-config — the assertion was impossible to satisfy on ANY route, and
the page was never broken. 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, so page.content() always contains it: probing
/dashboard, /dashboard/costs, /dashboard/settings and /login showed the string present
with every page rendering fine, and a pageerror probe on the failing run captured zero
client exceptions. This is the same trap that killed the sibling not.toContain("500")
in fc77100c3f ("raw HTML is unreliable") — that one was removed, this one was kept.
Now asserts on rendered text, which still catches a real error boundary. The pageerror
capture stays: the CI failure carried no stack trace, which is why it was misread twice.

Integration — 10 of the 14 shard-2 reds, all sibling-test gaps behind security fixes:
monitoring health now takes a Request and requires management auth (GHSA-mvf8-qc78-5mxm);
the OAuth import routes moved to requireManagementAuth (GHSA-mg76) — the test accepts
both guard shapes and gained a stronger anchor that every exported handler awaits a
guard on its own request, mutation-verified; skill tool names are derived from
encodeSkillToolName() and the fake upstream now returns the encoded name so
decodeSkillToolName() is exercised too; previous_response_id now fails closed (#10262);
proxy_logs persist as an async batch (#11182) so the test flushes first;
providerQuotaOverrides joined GET /api/resilience (#9871); the reasoning fixture used a
model that stopped being thinking-incompatible, replaced and pinned with a premise
assert so it cannot rot silently again.

A vacuous assert.ok(true, "all 10 streams completed without hanging") was replaced with
real anchors — content must arrive on every stream and the active Timeout count must not
grow.

Four are deliberately left red rather than aligned, each now tracked: #11551 (the
/v1/models after() wiring is dead — the route passes a third argument to a two-parameter
function and catalogCache never imports after, so the #8728 contract is unimplemented),
#11552 (~27% of requests emit an extra discarded upstream call; the delivered
distribution is exactly 0.70, so weighted routing is correct and the waste is the real
finding), the fixed-account combo pin (aligning it would destroy the per-step attribution
the test exists for), and the web_search fallback already tracked as #11524.

Package Artifact — the provenance stamp I added last round used git rev-parse HEAD, which
under pull_request is the ephemeral merge commit and therefore never an ancestor of the
release branch. Now takes the PR head sha.

Refs #10692
2026-08-25 16:46:23 -03:00

240 lines
8.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-proxy-registry-flow-"));
process.env.DATA_DIR = TEST_DATA_DIR;
// Disable dashboard auth for direct route handler calls in CI
// (CI sets JWT_SECRET + INITIAL_PASSWORD, causing isAuthRequired() → true)
process.env.DASHBOARD_PASSWORD = "";
process.env.INITIAL_PASSWORD = "";
delete process.env.JWT_SECRET;
const core = await import("../../src/lib/db/core.ts");
const providersDb = await import("../../src/lib/db/providers.ts");
const proxySettingsRoute = await import("../../src/app/api/settings/proxies/route.ts");
const proxyAssignmentsRoute =
await import("../../src/app/api/settings/proxies/assignments/route.ts");
const proxyBulkRoute = await import("../../src/app/api/settings/proxies/bulk-assign/route.ts");
const proxyHealthRoute = await import("../../src/app/api/settings/proxies/health/route.ts");
const proxyLogger = await import("../../src/lib/proxyLogger.ts");
async function resetStorage() {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}
test.after(async () => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
test("integration: proxy create with inline assignment is atomic and clears legacy config", async () => {
await resetStorage();
const proxyLegacyRoute = await import("../../src/app/api/settings/proxy/route.ts");
const legacySetRes = await proxyLegacyRoute.PUT(
new Request("http://localhost/api/settings/proxy", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
level: "provider",
id: "openai",
proxy: { type: "http", host: "legacy-openai.local", port: 8080 },
}),
})
);
assert.equal(legacySetRes.status, 200);
const createRes = await proxySettingsRoute.POST(
new Request("http://localhost/api/settings/proxies", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: "Atomic Flow Proxy",
type: "http",
host: "atomic-flow.local",
port: 8080,
source: "dashboard-custom",
assignment: { scope: "provider", scopeId: "openai" },
}),
})
);
assert.equal(createRes.status, 201);
const createdProxy = (await createRes.json()) as any;
assert.ok(createdProxy.id);
assert.equal(createdProxy.assignment.proxyId, createdProxy.id);
assert.equal(createdProxy.assignment.scope, "provider");
assert.equal(createdProxy.assignment.scopeId, "openai");
const assignmentsRes = await proxyAssignmentsRoute.GET(
new Request("http://localhost/api/settings/proxies/assignments?scope=provider&scopeId=openai")
);
assert.equal(assignmentsRes.status, 200);
const assignments = (await assignmentsRes.json()) as any;
assert.equal(assignments.items.length, 1);
assert.equal(assignments.items[0].proxyId, createdProxy.id);
const legacyGetRes = await proxyLegacyRoute.GET(
new Request("http://localhost/api/settings/proxy?level=provider&id=openai")
);
assert.equal(legacyGetRes.status, 200);
const legacyGet = (await legacyGetRes.json()) as any;
// The legacy /api/settings/proxy GET is now a unified bridge over the new proxy
// registry (getRegistryProxyForLevel resolves assignments). After the atomic
// create-with-inline-assignment, it resolves to the newly assigned proxy
// (atomic-flow) and the pre-existing legacy config (legacy-openai) is superseded.
assert.equal(legacyGet.proxy?.host, "atomic-flow.local");
assert.notEqual(legacyGet.proxy?.host, "legacy-openai.local");
});
test("integration: proxy registry full flow works and enforces safe delete", async () => {
await resetStorage();
const connection = await providersDb.createProviderConnection({
provider: "openai",
authType: "apikey",
name: "proxy-flow-account",
apiKey: "sk-flow-test",
});
const createRes = await proxySettingsRoute.POST(
new Request("http://localhost/api/settings/proxies", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: "Flow Proxy",
type: "http",
host: "flow.local",
port: 8080,
source: "dashboard-custom",
}),
})
);
assert.equal(createRes.status, 201);
const createdProxy = (await createRes.json()) as any;
assert.ok(createdProxy.id);
assert.equal(createdProxy.source, "dashboard-custom");
const assignRes = await proxyAssignmentsRoute.PUT(
new Request("http://localhost/api/settings/proxies/assignments", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
scope: "account",
scopeId: connection.id,
proxyId: createdProxy.id,
}),
})
);
assert.equal(assignRes.status, 200);
const resolveRes = await proxyAssignmentsRoute.GET(
new Request(
`http://localhost/api/settings/proxies/assignments?resolveConnectionId=${connection.id}`
)
);
assert.equal(resolveRes.status, 200);
const resolved = (await resolveRes.json()) as any;
assert.equal(resolved.level, "account");
assert.equal(resolved.source, "registry");
assert.equal(resolved.proxy.host, "flow.local");
const bulkRes = await proxyBulkRoute.PUT(
new Request("http://localhost/api/settings/proxies/bulk-assign", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
scope: "provider",
scopeIds: ["openai", "anthropic"],
proxyId: createdProxy.id,
}),
})
);
assert.equal(bulkRes.status, 200);
const bulkPayload = (await bulkRes.json()) as any;
assert.equal(bulkPayload.updated, 2);
assert.equal(bulkPayload.failed.length, 0);
proxyLogger.logProxyEvent({
status: "success",
proxy: { type: "http", host: "flow.local", port: 8080 },
latencyMs: 90,
level: "provider",
levelId: "openai",
provider: "openai",
});
proxyLogger.logProxyEvent({
status: "error",
proxy: { type: "http", host: "flow.local", port: 8080 },
latencyMs: 240,
level: "provider",
levelId: "openai",
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")
);
assert.equal(healthRes.status, 200);
const healthPayload = (await healthRes.json()) as any;
const row = healthPayload.items.find((item) => item.proxyId === createdProxy.id);
assert.ok(row);
assert.equal(row.totalRequests >= 2, true);
assert.equal(row.errorCount >= 1, true);
const deleteConflictRes = await proxySettingsRoute.DELETE(
new Request(`http://localhost/api/settings/proxies?id=${createdProxy.id}`, {
method: "DELETE",
})
);
assert.equal(deleteConflictRes.status, 409);
const deleteConflict = (await deleteConflictRes.json()) as any;
assert.equal(deleteConflict.error.type, "conflict");
assert.equal(typeof deleteConflict.requestId, "string");
assert.equal(deleteConflict.requestId.length > 0, true);
const clearAccountAssignment = await proxyAssignmentsRoute.PUT(
new Request("http://localhost/api/settings/proxies/assignments", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
scope: "account",
scopeId: connection.id,
proxyId: null,
}),
})
);
assert.equal(clearAccountAssignment.status, 200);
const clearProviderBulk = await proxyBulkRoute.PUT(
new Request("http://localhost/api/settings/proxies/bulk-assign", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
scope: "provider",
scopeIds: ["openai", "anthropic"],
proxyId: null,
}),
})
);
assert.equal(clearProviderBulk.status, 200);
const deleteOkRes = await proxySettingsRoute.DELETE(
new Request(`http://localhost/api/settings/proxies?id=${createdProxy.id}`, {
method: "DELETE",
})
);
assert.equal(deleteOkRes.status, 200);
const deleteOkPayload = (await deleteOkRes.json()) as any;
assert.equal(deleteOkPayload.success, true);
});