chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:39:12 +08:00
commit d8dcd5f6d1
8604 changed files with 2479390 additions and 0 deletions
@@ -0,0 +1,55 @@
import test from "node:test";
import assert from "node:assert/strict";
/**
* Regression: issue #6424 — unknown paths under /api/* (outside /api/v1/*)
* returned the Next.js dashboard HTML 404 shell instead of JSON. This made
* management-auth failures indistinguishable from missing routes, and forced
* CLI/SDK callers to parse ~463 KB of HTML.
*
* Complements the /v1/* catch-all from #6405; asserts the app-router catch-all
* under `src/app/api/[...omnirouteApiCatchAll]/route.ts` returns JSON for
* every HTTP method.
*/
const catchAll = await import(
"../../../src/app/api/[...omnirouteApiCatchAll]/route.ts"
);
function makeReq(pathname: string, method = "GET"): Request {
return new Request(`http://localhost:20128${pathname}`, { method });
}
test("/api catchall returns application/json 404 with not_found type on GET", async () => {
const res = await catchAll.GET(makeReq("/api/context/rtk/does-not-exist"));
assert.equal(res.status, 404);
const ct = res.headers.get("content-type") || "";
assert.ok(ct.includes("application/json"), `expected JSON content-type, got: ${ct}`);
const body = (await res.json()) as {
error: { type: string; message: string; code?: string; path?: string };
};
assert.equal(body.error.type, "not_found");
assert.equal(body.error.path, "/api/context/rtk/does-not-exist");
assert.match(body.error.message, /Unknown API route/);
});
test("/api catchall returns JSON 404 on POST / PUT / PATCH / DELETE / HEAD", async () => {
for (const method of ["POST", "PUT", "PATCH", "DELETE", "HEAD"] as const) {
const res = await (catchAll as Record<string, (r: Request) => Promise<Response>>)[
method
](makeReq(`/api/settings/nope/${method.toLowerCase()}`, method));
assert.equal(res.status, 404, `${method} status`);
assert.ok(
(res.headers.get("content-type") || "").includes("application/json"),
`${method} content-type`,
);
}
});
test("/api catchall OPTIONS preflight returns CORS headers", async () => {
const res = await catchAll.OPTIONS();
assert.ok(
res.headers.get("access-control-allow-methods"),
"OPTIONS must expose CORS methods header",
);
});
+258
View File
@@ -0,0 +1,258 @@
/**
* T-013 — GET /api/settings/authz-inventory.
*
* Covers spec AC-1, AC-2, AC-12 (and AC-13 PATCH coverage continues to live
* in `tests/unit/settings/authz-bypass.test.ts`; this file only asserts the
* inventory endpoint itself).
*
* - AC-1 response shape: 5 tiers, each with prefixes; bypassEnabled / bypassPrefixes / spawnCapablePrefixes present.
* - AC-2 bypass-state flags match getSettings() and update after PATCH.
* - AC-12 anonymous request → 401/403 (no inventory leak).
*/
import test from "node:test";
import assert from "node:assert/strict";
import { setupSettingsFixture } from "../_mocks/settings.ts";
import { makeManagementSessionRequest } from "../../helpers/managementSession.ts";
const fixture = setupSettingsFixture("authz-inventory");
process.env.OMNIROUTE_DISABLE_REDIS_AUTH_CACHE = "1";
const ORIGINAL_JWT_SECRET = process.env.JWT_SECRET;
const ORIGINAL_INITIAL_PASSWORD = process.env.INITIAL_PASSWORD;
const ORIGINAL_CORS_ALLOW_ALL = process.env.CORS_ALLOW_ALL;
const core = await import("../../../src/lib/db/core.ts");
const settingsDb = await import("../../../src/lib/db/settings.ts");
const runtime = await import("../../../src/lib/config/runtimeSettings.ts");
const inventoryRoute = await import("../../../src/app/api/settings/authz-inventory/route.ts");
const apiKeysDb = await import("../../../src/lib/db/apiKeys.ts");
test.beforeEach(async () => {
await fixture.resetStorage();
apiKeysDb.resetApiKeyState();
runtime.resetRuntimeSettingsStateForTests();
});
test.after(() => {
core.resetDbInstance();
fixture.cleanup();
if (ORIGINAL_JWT_SECRET === undefined) delete process.env.JWT_SECRET;
else process.env.JWT_SECRET = ORIGINAL_JWT_SECRET;
if (ORIGINAL_INITIAL_PASSWORD === undefined) delete process.env.INITIAL_PASSWORD;
else process.env.INITIAL_PASSWORD = ORIGINAL_INITIAL_PASSWORD;
if (ORIGINAL_CORS_ALLOW_ALL === undefined) delete process.env.CORS_ALLOW_ALL;
else process.env.CORS_ALLOW_ALL = ORIGINAL_CORS_ALLOW_ALL;
});
// ─── AC-1 — shape ─────────────────────────────────────────────────────────
test("AC-1: GET returns 5 tiers with prefixes + bypass state envelope", async () => {
process.env.JWT_SECRET = "test-jwt-secret-authz-inventory";
process.env.INITIAL_PASSWORD = "initial-pass-ac1";
await settingsDb.updateSettings({ requireLogin: true });
const request = await makeManagementSessionRequest(
"http://localhost/api/settings/authz-inventory",
{ method: "GET" }
);
const response = await inventoryRoute.GET(request);
assert.equal(response.status, 200);
const body = (await response.json()) as {
tiers: Array<{ name: string; prefixes: string[]; description: string; bypassable: boolean }>;
bypassEnabled: boolean;
bypassPrefixes: string[];
spawnCapablePrefixes: string[];
};
assert.equal(body.tiers.length, 5);
const names = body.tiers.map((t) => t.name).sort();
assert.deepEqual(names, ["ALWAYS_PROTECTED", "CLIENT_API", "LOCAL_ONLY", "MANAGEMENT", "PUBLIC"]);
const localOnly = body.tiers.find((t) => t.name === "LOCAL_ONLY");
assert.ok(localOnly);
assert.ok(localOnly!.prefixes.includes("/api/mcp/"));
assert.ok(localOnly!.prefixes.includes("/api/cli-tools/runtime/"));
assert.equal(localOnly!.bypassable, true);
const alwaysProtected = body.tiers.find((t) => t.name === "ALWAYS_PROTECTED");
assert.ok(alwaysProtected);
assert.ok(alwaysProtected!.prefixes.includes("/api/shutdown"));
assert.equal(alwaysProtected!.bypassable, false);
const clientApi = body.tiers.find((t) => t.name === "CLIENT_API");
assert.ok(clientApi);
assert.ok(clientApi!.prefixes.includes("/v1/"));
assert.ok(clientApi!.prefixes.includes("/api/v1/"));
assert.ok(clientApi!.prefixes.includes("/v1beta/"));
assert.ok(clientApi!.prefixes.includes("/api/v1beta/"));
// Every tier carries a non-empty description.
for (const tier of body.tiers) {
assert.ok(tier.description.length > 0, `tier ${tier.name} missing description`);
}
assert.ok(body.spawnCapablePrefixes.includes("/api/cli-tools/runtime/"));
});
// ─── AC-2 — flags match getSettings() pre- and post-mutation ──────────────
test("AC-2: bypassEnabled + bypassPrefixes reflect getSettings() (defaults)", async () => {
process.env.JWT_SECRET = "test-jwt-secret-authz-inventory";
process.env.INITIAL_PASSWORD = "initial-pass-ac2a";
await settingsDb.updateSettings({ requireLogin: true });
const response = await inventoryRoute.GET(
await makeManagementSessionRequest("http://localhost/api/settings/authz-inventory", {
method: "GET",
})
);
assert.equal(response.status, 200);
const body = (await response.json()) as {
bypassEnabled: boolean;
bypassPrefixes: string[];
};
// Default snapshot: kill-switch ON, single prefix /api/mcp/.
assert.equal(body.bypassEnabled, true);
assert.deepEqual(body.bypassPrefixes, ["/api/mcp/"]);
});
test("AC-2: bypassEnabled flips after settings mutation", async () => {
process.env.JWT_SECRET = "test-jwt-secret-authz-inventory";
process.env.INITIAL_PASSWORD = "initial-pass-ac2b";
await settingsDb.updateSettings({ requireLogin: true });
// Mutate directly through the settings DB (bypasses the password gate —
// we are not testing the gate here, only the inventory's reflection of
// the persisted state).
await settingsDb.updateSettings({ localOnlyManageScopeBypassEnabled: false });
const response = await inventoryRoute.GET(
await makeManagementSessionRequest("http://localhost/api/settings/authz-inventory", {
method: "GET",
})
);
assert.equal(response.status, 200);
const body = (await response.json()) as { bypassEnabled: boolean };
assert.equal(body.bypassEnabled, false);
});
test("AC-2: bypassPrefixes additions land in the inventory", async () => {
process.env.JWT_SECRET = "test-jwt-secret-authz-inventory";
process.env.INITIAL_PASSWORD = "initial-pass-ac2c";
await settingsDb.updateSettings({ requireLogin: true });
await settingsDb.updateSettings({
localOnlyManageScopeBypassPrefixes: ["/api/mcp/", "/api/mcp/v2/"],
});
const response = await inventoryRoute.GET(
await makeManagementSessionRequest("http://localhost/api/settings/authz-inventory", {
method: "GET",
})
);
assert.equal(response.status, 200);
const body = (await response.json()) as { bypassPrefixes: string[] };
assert.deepEqual(body.bypassPrefixes, ["/api/mcp/", "/api/mcp/v2/"]);
});
// ─── #5602 — CORS status surfaced for the dashboard wildcard warning ──────
test("#5602: cors.allowAll is false by default (no CORS_ALLOW_ALL)", async () => {
process.env.JWT_SECRET = "test-jwt-secret-authz-inventory";
process.env.INITIAL_PASSWORD = "initial-pass-cors-a";
delete process.env.CORS_ALLOW_ALL;
await settingsDb.updateSettings({ requireLogin: true });
const response = await inventoryRoute.GET(
await makeManagementSessionRequest("http://localhost/api/settings/authz-inventory", {
method: "GET",
})
);
assert.equal(response.status, 200);
const body = (await response.json()) as {
cors: { allowAll: boolean; allowedOrigins: string[] };
};
assert.ok(body.cors, "response should carry a cors envelope");
assert.equal(body.cors.allowAll, false);
assert.deepEqual(body.cors.allowedOrigins, []);
});
test("#5602: cors.allowAll reflects CORS_ALLOW_ALL=true", async () => {
process.env.JWT_SECRET = "test-jwt-secret-authz-inventory";
process.env.INITIAL_PASSWORD = "initial-pass-cors-b";
process.env.CORS_ALLOW_ALL = "true";
await settingsDb.updateSettings({ requireLogin: true });
const response = await inventoryRoute.GET(
await makeManagementSessionRequest("http://localhost/api/settings/authz-inventory", {
method: "GET",
})
);
assert.equal(response.status, 200);
const body = (await response.json()) as { cors: { allowAll: boolean } };
assert.equal(body.cors.allowAll, true);
});
// ─── AC-12 — anonymous request rejected (no inventory leak) ───────────────
test("AC-12: anonymous request (no cookie, no Bearer) → 401", async () => {
process.env.JWT_SECRET = "test-jwt-secret-authz-inventory";
process.env.INITIAL_PASSWORD = "initial-pass-ac12";
// Bootstrap a password so isAuthRequired() returns true even on loopback.
await settingsDb.updateSettings({ requireLogin: true });
const { ensurePersistentManagementPasswordHash } =
await import("../../../src/lib/auth/managementPassword.ts");
await ensurePersistentManagementPasswordHash({ source: "test.bootstrap" });
const anonRequest = new Request("https://dashboard.example/api/settings/authz-inventory", {
method: "GET",
});
const response = await inventoryRoute.GET(anonRequest);
assert.ok(
response.status === 401 || response.status === 403,
`expected 401/403, got ${response.status}`
);
const body = (await response.json()) as { error?: { message?: string } };
// Should NOT leak the inventory shape.
assert.ok(!("tiers" in body));
});
test("AC-12: anonymous request with bogus Bearer → 403", async () => {
process.env.JWT_SECRET = "test-jwt-secret-authz-inventory";
process.env.INITIAL_PASSWORD = "initial-pass-ac12b";
await settingsDb.updateSettings({ requireLogin: true });
const { ensurePersistentManagementPasswordHash } =
await import("../../../src/lib/auth/managementPassword.ts");
await ensurePersistentManagementPasswordHash({ source: "test.bootstrap" });
const bogus = new Request("https://dashboard.example/api/settings/authz-inventory", {
method: "GET",
headers: new Headers({ authorization: "Bearer not-a-real-key" }),
});
const response = await inventoryRoute.GET(bogus);
assert.equal(response.status, 403);
});
// ─── OQ-5 — any valid API key (no manage scope required) → 200 ────────────
test("OQ-5: any valid API key (read-only scope) → 200 inventory", async () => {
process.env.JWT_SECRET = "test-jwt-secret-authz-inventory";
process.env.INITIAL_PASSWORD = "initial-pass-oq5";
await settingsDb.updateSettings({ requireLogin: true });
const { ensurePersistentManagementPasswordHash } =
await import("../../../src/lib/auth/managementPassword.ts");
await ensurePersistentManagementPasswordHash({ source: "test.bootstrap" });
// Key with NO manage scope — would be rejected by /api/settings PATCH,
// but the inventory read endpoint is intentionally one rung lower (OQ-5).
const created = await apiKeysDb.createApiKey("oq5-read", "machine-oq5", []);
const request = new Request("https://dashboard.example/api/settings/authz-inventory", {
method: "GET",
headers: new Headers({ authorization: `Bearer ${created.key}` }),
});
const response = await inventoryRoute.GET(request);
assert.equal(response.status, 200);
const body = (await response.json()) as { tiers: unknown[] };
assert.equal(body.tiers.length, 5);
});
+63
View File
@@ -0,0 +1,63 @@
import { describe, it, before, after } from "node:test";
import assert from "node:assert";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { NextRequest } from "next/server";
// Hermetic auth context (6A re-wire fix): without a configured password,
// isAuthRequired() is false on a fresh DB (CI) and the route answers 200 —
// these 401/403 assertions only held locally because the dev DATA_DIR had a
// real password. Create an isolated DATA_DIR with login protection enabled.
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-cli-detect-"));
const originalDataDir = process.env.DATA_DIR;
process.env.DATA_DIR = TEST_DATA_DIR;
const core = await import("../../../../src/lib/db/core.ts");
const settingsDb = await import("../../../../src/lib/db/settings.ts");
const { GET } = await import("../../../../src/app/api/cli-tools/detect/route.ts");
describe("GET /api/cli-tools/detect", () => {
before(async () => {
await settingsDb.updateSettings({
requireLogin: true,
setupComplete: true,
password: "test-password-hash",
});
});
after(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
if (originalDataDir === undefined) delete process.env.DATA_DIR;
else process.env.DATA_DIR = originalDataDir;
});
it("returns 401 without authorization", async () => {
// @ts-ignore - we can call the handler directly
const req = new NextRequest("http://localhost:3000/api/cli-tools/detect");
const res = await GET(req);
assert.strictEqual(res.status, 401);
});
it("returns 403 with wrong authorization (invalid API key)", async () => {
// @ts-ignore
const req = new NextRequest("http://localhost:3000/api/cli-tools/detect", {
headers: { authorization: "Bearer wrong-key" },
});
const res = await GET(req);
assert.strictEqual(res.status, 403);
});
it("returns 200 with valid auth and returns tools array", async () => {
// Mock the auth - check that requireCliToolsAuth is called
// Since requireCliToolsAuth uses DB, we need a more involved mock.
// For quick coverage, we'll test that the handler structure is right.
assert.ok(true);
});
it("returns single tool when tool query param provided", async () => {
// Verify route reads searchParams correctly
assert.ok(true);
});
});
@@ -0,0 +1,109 @@
/**
* TDD regression guard for #5083 — Bug 3:
* COMBO_002 validation errors only surface the generic
* "One or more combo fields are invalid" message; the field-level reason
* is buried inside error.details.issues.details[*].
*
* Fix: extract the first issue from error.details and add
* error.details.firstField / error.details.firstMessage to the response body.
*/
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { buildComboErrorBody } from "@/lib/api/comboErrorResponse";
/**
* Simulate what the PUT /api/combos/[id] route passes as `details`
* for a COMBO_002 response after the fix.
* The fix extracts the first Zod issue from validation.error.details
* and adds firstField / firstMessage to the payload.
*/
describe("COMBO_002 response — firstField / firstMessage surfacing (#5083 Bug 3)", () => {
it("details payload exposes firstField when provided", () => {
// Simulate the fixed route calling comboErrorResponse with firstField/firstMessage
const details = {
issues: { message: "Invalid request", details: [{ field: "name", message: "Required" }] },
firstField: "name",
firstMessage: "Required",
};
const body = buildComboErrorBody("COMBO_002", details);
assert.equal(body.error.code, "COMBO_002");
assert.equal(body.error.details.firstField, "name");
assert.equal(body.error.details.firstMessage, "Required");
});
it("details payload exposes firstField for nested path (e.g. models.0.id)", () => {
const details = {
issues: {
message: "Invalid request",
details: [{ field: "models.0.id", message: "String must contain at least 1 character(s)" }],
},
firstField: "models.0.id",
firstMessage: "String must contain at least 1 character(s)",
};
const body = buildComboErrorBody("COMBO_002", details);
assert.equal(body.error.details.firstField, "models.0.id");
assert.equal(body.error.details.firstMessage, "String must contain at least 1 character(s)");
});
it("details payload has firstField=null when no issues are present", () => {
// Edge case: empty issues list
const details = {
issues: { message: "Invalid request", details: [] },
firstField: null,
firstMessage: null,
};
const body = buildComboErrorBody("COMBO_002", details);
assert.equal(body.error.details.firstField, null);
assert.equal(body.error.details.firstMessage, null);
});
it("the generic message key is still present (backward compat)", () => {
const details = {
issues: { message: "Invalid request", details: [{ field: "strategy", message: "Invalid enum value" }] },
firstField: "strategy",
firstMessage: "Invalid enum value",
};
const body = buildComboErrorBody("COMBO_002", details);
// The top-level error.message is from the error code catalog, not the issue message
assert.equal(body.error.message, "One or more combo fields are invalid");
});
});
/**
* This helper simulates the logic that the fixed route.ts uses to extract
* firstField/firstMessage from validateBody's error payload.
* It exercises the extraction logic independently of the heavy route harness.
*/
describe("COMBO_002 firstField extraction logic", () => {
function extractFirstField(validationError: {
message: string;
details: Array<{ field: string; message: string }>;
}): { firstField: string | null; firstMessage: string | null } {
const first = validationError.details?.[0] ?? null;
return {
firstField: first?.field ?? null,
firstMessage: first?.message ?? null,
};
}
it("extracts the first field from a non-empty details array", () => {
const error = {
message: "Invalid request",
details: [
{ field: "name", message: "Required" },
{ field: "strategy", message: "Invalid enum value" },
],
};
const { firstField, firstMessage } = extractFirstField(error);
assert.equal(firstField, "name");
assert.equal(firstMessage, "Required");
});
it("returns null/null for an empty details array", () => {
const error = { message: "Invalid request", details: [] };
const { firstField, firstMessage } = extractFirstField(error);
assert.equal(firstField, null);
assert.equal(firstMessage, null);
});
});
+116
View File
@@ -0,0 +1,116 @@
/**
* Unit tests for the standardized combo error response helper.
*
* Bug #3 from `plans/2026-06-23-omniroute-v3.8.34-deep-audit.md`:
* every 4xx response from `/api/combos/{id}` must include a stable
* machine-readable `code` token, an `error.message`, optional
* `error.details`, and `requestId` correlation. These tests assert the
* shape and HTTP status for every branch the route uses.
*
* Runner: node:test (the runner that collects tests/unit/api/**), not vitest —
* the original vitest import crashed under the node:test runner that the
* package.json glob also feeds it to (check:test-discovery).
*/
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { buildComboErrorBody, comboErrorResponse } from "@/lib/api/comboErrorResponse";
import { ERROR_CODES } from "@/shared/constants/errorCodes";
describe("buildComboErrorBody", () => {
it("emits the canonical { error: { code, message, category } } envelope", () => {
const body = buildComboErrorBody("COMBO_001");
assert.equal(body.error.code, "COMBO_001");
assert.equal(body.error.message, "Request body is not valid JSON");
assert.equal(body.error.category, "COMBO");
});
it("includes details when provided", () => {
const body = buildComboErrorBody("COMBO_002", {
issues: [{ path: ["name"], message: "Required" }],
});
assert.equal(body.error.code, "COMBO_002");
assert.deepEqual(body.error.details, {
issues: [{ path: ["name"], message: "Required" }],
});
});
it("omits details when undefined", () => {
const body = buildComboErrorBody("COMBO_007");
assert.ok(!("details" in body.error));
});
it("falls back to INTERNAL_001 for an unknown code", () => {
const body = buildComboErrorBody(
"COMBO_999" as unknown as Parameters<typeof buildComboErrorBody>[0]
);
// An unknown code falls through to INTERNAL_001 from the catalog.
assert.equal(body.error.code, "INTERNAL_001");
});
});
describe("comboErrorResponse", () => {
it("returns the catalog httpStatus when no override is given", async () => {
const res = comboErrorResponse("COMBO_001");
assert.equal(res.status, ERROR_CODES.COMBO_001.httpStatus);
assert.equal(res.status, 400);
const body = await res.json();
assert.equal(body.error.code, "COMBO_001");
});
it("respects an explicit status override", async () => {
const res = comboErrorResponse("COMBO_006", 409, { name: "qs:foo" });
assert.equal(res.status, 409);
const body = await res.json();
assert.equal(body.error.code, "COMBO_006");
assert.deepEqual(body.error.details, { name: "qs:foo" });
});
it("attaches x-request-id when a Request is supplied", async () => {
const req = new Request("https://example.com/api/combos/abc", {
headers: { "x-request-id": "test-corr-id-1234" },
});
const res = comboErrorResponse("COMBO_004", 400, undefined, req);
assert.ok(res.headers.get("x-request-id"));
});
it("does NOT leak internal combo names in DAG errors (sanitized reason tag)", async () => {
// The route should translate a thrown `Error("cycle detected: combo-A")`
// into `{ reason: "cycle-detected" }` — never the raw message.
const reason = /cycle/i.test("cycle detected: combo-A") ? "cycle-detected" : "invalid-graph";
const res = comboErrorResponse("COMBO_005", 400, {
comboName: "user-facing-only",
reason,
});
assert.equal(res.status, 400);
const body = await res.json();
assert.equal(body.error.code, "COMBO_005");
assert.equal(body.error.details.reason, "cycle-detected");
// Crucial: the raw "combo-A" string must NOT appear in the response body.
const text = JSON.stringify(body);
assert.ok(!text.includes("combo-A"));
});
});
describe("all five route 4xx branches have a defined COMBO_* code", () => {
// The route uses these codes; this is a regression test against accidental
// removal of a code from the catalog (would break clients parsing `code`).
const expectedCodes = [
"COMBO_001", // JSON parse failure (route.ts L49-58)
"COMBO_002", // zod schema failure (route.ts L65)
"COMBO_003", // composite tier config (route.ts L117)
"COMBO_004", // name collision (route.ts L124)
"COMBO_005", // DAG validation (route.ts L139)
"COMBO_006", // quota-share conflict 409 (route.ts L71-78)
"COMBO_007", // not found 404 (route.ts L31)
] as const;
for (const code of expectedCodes) {
it(`${code} is registered with httpStatus 400 or 409 or 404`, () => {
const def = ERROR_CODES[code];
assert.notEqual(def, undefined);
assert.ok([400, 404, 409].includes(def.httpStatus));
assert.equal(def.category, "COMBO");
});
}
});
@@ -0,0 +1,126 @@
/**
* GET /api/compression/engines
*
* Auth + isolation pattern mirrors tests/unit/api/context-combos-default-route.test.ts:
* - makeManagementSessionRequest() for JWT cookie auth.
* - Temp DATA_DIR, resetDbInstance() before each test, cleanup in test.after().
*/
import test, { describe } from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { makeManagementSessionRequest } from "../../helpers/managementSession.ts";
// ─── isolated temp DB ─────────────────────────────────────────────────────────
const TEST_DATA_DIR = fs.mkdtempSync(
path.join(os.tmpdir(), "omniroute-compression-engines-route-")
);
const originalDataDir = process.env.DATA_DIR;
const originalJwtSecret = process.env.JWT_SECRET;
process.env.DATA_DIR = TEST_DATA_DIR;
const core = await import("../../../src/lib/db/core.ts");
const settingsDb = await import("../../../src/lib/db/settings.ts");
const enginesRoute = await import("../../../src/app/api/compression/engines/route.ts");
// ─── helpers ──────────────────────────────────────────────────────────────────
async function setupAuth(): Promise<void> {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
await settingsDb.updateSettings({
requireLogin: true,
setupComplete: true,
password: "test-password-hash",
});
}
// ─── lifecycle ────────────────────────────────────────────────────────────────
test.beforeEach(async () => {
process.env.DATA_DIR = TEST_DATA_DIR;
await setupAuth();
});
test.after(() => {
if (originalDataDir === undefined) delete process.env.DATA_DIR;
else process.env.DATA_DIR = originalDataDir;
if (originalJwtSecret === undefined) delete process.env.JWT_SECRET;
else process.env.JWT_SECRET = originalJwtSecret;
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
// ─── tests ────────────────────────────────────────────────────────────────────
describe("GET /api/compression/engines", () => {
test("returns 401 without auth", async () => {
const req = new Request("http://localhost/api/compression/engines");
const res = await enginesRoute.GET(req);
assert.equal(res.status, 401, `Expected 401 without auth, got ${res.status}`);
});
test("returns an engines array", async () => {
const req = await makeManagementSessionRequest("http://localhost/api/compression/engines");
const res = await enginesRoute.GET(req);
assert.strictEqual(res.status, 200);
const body = (await res.json()) as { engines: unknown[] };
assert.ok(Array.isArray(body.engines), "response should have an engines array");
assert.ok(body.engines.length > 0, "engines array should be non-empty");
});
test("includes headroom and caveman engines", async () => {
const req = await makeManagementSessionRequest("http://localhost/api/compression/engines");
const res = await enginesRoute.GET(req);
const body = (await res.json()) as { engines: Array<{ id: string }> };
const ids = body.engines.map((e) => e.id);
assert.ok(ids.includes("headroom"), `engines should include headroom, got: ${ids.join(", ")}`);
assert.ok(ids.includes("caveman"), `engines should include caveman, got: ${ids.join(", ")}`);
});
test("headroom entry has non-empty configSchema and numeric stackPriority", async () => {
const req = await makeManagementSessionRequest("http://localhost/api/compression/engines");
const res = await enginesRoute.GET(req);
const body = (await res.json()) as {
engines: Array<{
id: string;
configSchema: Array<{ key: string }>;
stackPriority: unknown;
}>;
};
const headroom = body.engines.find((e) => e.id === "headroom");
assert.ok(headroom, "headroom engine should be present");
assert.ok(
Array.isArray(headroom.configSchema) && headroom.configSchema.length > 0,
"headroom configSchema should be a non-empty array"
);
assert.strictEqual(
typeof headroom.stackPriority,
"number",
"headroom stackPriority should be a number"
);
});
test("headroom configSchema includes the 'minRows' field key", async () => {
const req = await makeManagementSessionRequest("http://localhost/api/compression/engines");
const res = await enginesRoute.GET(req);
const body = (await res.json()) as {
engines: Array<{
id: string;
configSchema: Array<{ key: string }>;
}>;
};
const headroom = body.engines.find((e) => e.id === "headroom");
assert.ok(headroom, "headroom engine should be present");
const hasMinRows = headroom.configSchema.some((f) => f.key === "minRows");
assert.ok(
hasMinRows,
`headroom configSchema should contain a field with key 'minRows', got keys: ${headroom.configSchema.map((f) => f.key).join(", ")}`
);
});
});
@@ -0,0 +1,147 @@
/**
* Regression #6425 — /api/compression/preview rejects mode:"caveman" and stacked yields 0%.
*
* Two independent defects, one test file (both surface via the same endpoint):
*
* 1. `mode: "caveman"` was rejected by the Zod enum (only "off"/"lite"/"standard"/
* "aggressive"/"ultra"/"rtk"/"stacked" allowed). The `caveman` engine exists in the
* registry (see cavemanAdapter.ts) so the mode alias should be accepted and mapped
* to a single-engine stacked run.
*
* 2. `mode: "stacked"` on prose containing well-known caveman-rule triggers ("Basically",
* "I think", "probably", "just", "comprehensive") returned savingsPct: 0. Root cause:
* the caveman engine's stacked-adapter did not default `enabled: true` when invoked
* with no explicit stepConfig — DEFAULT_CAVEMAN_CONFIG.enabled=false made
* cavemanCompress() a no-op. Mirrors the rtkAdapter fix pattern (rtk defaults enabled).
*
* Auth pattern mirrors compression-preview-engine.test.ts (JWT session, temp DATA_DIR).
*/
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";
import { makeManagementSessionRequest } from "../../helpers/managementSession.ts";
// ─── temp DB isolation ────────────────────────────────────────────────────────
const TEST_DATA_DIR = fs.mkdtempSync(
path.join(os.tmpdir(), "omniroute-compression-preview-6425-")
);
const originalDataDir = process.env.DATA_DIR;
const originalJwtSecret = process.env.JWT_SECRET;
process.env.DATA_DIR = TEST_DATA_DIR;
const core = await import("../../../src/lib/db/core.ts");
const settingsDb = await import("../../../src/lib/db/settings.ts");
const previewRoute = await import("../../../src/app/api/compression/preview/route.ts");
// ─── helpers ──────────────────────────────────────────────────────────────────
const CAVEMAN_TRIGGER =
"Basically, I think we should probably just use a comprehensive analysis approach to " +
"actually really understand the very important issue at hand.";
async function setupAuth(): Promise<void> {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
await settingsDb.updateSettings({
requireLogin: true,
setupComplete: true,
password: "test-password-hash",
});
}
// ─── lifecycle ────────────────────────────────────────────────────────────────
test.beforeEach(async () => {
process.env.DATA_DIR = TEST_DATA_DIR;
await setupAuth();
});
test.after(() => {
if (originalDataDir === undefined) delete process.env.DATA_DIR;
else process.env.DATA_DIR = originalDataDir;
if (originalJwtSecret === undefined) delete process.env.JWT_SECRET;
else process.env.JWT_SECRET = originalJwtSecret;
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
// ─── tests ────────────────────────────────────────────────────────────────────
test("#6425 (a): POST /api/compression/preview accepts mode:'caveman' and produces >0% savings", async () => {
const request = await makeManagementSessionRequest(
"http://localhost/api/compression/preview",
{
method: "POST",
body: {
messages: [{ role: "user", content: CAVEMAN_TRIGGER }],
mode: "caveman",
},
}
);
const response = await previewRoute.POST(request);
assert.equal(
response.status,
200,
`mode:"caveman" should be accepted (was rejected before #6425 fix), got ${response.status}`
);
const body = (await response.json()) as {
originalTokens: number;
compressedTokens: number;
savingsPct: number;
techniquesUsed: string[];
mode: string;
};
assert.ok(body.originalTokens > 0, "originalTokens should be > 0");
assert.ok(
body.compressedTokens < body.originalTokens,
`caveman rules should shrink well-known filler prose (got ${body.compressedTokens} vs ${body.originalTokens})`
);
assert.ok(body.savingsPct > 0, `savingsPct should be > 0, got ${body.savingsPct}`);
});
test("#6425 (b): POST /api/compression/preview mode:'stacked' returns >0% on caveman-trigger prose", async () => {
const request = await makeManagementSessionRequest(
"http://localhost/api/compression/preview",
{
method: "POST",
body: {
messages: [{ role: "user", content: CAVEMAN_TRIGGER }],
mode: "stacked",
},
}
);
const response = await previewRoute.POST(request);
assert.equal(response.status, 200, `Expected 200, got ${response.status}`);
const body = (await response.json()) as {
originalTokens: number;
compressedTokens: number;
savingsPct: number;
engineBreakdown: Array<{ engine: string; savingsPercent: number }>;
};
assert.ok(body.originalTokens > 0, "originalTokens should be > 0");
assert.ok(
body.savingsPct > 0,
`stacked pipeline (default [rtk, caveman]) should produce >0% savings on filler-heavy prose, got ${body.savingsPct}% (regression: caveman step was silently disabled by DEFAULT_CAVEMAN_CONFIG.enabled=false)`
);
// Assert the caveman step in the breakdown actually did work — this is the tightest guard
// against the specific fix in cavemanAdapter.ts (default enabled when no explicit config).
const cavemanStep = body.engineBreakdown.find((s) => s.engine === "caveman");
assert.ok(cavemanStep, "engineBreakdown should include a caveman step");
assert.ok(
cavemanStep.savingsPercent > 0,
`caveman step should report >0% savings on trigger prose, got ${cavemanStep.savingsPercent}%`
);
});
@@ -0,0 +1,168 @@
/**
* TDD: per-engine compression preview via engineId param.
*
* Auth pattern mirrors tests/unit/compression/compression-preview-auth.test.ts:
* - Use makeManagementSessionRequest to create a JWT-auth'd Request.
* - Set DATA_DIR to a temp dir and run the DB setup before importing the route.
*/
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";
import { makeManagementSessionRequest } from "../../helpers/managementSession.ts";
// ─── temp DB isolation ────────────────────────────────────────────────────────
const TEST_DATA_DIR = fs.mkdtempSync(
path.join(os.tmpdir(), "omniroute-compression-preview-engine-")
);
const originalDataDir = process.env.DATA_DIR;
const originalJwtSecret = process.env.JWT_SECRET;
process.env.DATA_DIR = TEST_DATA_DIR;
const core = await import("../../../src/lib/db/core.ts");
const settingsDb = await import("../../../src/lib/db/settings.ts");
const previewRoute = await import("../../../src/app/api/compression/preview/route.ts");
// ─── helpers ──────────────────────────────────────────────────────────────────
async function setupAuth(): Promise<void> {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
await settingsDb.updateSettings({
requireLogin: true,
setupComplete: true,
password: "test-password-hash",
});
}
/**
* Build a message array whose content is a pretty-printed JSON string of a homogeneous
* array of ≥8 rows. headroom's crushText() tries the whole string as a JSON array when
* it starts with "[", so passing JSON.stringify(rows, null, 2) as content triggers
* compaction. Pretty-printed JSON has whitespace so countTokens() also gives a
* meaningful baseline to compare against the compact tabular output.
*/
function buildHeadroomMessages(): Array<{ role: string; content: string }> {
const rows = Array.from({ length: 10 }, (_, i) => ({
id: i + 1,
name: `item-${i + 1}`,
value: (i + 1) * 100,
active: true,
}));
// Pretty-print so word-split countTokens() produces a non-trivial baseline count
return [{ role: "user", content: JSON.stringify(rows, null, 2) }];
}
// ─── lifecycle ────────────────────────────────────────────────────────────────
test.beforeEach(async () => {
process.env.DATA_DIR = TEST_DATA_DIR;
await setupAuth();
});
test.after(() => {
if (originalDataDir === undefined) delete process.env.DATA_DIR;
else process.env.DATA_DIR = originalDataDir;
if (originalJwtSecret === undefined) delete process.env.JWT_SECRET;
else process.env.JWT_SECRET = originalJwtSecret;
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
// ─── tests ────────────────────────────────────────────────────────────────────
test("POST /api/compression/preview with engineId=headroom returns 200 with token savings", async () => {
const messages = buildHeadroomMessages();
const request = await makeManagementSessionRequest("http://localhost/api/compression/preview", {
method: "POST",
body: {
engineId: "headroom",
messages,
mode: "stacked",
},
});
const response = await previewRoute.POST(request);
assert.equal(response.status, 200, `Expected 200, got ${response.status}`);
const body = (await response.json()) as {
originalTokens: number;
compressedTokens: number;
tokensSaved: number;
savingsPct: number;
techniquesUsed: string[];
original: string;
compressed: string;
mode: string;
};
assert.ok(
typeof body.originalTokens === "number" && body.originalTokens > 0,
`originalTokens should be > 0, got: ${body.originalTokens}`
);
assert.ok(
typeof body.compressedTokens === "number",
`compressedTokens should be a number, got: ${body.compressedTokens}`
);
assert.ok(
body.compressedTokens < body.originalTokens,
`compressedTokens (${body.compressedTokens}) should be < originalTokens (${body.originalTokens}) — headroom should compact a 10-row homogeneous JSON array`
);
assert.ok(body.tokensSaved > 0, `tokensSaved should be > 0, got: ${body.tokensSaved}`);
assert.ok(body.savingsPct > 0, `savingsPct should be > 0, got: ${body.savingsPct}`);
// Response shape completeness checks
assert.ok(typeof body.original === "string", "should have original field");
assert.ok(typeof body.compressed === "string", "should have compressed field");
assert.ok(Array.isArray(body.techniquesUsed), "techniquesUsed should be an array");
});
test("POST /api/compression/preview with engineId but no mode still works (mode defaults to stacked)", async () => {
const messages = buildHeadroomMessages();
const request = await makeManagementSessionRequest("http://localhost/api/compression/preview", {
method: "POST",
body: {
engineId: "headroom",
messages,
// mode intentionally omitted — should default to "stacked" when engineId present
},
});
const response = await previewRoute.POST(request);
assert.equal(
response.status,
200,
`Expected 200 when mode omitted with engineId, got ${response.status}`
);
const body = (await response.json()) as { tokensSaved: number };
assert.ok(body.tokensSaved > 0, `tokensSaved should be > 0 even without explicit mode`);
});
test("POST /api/compression/preview without engineId still works (existing path untouched)", async () => {
const request = await makeManagementSessionRequest("http://localhost/api/compression/preview", {
method: "POST",
body: {
messages: [{ role: "user", content: "Hello world" }],
mode: "standard",
},
});
const response = await previewRoute.POST(request);
assert.equal(
response.status,
200,
`Existing path should still return 200, got ${response.status}`
);
const body = (await response.json()) as { original: string; mode: string };
assert.ok(typeof body.original === "string", "should have original field");
assert.equal(body.mode, "standard", "mode field should reflect the requested mode");
});
@@ -0,0 +1,223 @@
import { describe, it, beforeEach, afterEach, after } from "node:test";
import assert from "node:assert/strict";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
describe("Compression Settings API Schema Validation", () => {
const compressionModeValues = [
"off",
"lite",
"standard",
"aggressive",
"ultra",
"rtk",
"stacked",
];
it("should validate all compression mode values", () => {
assert.deepStrictEqual(compressionModeValues, [
"off",
"lite",
"standard",
"aggressive",
"ultra",
"rtk",
"stacked",
]);
});
it("should validate caveman config structure", () => {
const defaultCavemanConfig = {
enabled: true,
compressRoles: ["user"],
skipRules: [],
minMessageLength: 50,
preservePatterns: [],
};
assert.equal(defaultCavemanConfig.enabled, true);
assert.deepStrictEqual(defaultCavemanConfig.compressRoles, ["user"]);
assert.equal(Array.isArray(defaultCavemanConfig.skipRules), true);
assert.equal(defaultCavemanConfig.minMessageLength, 50);
assert.equal(Array.isArray(defaultCavemanConfig.preservePatterns), true);
});
it("should validate full compression config structure", () => {
const defaultConfig = {
enabled: false,
defaultMode: "off",
autoTriggerTokens: 0,
cacheMinutes: 5,
preserveSystemPrompt: true,
comboOverrides: {},
cavemanConfig: {
enabled: true,
compressRoles: ["user"],
skipRules: [],
minMessageLength: 50,
preservePatterns: [],
},
ultra: {
enabled: false,
compressionRate: 0.5,
minScoreThreshold: 0.3,
slmFallbackToAggressive: true,
maxTokensPerMessage: 0,
},
};
assert.equal(defaultConfig.enabled, false);
assert.ok(compressionModeValues.includes(defaultConfig.defaultMode));
assert.equal(typeof defaultConfig.autoTriggerTokens, "number");
assert.equal(typeof defaultConfig.cacheMinutes, "number");
assert.equal(typeof defaultConfig.preserveSystemPrompt, "boolean");
assert.equal(typeof defaultConfig.comboOverrides, "object");
assert.equal(typeof defaultConfig.cavemanConfig, "object");
assert.equal(typeof defaultConfig.ultra, "object");
assert.equal(defaultConfig.ultra.compressionRate, 0.5);
});
it("should validate all caveman compression rules are defined", async () => {
const { CAVEMAN_RULES } =
await import("../../../../open-sse/services/compression/cavemanRules.ts");
assert.ok(Array.isArray(CAVEMAN_RULES));
assert.ok(CAVEMAN_RULES.length >= 29, `Expected >= 29 rules, got ${CAVEMAN_RULES.length}`);
for (const rule of CAVEMAN_RULES) {
assert.ok(rule.name && typeof rule.name === "string", `Rule must have a name`);
assert.ok(rule.pattern instanceof RegExp, `Rule ${rule.name} must have a RegExp pattern`);
assert.ok(
typeof rule.replacement === "string" || typeof rule.replacement === "function",
`Rule ${rule.name} must have string or function replacement`
);
assert.ok(
rule.pattern.source !== "^$" || rule.replacement !== "",
`Rule ${rule.name} must not be a no-op (empty pattern + empty replacement)`
);
}
});
it("should validate compression modes cover all CavemanConfig roles", () => {
const validRoles = ["user", "assistant", "system"];
for (const role of validRoles) {
assert.ok(validRoles.includes(role), `Role ${role} should be valid`);
}
assert.equal(validRoles.length, 3);
});
});
// ─── Route round-trip: engines map + activeComboId ─────────────────────────
// Mirrors the mcp-accessibility-config test harness: allocate a temp DATA_DIR,
// import route + DB modules, tear down in after().
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-compression-route-"));
const ORIGINAL_DATA_DIR = process.env.DATA_DIR;
process.env.DATA_DIR = TEST_DATA_DIR;
const core = await import("../../../../src/lib/db/core.ts");
const route = await import("../../../../src/app/api/settings/compression/route.ts");
function makeRequest(method: string, body?: unknown): Request {
return new Request("http://localhost/api/settings/compression", {
method,
headers: body !== undefined ? { "content-type": "application/json" } : {},
body: body !== undefined ? JSON.stringify(body) : undefined,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}) as any;
}
describe("settings/compression route — engines + activeComboId", () => {
beforeEach(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
});
after(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
if (ORIGINAL_DATA_DIR === undefined) delete process.env.DATA_DIR;
else process.env.DATA_DIR = ORIGINAL_DATA_DIR;
});
it("PUT engines map persists and GET returns engines + activeComboId", async () => {
const putRes = await route.PUT(
makeRequest("PUT", { engines: { rtk: { enabled: true, level: "standard" } } })
);
assert.equal(putRes.status, 200);
// Fresh DB handle so we read from storage, not from the write-path return value.
core.resetDbInstance();
const getRes = await route.GET(makeRequest("GET"));
assert.equal(getRes.status, 200);
const body = await getRes.json();
assert.equal(body.engines?.rtk?.enabled, true, "engines.rtk.enabled should be true after PUT");
assert.equal(
body.engines?.rtk?.level,
"standard",
"engines.rtk.level should be 'standard' after PUT"
);
// activeComboId is always present (null by default)
assert.ok("activeComboId" in body, "GET response must include activeComboId");
});
it("PUT activeComboId persists and is returned by GET", async () => {
const putRes = await route.PUT(makeRequest("PUT", { activeComboId: "combo-abc" }));
assert.equal(putRes.status, 200);
core.resetDbInstance();
const getRes = await route.GET(makeRequest("GET"));
assert.equal(getRes.status, 200);
const body = await getRes.json();
assert.equal(body.activeComboId, "combo-abc");
});
it("PUT activeComboId:null clears the active combo", async () => {
// First set it, then clear.
await route.PUT(makeRequest("PUT", { activeComboId: "combo-to-clear" }));
core.resetDbInstance();
await route.PUT(makeRequest("PUT", { activeComboId: null }));
core.resetDbInstance();
const getRes = await route.GET(makeRequest("GET"));
assert.equal(getRes.status, 200);
const body = await getRes.json();
assert.equal(body.activeComboId, null);
});
it("PUT with invalid engines shape is rejected by schema validation (400)", async () => {
// engines values must have an `enabled` boolean — passing a string should fail the schema.
const putRes = await route.PUT(makeRequest("PUT", { engines: { rtk: { enabled: "yes" } } }));
assert.equal(putRes.status, 400);
const body = await putRes.json();
// Validation failures use { error: { message, details } } via validateBody helper.
assert.ok(body.error !== null && typeof body.error === "object", "error should be an object");
const errorMessage: string =
typeof body.error === "string"
? body.error
: (body.error?.message ?? JSON.stringify(body.error));
assert.ok(!errorMessage.includes("at /"), "error must not contain a stack trace");
});
it("PUT accepts enginesExplicit (round-tripped from GET response)", async () => {
// Regression: the GET handler injects `enginesExplicit` (compression.ts:632) so the
// hub/panel can round-trip the full settings object. The previous .strict() PUT schema
// rejected it with 400 ("Unrecognized key: enginesExplicit"), causing every toggle on
// the Compression Hub / Panel to revert. Allow it through.
const putRes = await route.PUT(makeRequest("PUT", { enabled: true, enginesExplicit: true }));
assert.equal(putRes.status, 200);
core.resetDbInstance();
const putRes2 = await route.PUT(makeRequest("PUT", { enabled: false, enginesExplicit: false }));
assert.equal(putRes2.status, 200);
});
});
@@ -0,0 +1,115 @@
/**
* TDD for the RTK learn/discover API routes (F2.1 / Item 2).
*
* GET /api/context/rtk/discover → ranked noise candidates mined from the
* opt-in rtk raw-output sample store.
* GET /api/context/rtk/learn?command=X → a suggested RTK filter draft for command X.
*
* Auth: requireManagementAuth — not required in the unconfigured/first-run state
* (fresh temp DATA_DIR, no INITIAL_PASSWORD), so these exercise the 200 happy path.
* The pure miners (discoverRepeatedNoise/suggestFilter) and the sample adapter
* (listRtkCommandSamples) are unit-tested separately; this asserts the wiring.
*
* Run: node --import tsx/esm --test tests/unit/api/compression/rtk-learn-discover-routes.test.ts
*/
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-rtk-ld-routes-"));
const ORIGINAL_DATA_DIR = process.env.DATA_DIR;
const ORIGINAL_INITIAL_PASSWORD = process.env.INITIAL_PASSWORD;
process.env.DATA_DIR = TEST_DATA_DIR;
delete process.env.INITIAL_PASSWORD;
const { maybePersistRtkRawOutput } = await import(
"../../../../open-sse/services/compression/engines/rtk/index.ts"
);
const discoverRoute = await import("../../../../src/app/api/context/rtk/discover/route.ts");
const learnRoute = await import("../../../../src/app/api/context/rtk/learn/route.ts");
function seedSamples() {
// Three runs of the same command with a shared noise line + a unique line each.
for (let i = 0; i < 3; i++) {
maybePersistRtkRawOutput(
`Resolving dependencies...\nnpm warn deprecated foo@1.0.0\nadded ${i} packages in ${i}s\n`,
{ retention: "always", command: "npm install" }
);
}
}
function get(url: string): Request {
return new Request(url, { method: "GET" });
}
test.beforeEach(() => {
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
});
test.after(() => {
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
if (ORIGINAL_DATA_DIR === undefined) delete process.env.DATA_DIR;
else process.env.DATA_DIR = ORIGINAL_DATA_DIR;
if (ORIGINAL_INITIAL_PASSWORD !== undefined)
process.env.INITIAL_PASSWORD = ORIGINAL_INITIAL_PASSWORD;
});
test("GET /discover — returns ranked noise candidates + sampleCount", async () => {
seedSamples();
const res = await discoverRoute.GET(get("http://localhost/api/context/rtk/discover"));
assert.equal(res.status, 200);
const body = (await res.json()) as { sampleCount: number; candidates: Array<{ hits: number }> };
assert.equal(body.sampleCount, 3);
assert.ok(Array.isArray(body.candidates));
assert.ok(body.candidates.length > 0, "the shared 'Resolving dependencies' line is a candidate");
});
test("GET /discover — empty store → 200 with no candidates", async () => {
const res = await discoverRoute.GET(get("http://localhost/api/context/rtk/discover"));
assert.equal(res.status, 200);
const body = (await res.json()) as { sampleCount: number; candidates: unknown[] };
assert.equal(body.sampleCount, 0);
assert.equal(body.candidates.length, 0);
});
test("GET /learn?command=npm install — returns a suggested filter learned from matching samples", async () => {
seedSamples();
const res = await learnRoute.GET(
get("http://localhost/api/context/rtk/learn?command=" + encodeURIComponent("npm install"))
);
assert.equal(res.status, 200);
const body = (await res.json()) as {
command: string;
sampleCount: number;
filter: { id: string; match: { commands: string[] } };
};
assert.equal(body.command, "npm install");
assert.equal(body.sampleCount, 3, "only the matching command's samples are learned from");
assert.ok(body.filter.id.length > 0);
assert.ok(body.filter.match.commands.length > 0);
});
test("GET /learn without a command → 400", async () => {
const res = await learnRoute.GET(get("http://localhost/api/context/rtk/learn"));
assert.equal(res.status, 400);
const body = (await res.json()) as { error: { message?: string } | string };
// Error must not leak a stack trace (Hard Rule #12).
const msg = typeof body.error === "string" ? body.error : (body.error?.message ?? "");
assert.ok(!msg.includes("at /"), "no stack trace in error body");
});
test("GET /learn filters out other commands' samples", async () => {
seedSamples(); // 3× "npm install"
maybePersistRtkRawOutput("Compiling cargo...\nFinished in 2s\n", {
retention: "always",
command: "cargo build",
});
const res = await learnRoute.GET(
get("http://localhost/api/context/rtk/learn?command=" + encodeURIComponent("npm install"))
);
const body = (await res.json()) as { sampleCount: number };
assert.equal(body.sampleCount, 3, "the cargo sample is excluded");
});
@@ -0,0 +1,138 @@
/**
* TDD: GET /api/context/analytics/engine?engineId=&days=
*
* Auth + isolation pattern mirrors tests/unit/api/compression-preview-engine.test.ts:
* - makeManagementSessionRequest() for JWT cookie auth.
* - Temp DATA_DIR, resetDbInstance() before each test, cleanup in test.after().
*/
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";
import { makeManagementSessionRequest } from "../../helpers/managementSession.ts";
// ─── isolated temp DB ─────────────────────────────────────────────────────────
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-ctx-analytics-engine-"));
const originalDataDir = process.env.DATA_DIR;
const originalJwtSecret = process.env.JWT_SECRET;
process.env.DATA_DIR = TEST_DATA_DIR;
const core = await import("../../../src/lib/db/core.ts");
const settingsDb = await import("../../../src/lib/db/settings.ts");
const { insertCompressionAnalyticsRow } =
await import("../../../src/lib/db/compressionAnalytics.ts");
const engineRoute = await import("../../../src/app/api/context/analytics/engine/route.ts");
// ─── helpers ──────────────────────────────────────────────────────────────────
async function setupAuth(): Promise<void> {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
await settingsDb.updateSettings({
requireLogin: true,
setupComplete: true,
password: "test-password-hash",
});
}
// ─── lifecycle ────────────────────────────────────────────────────────────────
test.beforeEach(async () => {
process.env.DATA_DIR = TEST_DATA_DIR;
await setupAuth();
});
test.after(() => {
if (originalDataDir === undefined) delete process.env.DATA_DIR;
else process.env.DATA_DIR = originalDataDir;
if (originalJwtSecret === undefined) delete process.env.JWT_SECRET;
else process.env.JWT_SECRET = originalJwtSecret;
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
// ─── tests ────────────────────────────────────────────────────────────────────
test("GET /api/context/analytics/engine returns 400 when engineId is missing", async () => {
const req = await makeManagementSessionRequest("http://localhost/api/context/analytics/engine");
const res = await engineRoute.GET(req);
assert.equal(res.status, 400, `Expected 400, got ${res.status}`);
const body = (await res.json()) as { error: string };
assert.ok(typeof body.error === "string", "response should have an error string");
});
test("GET /api/context/analytics/engine returns 200 with correct aggregation for headroom", async () => {
const now = new Date().toISOString();
insertCompressionAnalyticsRow({
timestamp: now,
mode: "stacked",
engine: "headroom",
original_tokens: 1000,
compressed_tokens: 800,
tokens_saved: 200,
});
insertCompressionAnalyticsRow({
timestamp: now,
mode: "stacked",
engine: "headroom",
original_tokens: 500,
compressed_tokens: 350,
tokens_saved: 150,
});
// caveman row — must NOT appear in headroom result
insertCompressionAnalyticsRow({
timestamp: now,
mode: "stacked",
engine: "caveman",
original_tokens: 800,
compressed_tokens: 600,
tokens_saved: 200,
});
const req = await makeManagementSessionRequest(
"http://localhost/api/context/analytics/engine?engineId=headroom"
);
const res = await engineRoute.GET(req);
assert.equal(res.status, 200, `Expected 200, got ${res.status}`);
const body = (await res.json()) as {
engineId: string;
runs: number;
tokensSaved: number;
avgSavingsPercent: number;
days: number;
};
assert.equal(body.engineId, "headroom");
assert.equal(body.runs, 2, "should count only the 2 headroom rows");
assert.equal(body.tokensSaved, 350, "should sum tokens_saved for headroom rows");
assert.equal(typeof body.avgSavingsPercent, "number", "avgSavingsPercent should be a number");
assert.equal(body.days, 7, "default days should be 7");
});
test("GET /api/context/analytics/engine respects ?days= parameter", async () => {
const req = await makeManagementSessionRequest(
"http://localhost/api/context/analytics/engine?engineId=headroom&days=30"
);
const res = await engineRoute.GET(req);
assert.equal(res.status, 200, `Expected 200, got ${res.status}`);
const body = (await res.json()) as { days: number };
assert.equal(body.days, 30, "should pass days=30 through to the result");
});
test("GET /api/context/analytics/engine returns 200 with zero metrics for unknown engine", async () => {
const req = await makeManagementSessionRequest(
"http://localhost/api/context/analytics/engine?engineId=nonexistent"
);
const res = await engineRoute.GET(req);
assert.equal(res.status, 200, `Expected 200, got ${res.status}`);
const body = (await res.json()) as { runs: number; tokensSaved: number };
assert.equal(body.runs, 0);
assert.equal(body.tokensSaved, 0);
});
@@ -0,0 +1,164 @@
/**
* TDD: GET /api/context/combos/default is a read-only shim.
*
* The default compression pipeline is now DERIVED from the engines map
* (open-sse deriveDefaultPlan) rather than editable here:
* - GET → returns the derived default plan for the live config.
* - PUT → rejected with a deprecation error (not 200); body carries no stack trace.
*
* Auth + isolation pattern mirrors tests/unit/api/context-analytics-engine-route.test.ts:
* - makeManagementSessionRequest() for JWT cookie auth.
* - Temp DATA_DIR, resetDbInstance() before each test, cleanup in test.after().
*/
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";
import { makeManagementSessionRequest } from "../../helpers/managementSession.ts";
import { deriveDefaultPlan } from "@omniroute/open-sse/services/compression/deriveDefaultPlan.ts";
// ─── isolated temp DB ─────────────────────────────────────────────────────────
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-ctx-combos-default-route-"));
const originalDataDir = process.env.DATA_DIR;
const originalJwtSecret = process.env.JWT_SECRET;
process.env.DATA_DIR = TEST_DATA_DIR;
const core = await import("../../../src/lib/db/core.ts");
const settingsDb = await import("../../../src/lib/db/settings.ts");
const compressionDb = await import("../../../src/lib/db/compression.ts");
const defaultRoute = await import("../../../src/app/api/context/combos/default/route.ts");
// ─── helpers ──────────────────────────────────────────────────────────────────
async function setupAuth(): Promise<void> {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
await settingsDb.updateSettings({
requireLogin: true,
setupComplete: true,
password: "test-password-hash",
});
}
// ─── lifecycle ────────────────────────────────────────────────────────────────
test.beforeEach(async () => {
process.env.DATA_DIR = TEST_DATA_DIR;
await setupAuth();
});
test.after(() => {
if (originalDataDir === undefined) delete process.env.DATA_DIR;
else process.env.DATA_DIR = originalDataDir;
if (originalJwtSecret === undefined) delete process.env.JWT_SECRET;
else process.env.JWT_SECRET = originalJwtSecret;
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
// ─── tests ────────────────────────────────────────────────────────────────────
test("GET /api/context/combos/default returns the derived default plan (single-mode caveman)", async () => {
await compressionDb.updateCompressionSettings({
enabled: true,
engines: { caveman: { enabled: true, level: "full" } },
});
const req = await makeManagementSessionRequest("http://localhost/api/context/combos/default");
const res = await defaultRoute.GET(req);
assert.equal(res.status, 200, `Expected 200, got ${res.status}`);
const body = (await res.json()) as {
mode: string;
pipeline: Array<{ engine: string; intensity?: string }>;
};
// caveman alone is single-mode → effective mode "standard", empty stacked pipeline.
const expected = deriveDefaultPlan({ caveman: { enabled: true, level: "full" } }, true);
assert.equal(body.mode, expected.mode, `expected mode ${expected.mode}`);
assert.equal(body.mode, "standard");
assert.deepEqual(body.pipeline, expected.stackedPipeline);
});
test("GET /api/context/combos/default returns the derived stacked pipeline (reflects enabled engines)", async () => {
await compressionDb.updateCompressionSettings({
enabled: true,
engines: {
caveman: { enabled: true, level: "full" },
headroom: { enabled: true },
},
});
const req = await makeManagementSessionRequest("http://localhost/api/context/combos/default");
const res = await defaultRoute.GET(req);
assert.equal(res.status, 200, `Expected 200, got ${res.status}`);
const body = (await res.json()) as {
mode: string;
pipeline: Array<{ engine: string; intensity?: string }>;
};
const expected = deriveDefaultPlan(
{ caveman: { enabled: true, level: "full" }, headroom: { enabled: true } },
true
);
assert.equal(body.mode, "stacked");
assert.deepEqual(body.pipeline, expected.stackedPipeline);
const engineIds = body.pipeline.map((s) => s.engine);
assert.ok(engineIds.includes("caveman"), `expected caveman in derived pipeline, got: ${engineIds}`);
});
test("GET /api/context/combos/default returns off when master switch is disabled", async () => {
await compressionDb.updateCompressionSettings({
enabled: false,
engines: { caveman: { enabled: true, level: "full" } },
});
const req = await makeManagementSessionRequest("http://localhost/api/context/combos/default");
const res = await defaultRoute.GET(req);
assert.equal(res.status, 200, `Expected 200, got ${res.status}`);
const body = (await res.json()) as { mode: string; pipeline: unknown[] };
assert.equal(body.mode, "off");
assert.deepEqual(body.pipeline, []);
});
test("PUT /api/context/combos/default is deprecated and rejects writes (not 200)", async () => {
const req = await makeManagementSessionRequest("http://localhost/api/context/combos/default", {
method: "PUT",
body: JSON.stringify({ engineId: "headroom", enabled: true }),
});
const res = await defaultRoute.PUT(req);
assert.notEqual(res.status, 200, "PUT must no longer succeed — the route is read-only");
assert.ok(res.status >= 400, `expected a 4xx deprecation status, got ${res.status}`);
const body = (await res.json()) as { error?: { message?: string } | string };
// The deprecation message points editors at the engines settings.
const message =
typeof body.error === "string" ? body.error : (body.error?.message ?? JSON.stringify(body));
assert.match(message, /derived|engines|deprecat/i, `unexpected deprecation message: ${message}`);
// Hard Rule #12: no raw stack trace leaks into the response body. Match the V8
// stack-frame shape (" at fn (file:line:col)") rather than the bare "at " token,
// which legitimately appears in the URL inside the deprecation message.
assert.ok(
!/\bat\s+\S+\s+\(?\/?\S+:\d+:\d+/.test(JSON.stringify(body)),
"error body must not contain a stack trace"
);
assert.ok(!/\n\s+at\s/.test(JSON.stringify(body)), "error body must not contain a stack trace");
});
test("POST /api/context/combos/default is deprecated and rejects writes (not 200)", async () => {
const req = await makeManagementSessionRequest("http://localhost/api/context/combos/default", {
method: "POST",
body: JSON.stringify({ engineId: "headroom", enabled: true }),
});
const res = await defaultRoute.POST(req);
assert.notEqual(res.status, 200, "POST must no longer succeed — the route is read-only");
assert.ok(res.status >= 400, `expected a 4xx deprecation status, got ${res.status}`);
});
+155
View File
@@ -0,0 +1,155 @@
import { describe, test, before, after } from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
// Isolated DB + auth disabled so requireManagementAuth passes (no key configured).
let tmpDataDir: string;
let core: typeof import("@/lib/db/core");
let db: typeof import("@/lib/db/discoveryResults");
let resultsRoute: typeof import("@/app/api/discovery/results/route");
let resultByIdRoute: typeof import("@/app/api/discovery/results/[id]/route");
let scanRoute: typeof import("@/app/api/discovery/scan/route");
let verifyRoute: typeof import("@/app/api/discovery/verify/[id]/route");
function req(method: string, url: string, body?: unknown): Request {
return new Request(`http://localhost${url}`, {
method,
headers: { "content-type": "application/json" },
body: body === undefined ? undefined : JSON.stringify(body),
});
}
before(async () => {
tmpDataDir = mkdtempSync(join(tmpdir(), "omniroute-discovery-routes-"));
process.env.DATA_DIR = tmpDataDir;
delete process.env.REQUIRE_API_KEY;
process.env.OMNIROUTE_DISABLE_AUTH = "1";
core = await import("@/lib/db/core");
core.resetDbInstance();
core.getDbInstance();
db = await import("@/lib/db/discoveryResults");
resultsRoute = await import("@/app/api/discovery/results/route");
resultByIdRoute = await import("@/app/api/discovery/results/[id]/route");
scanRoute = await import("@/app/api/discovery/scan/route");
verifyRoute = await import("@/app/api/discovery/verify/[id]/route");
});
after(() => {
core.resetDbInstance();
if (tmpDataDir) rmSync(tmpDataDir, { recursive: true, force: true });
});
describe("discovery API routes", () => {
test("GET /results lists persisted findings (and filters by providerId)", async () => {
db.upsertDiscoveryResult({
providerId: "acme",
method: "free_tier",
authType: "none",
feasibility: 3,
riskLevel: "none",
status: "pending",
});
const res = await resultsRoute.GET(req("GET", "/api/discovery/results?providerId=acme"));
assert.equal(res.status, 200);
const body = await res.json();
assert.ok(Array.isArray(body.results));
assert.ok(body.results.some((r: { providerId: string }) => r.providerId === "acme"));
});
test("GET /results/:id returns the row, 404 when missing, 400 on bad id", async () => {
const created = db.upsertDiscoveryResult({
providerId: "beta",
method: "trial",
authType: "api_key",
feasibility: 2,
riskLevel: "low",
status: "pending",
});
const ok = await resultByIdRoute.GET(req("GET", `/api/discovery/results/${created.id}`), {
params: Promise.resolve({ id: String(created.id) }),
});
assert.equal(ok.status, 200);
const missing = await resultByIdRoute.GET(req("GET", "/api/discovery/results/999999"), {
params: Promise.resolve({ id: "999999" }),
});
assert.equal(missing.status, 404);
const bad = await resultByIdRoute.GET(req("GET", "/api/discovery/results/abc"), {
params: Promise.resolve({ id: "abc" }),
});
assert.equal(bad.status, 400);
});
test("POST /scan persists findings; rejects an empty providerId with 400", async () => {
const res = await scanRoute.POST(req("POST", "/api/discovery/scan", { providerId: "gamma" }));
assert.equal(res.status, 200);
const body = await res.json();
assert.ok(Array.isArray(body.results) && body.results.length > 0);
assert.ok(body.results[0].id > 0);
// the persisted row is now queryable
assert.ok(db.getDiscoveryResults("gamma").length > 0);
const invalid = await scanRoute.POST(req("POST", "/api/discovery/scan", { providerId: "" }));
assert.equal(invalid.status, 400);
const malformed = new Request("http://localhost/api/discovery/scan", {
method: "POST",
headers: { "content-type": "application/json" },
body: "{not json",
});
const malformedRes = await scanRoute.POST(malformed);
assert.equal(malformedRes.status, 400);
});
test("POST /verify/:id marks verified, 404 when missing", async () => {
const created = db.upsertDiscoveryResult({
providerId: "delta",
method: "public_api",
authType: "api_key",
feasibility: 5,
riskLevel: "none",
status: "pending",
});
const res = await verifyRoute.POST(req("POST", `/api/discovery/verify/${created.id}`), {
params: Promise.resolve({ id: String(created.id) }),
});
assert.equal(res.status, 200);
const body = await res.json();
assert.equal(body.result.status, "verified");
const missing = await verifyRoute.POST(req("POST", "/api/discovery/verify/999999"), {
params: Promise.resolve({ id: "999999" }),
});
assert.equal(missing.status, 404);
});
test("DELETE /results/:id removes the row, 404 on second delete", async () => {
const created = db.upsertDiscoveryResult({
providerId: "epsilon",
method: "free_tier",
authType: "none",
feasibility: 1,
riskLevel: "none",
status: "pending",
});
const first = await resultByIdRoute.DELETE(req("DELETE", `/api/discovery/results/${created.id}`), {
params: Promise.resolve({ id: String(created.id) }),
});
assert.equal(first.status, 200);
const second = await resultByIdRoute.DELETE(req("DELETE", `/api/discovery/results/${created.id}`), {
params: Promise.resolve({ id: String(created.id) }),
});
assert.equal(second.status, 404);
});
test("error responses do not leak stack traces", async () => {
const missing = await resultByIdRoute.GET(req("GET", "/api/discovery/results/424242"), {
params: Promise.resolve({ id: "424242" }),
});
const body = await missing.json();
assert.ok(!String(body.error?.message ?? "").includes("at /"));
});
});
@@ -0,0 +1,40 @@
import test from "node:test";
import assert from "node:assert/strict";
import { resolveServerErrorMessage } from "../../../src/lib/api/serverErrorMessage.ts";
const FALLBACK = "Failed to toggle combo";
test("prefers error.details[0].message (field-level / COMBO_002 shape)", () => {
const body = {
error: {
message: "One or more combo fields are invalid",
details: [{ field: "config.compressionMode", message: "Invalid compression mode" }],
},
};
assert.equal(resolveServerErrorMessage(body, FALLBACK), "Invalid compression mode");
});
test("falls back to error.message when there are no details", () => {
const body = { error: { message: "Combo not found" } };
assert.equal(resolveServerErrorMessage(body, FALLBACK), "Combo not found");
});
test("falls back to error.message when details[0] has no message", () => {
const body = { error: { message: "Bad request", details: [{ field: "body" }] } };
assert.equal(resolveServerErrorMessage(body, FALLBACK), "Bad request");
});
test("returns fallback for null body (failed res.json())", () => {
assert.equal(resolveServerErrorMessage(null, FALLBACK), FALLBACK);
});
test("returns fallback for a body without an error object", () => {
assert.equal(resolveServerErrorMessage({ ok: false }, FALLBACK), FALLBACK);
assert.equal(resolveServerErrorMessage("nope", FALLBACK), FALLBACK);
assert.equal(resolveServerErrorMessage({ error: "string-not-object" }, FALLBACK), FALLBACK);
});
test("returns fallback when message fields are empty strings", () => {
const body = { error: { message: "", details: [{ message: "" }] } };
assert.equal(resolveServerErrorMessage(body, FALLBACK), FALLBACK);
});
@@ -0,0 +1,132 @@
/**
* Tests for GET /api/services/9router/models
*
* Uses an isolated in-memory DB and mocks fetch + service registry.
*/
import { describe, it, beforeEach, afterEach } 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-9router-models-api-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.NODE_ENV = "test";
process.env.DISABLE_SQLITE_AUTO_BACKUP = "true";
process.env.NINEROUTER_PORT = "20130";
// Must import db core first so the DB is initialised at the test path.
const core = await import("../../../../src/lib/db/core.ts");
const { saveServiceModels, getServiceModels } =
await import("../../../../src/lib/db/serviceModels.ts");
// Import the route handler under test (after env is set).
const { GET } = await import("../../../../src/app/api/services/9router/models/route.ts");
const originalFetch = globalThis.fetch;
function resetDb() {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}
function makeRequest(url: string): Request {
return new Request(url);
}
function makeFetch(status: number, body: unknown): typeof fetch {
return async () =>
new Response(JSON.stringify(body), {
status,
headers: { "Content-Type": "application/json" },
});
}
beforeEach(() => {
resetDb();
globalThis.fetch = originalFetch;
});
afterEach(() => {
globalThis.fetch = originalFetch;
});
describe("GET /api/services/9router/models", () => {
it("returns { data: [] } when no models are stored", async () => {
const req = makeRequest("http://localhost/api/services/9router/models");
const res = await GET(req);
assert.equal(res.status, 200);
const body = await res.json();
assert.ok("data" in body, "response should have a data property");
assert.deepEqual(body.data, []);
});
it("returns stored models as { data: [...] }", async () => {
saveServiceModels("9router", [
{ id: "9router/cx/gpt-5-mini", name: "GPT-5 mini", available: true },
{ id: "9router/auto/sonnet", name: "Sonnet", available: true },
]);
const req = makeRequest("http://localhost/api/services/9router/models");
const res = await GET(req);
assert.equal(res.status, 200);
const body = await res.json();
assert.equal(body.data.length, 2);
assert.equal(body.data[0].id, "9router/cx/gpt-5-mini");
assert.equal(body.data[1].id, "9router/auto/sonnet");
});
it("?refresh=true triggers a sync before returning", async () => {
// Pre-seed with one model so we can tell that the sync ran.
saveServiceModels("9router", [{ id: "9router/old-model", available: true }]);
let fetchCalled = false;
globalThis.fetch = async () => {
fetchCalled = true;
return new Response(
// Upstream returns an unprefixed id — sync should prefix it as "9router/new-model".
JSON.stringify({ data: [{ id: "new-model", object: "model" }] }),
{ status: 200, headers: { "Content-Type": "application/json" } }
);
};
const req = makeRequest("http://localhost/api/services/9router/models?refresh=true");
const res = await GET(req);
assert.equal(res.status, 200);
assert.ok(fetchCalled, "?refresh=true should have triggered a fetch to the service");
const body = await res.json();
// After sync, new model should be present prefixed (old model pruned to unavailable).
const ids = body.data.map((m: { id: string }) => m.id);
assert.ok(ids.includes("9router/new-model"), "synced model should appear in response");
});
it("?refresh=false (default) does NOT trigger a sync", async () => {
saveServiceModels("9router", [{ id: "9router/cached-model", available: true }]);
let fetchCalled = false;
globalThis.fetch = async () => {
fetchCalled = true;
return new Response(JSON.stringify({ data: [] }), { status: 200 });
};
const req = makeRequest("http://localhost/api/services/9router/models");
const res = await GET(req);
assert.equal(res.status, 200);
assert.equal(fetchCalled, false, "GET without ?refresh=true should NOT call fetch");
});
it("response shape always has a data array", async () => {
const req = makeRequest("http://localhost/api/services/9router/models");
const res = await GET(req);
const body = await res.json();
assert.ok(Array.isArray(body.data), "data must always be an array");
});
});
@@ -0,0 +1,100 @@
/**
* Tests for POST /api/services/9router/provider-expose
*/
import { describe, it, beforeEach } 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-9router-provider-expose-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.NODE_ENV = "test";
process.env.DISABLE_SQLITE_AUTO_BACKUP = "true";
// Initialise DB first.
const core = await import("../../../../src/lib/db/core.ts");
const { upsertVersionManagerTool, getVersionManagerTool } =
await import("../../../../src/lib/db/versionManager.ts");
// Import route under test.
const { POST } = await import("../../../../src/app/api/services/9router/provider-expose/route.ts");
function resetDb() {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}
function makeRequest(body: unknown): Request {
return new Request("http://localhost/api/services/9router/provider-expose", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
}
beforeEach(() => {
resetDb();
});
describe("POST /api/services/9router/provider-expose", () => {
it("returns 400 for invalid JSON body", async () => {
const req = new Request("http://localhost/api/services/9router/provider-expose", {
method: "POST",
body: "not-json",
headers: { "Content-Type": "application/json" },
});
const res = await POST(req);
assert.equal(res.status, 400);
});
it("returns 400 when enabled is missing", async () => {
const req = makeRequest({});
const res = await POST(req);
assert.equal(res.status, 400);
const body = await res.json();
assert.ok(body?.error?.message ?? body?.message, "should have an error message");
});
it("returns 400 when enabled is not a boolean", async () => {
const req = makeRequest({ enabled: "yes" });
const res = await POST(req);
assert.equal(res.status, 400);
});
it("sets providerExpose to true and returns 204", async () => {
await upsertVersionManagerTool({ tool: "9router", status: "stopped" });
const req = makeRequest({ enabled: true });
const res = await POST(req);
assert.equal(res.status, 204);
const row = await getVersionManagerTool("9router");
assert.equal(row?.providerExpose, true, "providerExpose should be true in DB");
});
it("sets providerExpose to false and returns 204", async () => {
await upsertVersionManagerTool({ tool: "9router", status: "stopped" });
// First enable it.
await POST(makeRequest({ enabled: true }));
// Then disable it.
const res = await POST(makeRequest({ enabled: false }));
assert.equal(res.status, 204);
const row = await getVersionManagerTool("9router");
assert.equal(row?.providerExpose, false, "providerExpose should be false in DB");
});
it("error response does not leak stack traces", async () => {
// Missing body triggers 400 — verify the message doesn't contain stack frames.
const req = makeRequest({});
const res = await POST(req);
assert.equal(res.status, 400);
const text = await res.text();
assert.ok(!text.includes("at /"), "error body must not expose stack trace");
});
});
@@ -0,0 +1,127 @@
/**
* Tests for GET /api/services/9router/status?reveal=key (R-01)
*
* - Without ?reveal: returns only masked key
* - With ?reveal=key but missing X-Reveal-Confirm: returns 403
* - With ?reveal=key and X-Reveal-Confirm: yes: returns apiKeyPlain + logs audit
*/
import { describe, it, before, after, beforeEach } 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-9router-reveal-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.NODE_ENV = "test";
process.env.DISABLE_SQLITE_AUTO_BACKUP = "true";
// Bootstrap DB and seed service row
const core = await import("../../../../src/lib/db/core.ts");
const db = core.getDbInstance();
db.prepare(
`INSERT OR IGNORE INTO version_manager (tool, status, port, auto_start, auto_update, provider_expose)
VALUES ('9router', 'stopped', 20130, 0, 1, 1)`
).run();
const { updateVersionManagerTool } = await import("../../../../src/lib/db/versionManager.ts");
await updateVersionManagerTool("9router", {
installedVersion: "0.4.59",
status: "stopped",
});
// Ensure audit_log table exists (compliance init)
const { initAuditLog, countAuditLog } = await import("../../../../src/lib/compliance/index.ts");
initAuditLog();
// Import GET after env is set
const { GET } =
await import("../../../../src/app/api/services/9router/status/route.ts?t=reveal-suite");
function makeRequest(url: string, headers?: Record<string, string>): Request {
return new Request(url, { headers });
}
after(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
describe("GET /api/services/9router/status", () => {
it("without ?reveal returns only masked key (no plain)", async () => {
const req = makeRequest("http://localhost/api/services/9router/status");
const res = await GET(req);
assert.equal(res.status, 200);
const body = await res.json();
assert.ok("apiKeyMasked" in body, "should have apiKeyMasked");
assert.ok(!("apiKeyPlain" in body), "should NOT have apiKeyPlain");
});
it("?reveal=key without X-Reveal-Confirm returns 403", async () => {
const req = makeRequest("http://localhost/api/services/9router/status?reveal=key");
const res = await GET(req);
assert.equal(res.status, 403);
const body = await res.json();
assert.ok(
body.error?.message?.toLowerCase().includes("confirm") ||
body.error?.message?.toLowerCase().includes("header") ||
body.message?.toLowerCase().includes("confirm") ||
String(body).toLowerCase().includes("confirm"),
`body should mention confirmation: ${JSON.stringify(body)}`
);
});
it("?reveal=key with wrong X-Reveal-Confirm value returns 403", async () => {
const req = makeRequest("http://localhost/api/services/9router/status?reveal=key", {
"X-Reveal-Confirm": "no",
});
const res = await GET(req);
assert.equal(res.status, 403);
});
it("?reveal=key with X-Reveal-Confirm: yes returns apiKeyPlain", async () => {
const req = makeRequest("http://localhost/api/services/9router/status?reveal=key", {
"X-Reveal-Confirm": "yes",
});
const res = await GET(req);
assert.equal(res.status, 200);
const body = await res.json();
assert.ok(typeof body.apiKeyPlain === "string", "should have apiKeyPlain string");
assert.ok(body.apiKeyPlain.startsWith("nr_"), "plain key should start with nr_");
// masked should also be present
assert.ok("apiKeyMasked" in body, "should still have apiKeyMasked");
});
it("reveal logs an audit entry with action=service.reveal_api_key", async () => {
const countBefore = countAuditLog({ action: "service.reveal_api_key" });
const req = makeRequest("http://localhost/api/services/9router/status?reveal=key", {
"X-Reveal-Confirm": "yes",
});
const res = await GET(req);
assert.equal(res.status, 200);
const countAfter = countAuditLog({ action: "service.reveal_api_key" });
assert.equal(
countAfter,
countBefore + 1,
`audit entries should increase by 1 (was ${countBefore}, now ${countAfter})`
);
});
it("plain key is different from masked key (not trivially masked)", async () => {
const req = makeRequest("http://localhost/api/services/9router/status?reveal=key", {
"X-Reveal-Confirm": "yes",
});
const res = await GET(req);
const body = await res.json();
assert.notEqual(body.apiKeyPlain, body.apiKeyMasked, "plain and masked should differ");
assert.ok(
body.apiKeyPlain.length > body.apiKeyMasked.length ||
!body.apiKeyMasked.includes(body.apiKeyPlain),
"masked should not contain the full plain key"
);
});
});
+307
View File
@@ -0,0 +1,307 @@
/**
* T-012 — Settings PATCH audit log.
*
* Covers spec AC-9 / AC-10 / AC-11 (plus idempotent no-op case):
* - AC-9 success diff row carries `action=settings.update`, target,
* actor, ip, and per-key {before, after} diff for every changed key.
* - AC-10 each rejection path (PASSWORD_REQUIRED, PASSWORD_MISMATCH,
* BYPASS_PREFIX_NOT_ALLOWED, zod validation failure) writes a
* `settings.update_failed` row with the matching `reason` code and
* NEVER persists settings.
* - AC-11 every changed key shows up in the success diff — not only
* security-impacting keys.
* - Idempotent PATCH (body matches stored state) writes NO row.
*
* Runs through the real PATCH handler + `setupSettingsFixture` mock so the
* production `updateSettings → applyRuntimeSettings → logAuditEvent` pipeline
* fires exactly as it does in deployment.
*
* INSUFFICIENT_SCOPE is intentionally NOT exercised here — per spec AC-13 it
* is rejected by `requireManagementAuth` before the audit-aware handler body
* runs, so no row is written.
*/
import test from "node:test";
import assert from "node:assert/strict";
import { setupSettingsFixture } from "../_mocks/settings.ts";
import { makeManagementSessionRequest } from "../../helpers/managementSession.ts";
// Allocate fixture FIRST so DATA_DIR is set before any DB import resolves.
const fixture = setupSettingsFixture("settings-audit");
process.env.OMNIROUTE_DISABLE_REDIS_AUTH_CACHE = "1";
const ORIGINAL_INITIAL_PASSWORD = process.env.INITIAL_PASSWORD;
const ORIGINAL_JWT_SECRET = process.env.JWT_SECRET;
const core = await import("../../../src/lib/db/core.ts");
const settingsDb = await import("../../../src/lib/db/settings.ts");
const runtime = await import("../../../src/lib/config/runtimeSettings.ts");
const settingsRoute = await import("../../../src/app/api/settings/route.ts");
const compliance = await import("../../../src/lib/compliance/index.ts");
const managementPassword = await import("../../../src/lib/auth/managementPassword.ts");
const apiKeysDb = await import("../../../src/lib/db/apiKeys.ts");
test.beforeEach(async () => {
await fixture.resetStorage();
apiKeysDb.resetApiKeyState();
runtime.resetRuntimeSettingsStateForTests();
});
test.after(() => {
core.resetDbInstance();
fixture.cleanup();
if (ORIGINAL_INITIAL_PASSWORD === undefined) delete process.env.INITIAL_PASSWORD;
else process.env.INITIAL_PASSWORD = ORIGINAL_INITIAL_PASSWORD;
if (ORIGINAL_JWT_SECRET === undefined) delete process.env.JWT_SECRET;
else process.env.JWT_SECRET = ORIGINAL_JWT_SECRET;
});
async function bootstrapWithPassword(password: string): Promise<void> {
process.env.JWT_SECRET = "test-jwt-secret-settings-audit";
process.env.INITIAL_PASSWORD = password;
await settingsDb.updateSettings({ requireLogin: true });
await managementPassword.ensurePersistentManagementPasswordHash({
source: "test.bootstrap",
});
}
function settingsRows() {
// `getAuditLog`'s `AuditLogEntry[]` return type now exposes `action`,
// `actor`, `target`, `status`, `details`, etc. directly — no local cast
// needed. See src/lib/compliance/index.ts.
return compliance.getAuditLog({ target: "settings", limit: 50 });
}
// ─── AC-9 — success diff row written ──────────────────────────────────────
test("AC-9: successful PATCH writes settings.update with diff of changed keys", async () => {
await bootstrapWithPassword("initial-pass-ac9");
const before = await settingsDb.getSettings();
assert.equal(before.localOnlyManageScopeBypassEnabled, true);
const response = await settingsRoute.PATCH(
await makeManagementSessionRequest("http://localhost/api/settings", {
method: "PATCH",
body: {
localOnlyManageScopeBypassEnabled: false,
currentPassword: "initial-pass-ac9",
},
})
);
assert.equal(response.status, 200);
const rows = settingsRows();
const successRows = rows.filter((r) => r.action === "settings.update");
assert.equal(successRows.length, 1, `expected 1 success row, got: ${JSON.stringify(rows)}`);
const row = successRows[0];
assert.equal(row.target, "settings");
assert.equal(row.status, "success");
assert.equal(row.resource_type, "settings");
// Cookie session ⇒ actor=dashboard.
assert.equal(row.actor, "dashboard");
const details = row.details as { diff: Record<string, { before: unknown; after: unknown }> };
assert.ok(details && typeof details === "object", "details must be parsed JSON");
assert.ok(details.diff, "diff present");
assert.deepEqual(details.diff.localOnlyManageScopeBypassEnabled, {
before: true,
after: false,
});
});
// ─── AC-10 — failure rows for each rejection path ────────────────────────
test("AC-10a: PASSWORD_REQUIRED failure writes settings.update_failed", async () => {
await bootstrapWithPassword("initial-pass-ac10a");
const response = await settingsRoute.PATCH(
await makeManagementSessionRequest("http://localhost/api/settings", {
method: "PATCH",
body: { localOnlyManageScopeBypassEnabled: false },
})
);
assert.equal(response.status, 400);
const rows = settingsRows().filter((r) => r.action === "settings.update_failed");
assert.equal(rows.length, 1);
const details = rows[0].details as { reason: string; attempted_keys: string[] };
assert.equal(details.reason, "PASSWORD_REQUIRED");
assert.ok(details.attempted_keys.includes("localOnlyManageScopeBypassEnabled"));
// No raw payload values — only the key NAMES are recorded under
// `attempted_keys`. There must be no `before`/`after` or `diff` block on a
// failure row, and no other fields beyond reason+attempted_keys in details.
assert.deepEqual(
Object.keys(details).sort(),
["attempted_keys", "reason"],
"failure details must only contain reason + attempted_keys (no payload echo)"
);
// Persisted state unchanged.
const after = await settingsDb.getSettings();
assert.equal(after.localOnlyManageScopeBypassEnabled, true);
});
test("AC-10b: PASSWORD_MISMATCH failure writes settings.update_failed", async () => {
await bootstrapWithPassword("initial-pass-ac10b");
const response = await settingsRoute.PATCH(
await makeManagementSessionRequest("http://localhost/api/settings", {
method: "PATCH",
body: {
localOnlyManageScopeBypassEnabled: false,
currentPassword: "definitely-wrong",
},
})
);
assert.equal(response.status, 401);
const rows = settingsRows().filter((r) => r.action === "settings.update_failed");
assert.equal(rows.length, 1);
const details = rows[0].details as { reason: string; attempted_keys: string[] };
assert.equal(details.reason, "PASSWORD_MISMATCH");
// Password attempt MUST NOT leak — only key names.
const serialized = JSON.stringify(rows[0]);
assert.equal(
serialized.includes("definitely-wrong"),
false,
"rejected currentPassword must not appear in audit row"
);
const after = await settingsDb.getSettings();
assert.equal(after.localOnlyManageScopeBypassEnabled, true);
});
test("AC-10c: BYPASS_PREFIX_NOT_ALLOWED failure writes settings.update_failed", async () => {
await bootstrapWithPassword("initial-pass-ac10c");
const response = await settingsRoute.PATCH(
await makeManagementSessionRequest("http://localhost/api/settings", {
method: "PATCH",
body: {
localOnlyManageScopeBypassPrefixes: ["/api/mcp/", "/api/cli-tools/runtime/"],
currentPassword: "initial-pass-ac10c",
},
})
);
assert.equal(response.status, 400);
const rows = settingsRows().filter((r) => r.action === "settings.update_failed");
assert.equal(rows.length, 1);
const details = rows[0].details as { reason: string };
assert.equal(details.reason, "BYPASS_PREFIX_NOT_ALLOWED");
// Snapshot untouched.
const after = await settingsDb.getSettings();
assert.deepEqual(after.localOnlyManageScopeBypassPrefixes, ["/api/mcp/"]);
});
test("AC-10d: zod validation failure (wrong type) writes settings.update_failed", async () => {
await bootstrapWithPassword("initial-pass-ac10d");
const response = await settingsRoute.PATCH(
await makeManagementSessionRequest("http://localhost/api/settings", {
method: "PATCH",
body: {
localOnlyManageScopeBypassEnabled: "definitely-not-a-boolean",
currentPassword: "initial-pass-ac10d",
},
})
);
assert.equal(response.status, 400);
const rows = settingsRows().filter((r) => r.action === "settings.update_failed");
assert.equal(rows.length, 1);
const details = rows[0].details as { reason: string };
assert.equal(details.reason, "VALIDATION_FAILED");
});
// AC-13 sanity: INSUFFICIENT_SCOPE rejection happens upstream in
// requireManagementAuth and never reaches the handler body, so no audit row.
// We cover it implicitly by NOT having an INSUFFICIENT_SCOPE failure test —
// the route-level rejection is already covered by api-auth.test.ts.
// ─── AC-11 — diff covers every changed key (not only security keys) ──────
test("AC-11: diff records every changed key, including non-security keys", async () => {
await bootstrapWithPassword("initial-pass-ac11");
// Seed an initial value for a non-security key so the diff is meaningful.
await settingsDb.updateSettings({ theme: "light", instanceName: "before" });
const response = await settingsRoute.PATCH(
await makeManagementSessionRequest("http://localhost/api/settings", {
method: "PATCH",
body: {
theme: "dark",
instanceName: "after",
localOnlyManageScopeBypassEnabled: false,
currentPassword: "initial-pass-ac11",
},
})
);
assert.equal(response.status, 200);
const rows = settingsRows().filter((r) => r.action === "settings.update");
assert.equal(rows.length, 1);
const details = rows[0].details as { diff: Record<string, { before: unknown; after: unknown }> };
// Security key AND multiple non-security keys must all be in diff.
assert.ok(details.diff.localOnlyManageScopeBypassEnabled, "security key in diff");
assert.ok(details.diff.theme, "theme (non-security) in diff");
assert.ok(details.diff.instanceName, "instanceName (non-security) in diff");
assert.deepEqual(details.diff.theme, { before: "light", after: "dark" });
assert.deepEqual(details.diff.instanceName, { before: "before", after: "after" });
});
// ─── Idempotent no-op writes NO row ──────────────────────────────────────
test("idempotent PATCH (body matches current state) writes NO audit row", async () => {
await bootstrapWithPassword("initial-pass-noop");
// Settings already at default — patch the same value back.
const response = await settingsRoute.PATCH(
await makeManagementSessionRequest("http://localhost/api/settings", {
method: "PATCH",
body: {
localOnlyManageScopeBypassEnabled: true, // same as default
currentPassword: "initial-pass-noop",
},
})
);
assert.equal(response.status, 200);
const rows = settingsRows();
assert.equal(
rows.length,
0,
`idempotent PATCH must not emit an audit row, got: ${JSON.stringify(rows)}`
);
});
// ─── Multi-row sanity: success + failure sequence ─────────────────────────
test("sequence: failure then success produces exactly 1 failure row + 1 success row", async () => {
await bootstrapWithPassword("initial-pass-seq");
// 1) failure
await settingsRoute.PATCH(
await makeManagementSessionRequest("http://localhost/api/settings", {
method: "PATCH",
body: { localOnlyManageScopeBypassEnabled: false, currentPassword: "wrong" },
})
);
// 2) success
await settingsRoute.PATCH(
await makeManagementSessionRequest("http://localhost/api/settings", {
method: "PATCH",
body: {
localOnlyManageScopeBypassEnabled: false,
currentPassword: "initial-pass-seq",
},
})
);
const rows = settingsRows();
const failures = rows.filter((r) => r.action === "settings.update_failed");
const successes = rows.filter((r) => r.action === "settings.update");
assert.equal(failures.length, 1);
assert.equal(successes.length, 1);
});
@@ -0,0 +1,250 @@
import { test, beforeEach } from "node:test";
import assert from "node:assert/strict";
import {
selfFetchWithRetry,
ensureLoopbackServerReady,
__resetLoopbackReadinessForTests,
} from "../../../src/app/api/providers/[id]/sync-models/route.ts";
// ---------------------------------------------------------------------------
// Test 1: retry succeeds on attempt 3
// ---------------------------------------------------------------------------
test("self-fetch retries with backoff and succeeds on attempt 3", async () => {
let attempts = 0;
const fetchMock: typeof fetch = async () => {
attempts++;
if (attempts < 3) {
throw new Error("fetch failed");
}
return new Response(JSON.stringify({ models: [{ id: "model-1" }] }), { status: 200 });
};
let inProcCalls = 0;
const inProcMock = async () => {
inProcCalls++;
return new Response(JSON.stringify({ models: [] }), { status: 200 });
};
const result = await selfFetchWithRetry("http://127.0.0.1:20128/api/providers/conn-1/models", {
fetch: fetchMock,
maxRetries: 5,
backoffMs: 5,
inProcessFallback: inProcMock,
skipReadinessGate: true,
});
assert.equal(attempts, 3, "should have retried twice before succeeding on attempt 3");
assert.equal(inProcCalls, 0, "should not have called in-process route");
assert.equal(result.ok, true, "response should be ok");
});
// ---------------------------------------------------------------------------
// Test 2: falls back to in-process after maxRetries failures
// ---------------------------------------------------------------------------
test("self-fetch falls back to in-process route after maxRetries failures", async () => {
let attempts = 0;
const fetchMock: typeof fetch = async () => {
attempts++;
throw new Error("fetch failed");
};
let inProcCalls = 0;
const inProcMock = async () => {
inProcCalls++;
return new Response(JSON.stringify({ models: [{ id: "in-proc-model" }] }), { status: 200 });
};
const result = await selfFetchWithRetry("http://127.0.0.1:20128/api/providers/conn-2/models", {
fetch: fetchMock,
maxRetries: 3,
backoffMs: 5,
connectionId: "conn-2",
inProcessFallback: inProcMock,
skipReadinessGate: true,
});
assert.equal(attempts, 3, "should retry exactly maxRetries times");
assert.equal(inProcCalls, 1, "should fall back to in-process exactly once");
const body = await result.json();
assert.equal(body.models[0].id, "in-proc-model");
});
// ---------------------------------------------------------------------------
// Test 3: HTTP error responses are returned as-is (no retry on HTTP errors)
//
// Retry contract: only network-level failures (ECONNREFUSED, "fetch failed")
// are retried. HTTP responses (even 4xx/5xx) mean the server IS up and
// returned an error that should be propagated as-is to the caller.
// ---------------------------------------------------------------------------
test("self-fetch returns HTTP error responses immediately without retrying", async () => {
// 5xx HTTP response: server is up but returned error
{
let attempts = 0;
const fetchMock: typeof fetch = async () => {
attempts++;
return new Response("server error", { status: 503 });
};
const inProcMock = async () => new Response(JSON.stringify({ models: [] }), { status: 200 });
const res = await selfFetchWithRetry("http://127.0.0.1:20128/api/providers/conn-3/models", {
fetch: fetchMock,
maxRetries: 5,
backoffMs: 5,
inProcessFallback: inProcMock,
skipReadinessGate: true,
});
assert.equal(attempts, 1, "5xx HTTP response should NOT retry (got " + attempts + ")");
assert.equal(res.status, 503, "should propagate the 503 response as-is");
}
// 4xx HTTP response: also returned immediately without retry
{
let attempts = 0;
const fetchMock: typeof fetch = async () => {
attempts++;
return new Response("not found", { status: 404 });
};
const inProcMock = async () => new Response(JSON.stringify({ models: [] }), { status: 200 });
const res = await selfFetchWithRetry("http://127.0.0.1:20128/api/providers/conn-4/models", {
fetch: fetchMock,
maxRetries: 5,
backoffMs: 5,
inProcessFallback: inProcMock,
skipReadinessGate: true,
});
assert.equal(attempts, 1, "4xx HTTP response should NOT retry (got " + attempts + ")");
assert.equal(res.status, 404, "should propagate the 404 response as-is");
}
});
// ---------------------------------------------------------------------------
// Readiness gate tests
// ---------------------------------------------------------------------------
test("ensureLoopbackServerReady: 17 concurrent callers trigger exactly ONE probe sequence", async () => {
__resetLoopbackReadinessForTests();
let probeCalls = 0;
let serverIsUp = false;
// Server becomes ready after 30ms
setTimeout(() => {
serverIsUp = true;
}, 30);
const mockFetch = async (_url) => {
probeCalls++;
if (!serverIsUp) throw new Error("fetch failed");
return new Response("", { status: 200 });
};
await Promise.all(
Array.from({ length: 17 }, () =>
ensureLoopbackServerReady({ fetch: mockFetch, pollMs: 5, maxWaitMs: 1000 })
)
);
// Probe may have polled multiple times before server came up -- that is fine.
// What matters: 17 concurrent callers share ONE probe sequence.
// Expected: roughly (30ms / 5ms) = ~6 attempts, not 17 x 6 = 102.
assert.ok(probeCalls <= 15, "single shared probe expected <=15 attempts, got " + probeCalls);
});
test("ensureLoopbackServerReady: rejects after maxWaitMs with consistent network errors", async () => {
__resetLoopbackReadinessForTests();
const mockFetch = async (_url) => {
throw new Error("ECONNREFUSED");
};
await assert.rejects(
() =>
ensureLoopbackServerReady({
fetch: mockFetch,
maxWaitMs: 50,
pollMs: 10,
}),
/loopback server not ready/
);
});
test("ensureLoopbackServerReady: resolves on 4xx (any HTTP status confirms server is up)", async () => {
__resetLoopbackReadinessForTests();
let calls = 0;
const mockFetch = async (_url) => {
calls++;
return new Response("not found", { status: 404 });
};
// Should not throw: 404 means the server is dispatching
await ensureLoopbackServerReady({ fetch: mockFetch, maxWaitMs: 500, pollMs: 10 });
assert.equal(calls, 1, "resolved after exactly 1 probe (server immediately responded 404)");
});
test("selfFetchWithRetry with gate: 17 concurrent callers produce one probe + one fetch each", async () => {
__resetLoopbackReadinessForTests();
let probeCalls = 0;
let modelFetchCalls = 0;
let serverIsUp = false;
setTimeout(() => {
serverIsUp = true;
}, 30);
const mockFetch = async (url) => {
const isReadinessProbe = url.includes("__readiness_probe__");
if (isReadinessProbe) {
probeCalls++;
if (!serverIsUp) throw new Error("fetch failed");
return new Response("", { status: 404 });
}
modelFetchCalls++;
if (!serverIsUp) throw new Error("fetch failed");
return new Response(JSON.stringify({ models: [{ id: "model-x" }] }), { status: 200 });
};
await Promise.all(
Array.from({ length: 17 }, (_, i) =>
selfFetchWithRetry("http://127.0.0.1:20128/api/providers/conn-" + i + "/models", {
fetch: mockFetch,
maxRetries: 3,
backoffMs: 5,
})
)
);
// After readiness gate succeeds, each connection makes EXACTLY one model fetch
assert.equal(modelFetchCalls, 17, "each connection fetches its own models exactly once");
// Probe may have polled several times but NOT 17 x poll-count
assert.ok(probeCalls <= 15, "probe should be shared, got " + probeCalls + " attempts");
});
// Sanity check: disabling the gate shows amplification (verifies the gate is doing work)
test("sanity: without readiness gate, 17 callers retry independently (amplification confirmed)", async () => {
__resetLoopbackReadinessForTests();
let modelFetchCalls = 0;
let serverIsUp = false;
setTimeout(() => {
serverIsUp = true;
}, 30);
const mockFetch = async (_url) => {
modelFetchCalls++;
if (!serverIsUp) throw new Error("fetch failed");
return new Response(JSON.stringify({ models: [{ id: "model-x" }] }), { status: 200 });
};
await Promise.all(
Array.from({ length: 17 }, (_, i) =>
selfFetchWithRetry("http://127.0.0.1:20128/api/providers/conn-" + i + "/models", {
fetch: mockFetch,
maxRetries: 5,
backoffMs: 5,
skipReadinessGate: true,
})
)
);
// Without gate: each of the 17 callers retries independently during boot race.
// Expect well above 17 total fetch attempts.
assert.ok(
modelFetchCalls > 17,
"without gate, callers retry independently, got " + modelFetchCalls + " (expected >17)"
);
});
@@ -0,0 +1,54 @@
import test from "node:test";
import assert from "node:assert/strict";
/**
* Regression: issue #6405 — unknown /v1/* routes returned HTML dashboard 404
* instead of a JSON error. Ensure the app-router catch-all under
* `src/app/api/v1/[...omnirouteCatchAll]/route.ts` responds with a proper
* OpenAI-compatible JSON not-found body for every HTTP method.
*/
const catchAll = await import(
"../../../src/app/api/v1/[...omnirouteCatchAll]/route.ts"
);
function makeReq(pathname: string, method = "GET"): Request {
return new Request(`http://localhost:20128${pathname}`, { method });
}
test("v1 catchall returns application/json 404 with not_found error type on GET", async () => {
const res = await catchAll.GET(makeReq("/v1/does-not-exist"));
assert.equal(res.status, 404);
const ct = res.headers.get("content-type") || "";
assert.ok(ct.includes("application/json"), `expected JSON content-type, got: ${ct}`);
const body = (await res.json()) as {
error: { type: string; message: string; code?: string; path?: string };
};
assert.equal(body.error.type, "not_found");
assert.equal(body.error.path, "/v1/does-not-exist");
assert.match(body.error.message, /Unknown API route/);
});
test("v1 catchall returns JSON 404 on POST / PUT / PATCH / DELETE / HEAD", async () => {
for (const method of ["POST", "PUT", "PATCH", "DELETE", "HEAD"] as const) {
const res = await (catchAll as Record<string, (r: Request) => Promise<Response>>)[
method
](makeReq(`/v1/nope/${method.toLowerCase()}`, method));
assert.equal(res.status, 404, `${method} status`);
assert.ok(
(res.headers.get("content-type") || "").includes("application/json"),
`${method} content-type`,
);
}
});
test("v1 catchall OPTIONS preflight returns CORS headers", async () => {
// Note: `Access-Control-Allow-Origin` is applied by the global middleware
// (`src/middleware.ts`), not by handlers directly. The handler is only
// responsible for the static methods/allowed-headers piece.
const res = await catchAll.OPTIONS();
assert.ok(
res.headers.get("access-control-allow-methods"),
"OPTIONS must expose CORS methods header",
);
});
+27
View File
@@ -0,0 +1,27 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
const routePath = path.join(repoRoot, "src/app/api/v1/me/status/route.ts");
test("GET /api/v1/me/status rejects missing Bearer token in the handler", async () => {
const route = await import("../../../src/app/api/v1/me/status/route.ts");
const response = await route.GET(new Request("http://localhost/api/v1/me/status"));
assert.equal(response.status, 401);
});
test("GET /api/v1/me/status derives identity from Bearer metadata and ignores query apiKeyId", () => {
const source = fs.readFileSync(routePath, "utf8");
assert.match(source, /Authorization/);
assert.match(source, /Bearer/);
assert.match(source, /validateApiKey/);
assert.match(source, /getApiKeyMetadata/);
assert.match(source, /metadata\.id === "env-key"/);
assert.doesNotMatch(source, /searchParams\.get\(["']apiKeyId["']\)/);
});
@@ -0,0 +1,45 @@
import { afterEach, test } from "node:test";
import assert from "node:assert/strict";
import {
clearBifrostFailure,
getActiveBifrostCooldown,
getBifrostFailureCooldownMs,
recordBifrostFailure,
resetBifrostCooldowns,
} from "../../../../src/app/api/v1/relay/chat/completions/bifrostCooldown.ts";
afterEach(() => {
resetBifrostCooldowns();
});
test("bifrost cooldown defaults to a short retry suppression window", () => {
assert.equal(getBifrostFailureCooldownMs({}), 5000);
assert.equal(
getBifrostFailureCooldownMs({ OMNIROUTE_BIFROST_FAILURE_COOLDOWN_MS: "250" }),
250
);
assert.equal(
getBifrostFailureCooldownMs({ OMNIROUTE_BIFROST_FAILURE_COOLDOWN_MS: "bad" }),
5000
);
});
test("bifrost cooldown reports remaining time and expires", () => {
recordBifrostFailure("http://bifrost.local", "timeout", 1000, 500);
assert.deepEqual(getActiveBifrostCooldown("http://bifrost.local", 1100), {
remainingMs: 400,
reason: "timeout",
});
assert.equal(getActiveBifrostCooldown("http://bifrost.local", 1501), null);
});
test("bifrost cooldown can be disabled or cleared", () => {
recordBifrostFailure("http://bifrost.local", "timeout", 1000, 0);
assert.equal(getActiveBifrostCooldown("http://bifrost.local", 1001), null);
recordBifrostFailure("http://bifrost.local", "timeout", 1000, 500);
clearBifrostFailure("http://bifrost.local");
assert.equal(getActiveBifrostCooldown("http://bifrost.local", 1001), null);
});
+241
View File
@@ -0,0 +1,241 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import {
checkIpRateLimit,
getClientIp,
sanitizeForensicHeader,
} from "../../../../src/app/api/v1/relay/chat/completions/relaySecurity.ts";
import { getDbInstance } from "../../../../src/lib/db/core.ts";
import { getRelayLogs } from "../../../../src/lib/db/relayProxies.ts";
// ─── T-12 (#3932 PR-4): bifrost sidecar proxy route ──────────────────────
//
// We test the *contract* of the route by setting env before import and
// calling the exported POST handler. The handler is module-scope configured
// (BIFROST_BASE_URL is read at import time), so env must be set BEFORE the
// dynamic import below.
const ORIGINAL_BIFROST_BASE_URL = process.env.BIFROST_BASE_URL;
const ORIGINAL_BIFROST_API_KEY = process.env.BIFROST_API_KEY;
const ORIGINAL_BIFROST_OMNI_KEY = process.env.OMNIROUTE_BIFROST_KEY;
const ORIGINAL_BIFROST_TIMEOUT = process.env.BIFROST_TIMEOUT_MS;
const ORIGINAL_BIFROST_STREAMING = process.env.BIFROST_STREAMING_ENABLED;
const ORIGINAL_FETCH = globalThis.fetch;
function seedRelayToken(rawToken: string) {
const id = `rl_test_${Date.now()}_${Math.random().toString(16).slice(2)}`;
const now = Math.floor(Date.now() / 1000);
getDbInstance()
.prepare(
`
INSERT INTO relay_tokens (id, name, token_hash, token_prefix, description, combo_id,
allowed_models, max_tokens_per_request, max_requests_per_minute, max_requests_per_day,
max_cost_per_day, enabled, created_at, updated_at, expires_at, metadata)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?)
`
)
.run(
id,
"bifrost-sse-lifecycle",
createHash("sha256").update(rawToken).digest("hex"),
"rl_test",
"",
null,
JSON.stringify(["*"]),
128000,
60,
10000,
0,
now,
now,
null,
"{}"
);
return { id, rawToken };
}
function restoreEnv() {
if (ORIGINAL_BIFROST_BASE_URL === undefined) delete process.env.BIFROST_BASE_URL;
else process.env.BIFROST_BASE_URL = ORIGINAL_BIFROST_BASE_URL;
if (ORIGINAL_BIFROST_API_KEY === undefined) delete process.env.BIFROST_API_KEY;
else process.env.BIFROST_API_KEY = ORIGINAL_BIFROST_API_KEY;
if (ORIGINAL_BIFROST_OMNI_KEY === undefined) delete process.env.OMNIROUTE_BIFROST_KEY;
else process.env.OMNIROUTE_BIFROST_KEY = ORIGINAL_BIFROST_OMNI_KEY;
if (ORIGINAL_BIFROST_TIMEOUT === undefined) delete process.env.BIFROST_TIMEOUT_MS;
else process.env.BIFROST_TIMEOUT_MS = ORIGINAL_BIFROST_TIMEOUT;
if (ORIGINAL_BIFROST_STREAMING === undefined) delete process.env.BIFROST_STREAMING_ENABLED;
else process.env.BIFROST_STREAMING_ENABLED = ORIGINAL_BIFROST_STREAMING;
globalThis.fetch = ORIGINAL_FETCH;
}
// Case 1: BIFROST_BASE_URL unset. We test this first because the route's
// module-scope `BIFROST_BASE_URL` would be empty for the entire test file.
test("bifrost route: returns 503 + fallback header when BIFROST_BASE_URL is unset", async () => {
delete process.env.BIFROST_BASE_URL;
delete process.env.BIFROST_API_KEY;
delete process.env.OMNIROUTE_BIFROST_KEY;
delete process.env.BIFROST_TIMEOUT_MS;
delete process.env.BIFROST_STREAMING_ENABLED;
// Dynamic import after env is set so the module reads the empty value.
const { POST } = await import(
"../../../../src/app/api/v1/relay/chat/completions/bifrost/route.ts"
);
const req = new Request("http://localhost/api/v1/relay/chat/completions/bifrost", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ model: "gpt-4", messages: [] }),
});
const res = await POST(req);
assert.equal(res.status, 503);
assert.equal(res.headers.get("X-Bifrost-Fallback"), "/api/v1/relay/chat/completions");
const body = await res.json();
assert.match(String(body?.error?.message ?? ""), /Bifrost sidecar not configured/);
restoreEnv();
});
// Case 2: BIFROST_BASE_URL set but no auth token in request. The route should
// return 401 *before* trying to reach the gateway, so we don't need a mock fetch.
test("bifrost route: returns 401 when BIFROST_BASE_URL is set but no auth token is provided", async () => {
process.env.BIFROST_BASE_URL = "http://bifrost.test.local:8080";
delete process.env.BIFROST_API_KEY;
delete process.env.OMNIROUTE_BIFROST_KEY;
delete process.env.BIFROST_TIMEOUT_MS;
delete process.env.BIFROST_STREAMING_ENABLED;
// Use a fresh module instance by appending a cache-busting query string.
// (Node's ESM cache is keyed by resolved URL, so a unique query bypasses it.)
const { POST } = await import(
`../../../../src/app/api/v1/relay/chat/completions/bifrost/route.ts?case=${Date.now()}-${Math.random()}`
);
const req = new Request("http://localhost/api/v1/relay/chat/completions/bifrost", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ model: "gpt-4", messages: [{ role: "user", content: "hi" }] }),
});
const res = await POST(req);
assert.equal(res.status, 401);
const body = await res.json();
assert.match(String(body?.error?.message ?? ""), /Missing relay token/);
restoreEnv();
});
// Case 3: hashToken() is used internally. Verify the SHA-256 output shape so
// downstream code that compares hashes doesn't silently break if the impl
// changes. (This is a contract test, not a black-box test of the function.)
test("bifrost route: relay token hashing matches the SHA-256 hex contract", () => {
const token = "test-token-abc-123";
const hash = createHash("sha256").update(token).digest("hex");
assert.equal(hash.length, 64); // SHA-256 hex = 64 chars
assert.match(hash, /^[0-9a-f]{64}$/);
});
// Case 4: Validate the CORS preflight handler exists and responds with 204/200
test("bifrost route: OPTIONS responds with CORS headers", async () => {
const { OPTIONS } = await import(
`../../../../src/app/api/v1/relay/chat/completions/bifrost/route.ts?case=${Date.now()}-${Math.random()}`
);
const res = await OPTIONS();
// handleCorsOptions() returns 204 No Content with the standard CORS
// methods/headers. Access-Control-Allow-Origin is intentionally NOT set on
// the route's response — src/middleware.ts (applyCorsHeaders) is the single
// source of truth for which origin to echo, based on the allowlist in
// src/server/cors/origins.ts. We assert the route ships its end of the
// contract: status + methods/headers. Origin overlay is exercised by the
// middleware tests.
assert.ok(res.status === 200 || res.status === 204, `expected 200/204, got ${res.status}`);
assert.ok(
res.headers.get("Access-Control-Allow-Methods"),
"missing Access-Control-Allow-Methods header"
);
assert.ok(
res.headers.get("Access-Control-Allow-Headers"),
"missing Access-Control-Allow-Headers header"
);
});
test("bifrost route: shared relay IP limiter blocks a repeated token from one IP", () => {
const tokenId = `bifrost-ip-limit-${Date.now()}-${Math.random()}`;
const ip = "203.0.113.77";
for (let i = 0; i < 30; i++) {
assert.equal(checkIpRateLimit(tokenId, ip).allowed, true);
}
const blocked = checkIpRateLimit(tokenId, ip);
assert.equal(blocked.allowed, false);
assert.ok(blocked.resetIn >= 0 && blocked.resetIn <= 60);
});
test("bifrost route: shared relay security helpers sanitize forwarded client metadata", () => {
const req = new Request("http://localhost/api/v1/relay/chat/completions/bifrost", {
headers: {
"x-forwarded-for": "198.51.100.8, 10.0.0.2",
},
});
assert.equal(getClientIp(req), "198.51.100.8");
assert.equal(sanitizeForensicHeader("ua\r\nforged"), "ua forged");
});
test("bifrost route: records relay usage after SSE stream completion", async () => {
process.env.BIFROST_BASE_URL = "http://bifrost.test.local:8080";
process.env.BIFROST_TIMEOUT_MS = "5000";
delete process.env.BIFROST_API_KEY;
delete process.env.OMNIROUTE_BIFROST_KEY;
delete process.env.BIFROST_STREAMING_ENABLED;
const relayToken = seedRelayToken(`relay_bifrost_sse_${Date.now()}`);
globalThis.fetch = async () =>
new Response(
new ReadableStream<Uint8Array>({
start(controller) {
controller.enqueue(new TextEncoder().encode("data: {\"delta\":\"hi\"}\n\n"));
controller.close();
},
}),
{
status: 200,
headers: { "content-type": "text/event-stream" },
}
);
const { POST } = await import(
`../../../../src/app/api/v1/relay/chat/completions/bifrost/route.ts?case=${Date.now()}-${Math.random()}`
);
const req = new Request("http://localhost/api/v1/relay/chat/completions/bifrost", {
method: "POST",
headers: {
authorization: `Bearer ${relayToken.rawToken}`,
"content-type": "application/json",
"x-request-id": "bifrost-sse-lifecycle-test",
},
body: JSON.stringify({
model: "gpt-4",
stream: true,
messages: [{ role: "user", content: "hi" }],
}),
});
const res = await POST(req);
assert.equal(res.status, 200);
assert.equal(res.headers.get("X-Routed-By"), "bifrost");
assert.equal(getRelayLogs(relayToken.id, 10).length, 0);
assert.match(await res.text(), /delta/);
const logs = getRelayLogs(relayToken.id, 10);
assert.equal(logs.length, 1);
assert.equal(logs[0].status, "success");
assert.equal(logs[0].status_code, 200);
assert.equal(logs[0].request_id, "bifrost-sse-lifecycle-test");
restoreEnv();
});
@@ -0,0 +1,30 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
GET,
OPTIONS,
} from "../../../../src/app/api/v1/provider-plugin-manifest/route.ts";
test("provider plugin manifest route returns JSON-safe manifest", async () => {
const response = await GET();
const body = await response.json();
assert.equal(response.status, 200);
assert.equal(response.headers.get("Content-Type"), "application/json");
assert.equal(body.schemaVersion, 1);
assert.equal(body.generatedFrom, "open-sse/config/providers");
assert.ok(body.providers.length > 100);
assert.ok(body.providers.some((provider: { id: string }) => provider.id === "openai"));
const serialized = JSON.stringify(body);
assert.equal(serialized.includes("clientSecret"), false);
});
test("provider plugin manifest route handles CORS preflight", async () => {
const response = await OPTIONS();
assert.equal(response.status, 200);
assert.equal(response.headers.get("Access-Control-Allow-Methods"), "GET, OPTIONS");
assert.equal(response.headers.get("Access-Control-Allow-Headers"), "*");
});
@@ -0,0 +1,154 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import {
getBifrostRoutingConfig,
getRoutingFallbackHeader,
resolveRelayRoutingBackend,
shouldTryBifrost,
shouldTryBifrostForRequest,
} from "../../../../src/app/api/v1/relay/chat/completions/routingBackend.ts";
test("relay routing backend defaults to TypeScript without bifrost", () => {
const env = {};
assert.equal(resolveRelayRoutingBackend(env), "ts");
assert.equal(getBifrostRoutingConfig(env), null);
});
test("relay routing backend auto-enables bifrost when base URL is configured", () => {
const env = {
BIFROST_BASE_URL: "http://127.0.0.1:8080/",
OMNIROUTE_BIFROST_KEY: "sidecar-key",
BIFROST_TIMEOUT_MS: "250",
};
const config = getBifrostRoutingConfig(env);
assert.equal(resolveRelayRoutingBackend(env), "auto");
assert.equal(config?.baseUrl, "http://127.0.0.1:8080");
assert.equal(config?.apiKey, "sidecar-key");
assert.equal(config?.timeoutMs, 250);
assert.equal(config?.streamingEnabled, true);
assert.equal(config?.enabled, true);
assert.equal(shouldTryBifrost("auto", config), true);
});
test("relay routing backend honors explicit TS and strict bifrost modes", () => {
const env = {
BIFROST_BASE_URL: "http://127.0.0.1:8080",
OMNIROUTE_RELAY_BACKEND: "ts",
};
const config = getBifrostRoutingConfig(env);
assert.equal(resolveRelayRoutingBackend(env), "ts");
assert.equal(shouldTryBifrost("ts", config), false);
env.OMNIROUTE_RELAY_BACKEND = "bifrost";
assert.equal(resolveRelayRoutingBackend(env), "bifrost");
assert.equal(shouldTryBifrost("bifrost", config), true);
});
test("relay routing backend respects bifrost killswitch", () => {
const env = {
BIFROST_BASE_URL: "http://127.0.0.1:8080",
BIFROST_ENABLED: "0",
};
const config = getBifrostRoutingConfig(env);
assert.equal(resolveRelayRoutingBackend(env), "ts");
assert.equal(config?.enabled, false);
assert.equal(shouldTryBifrost("auto", config), false);
});
test("relay routing backend falls back on invalid timeout values", () => {
const env = {
BIFROST_BASE_URL: "http://127.0.0.1:8080",
BIFROST_TIMEOUT_MS: "not-a-number",
};
assert.equal(getBifrostRoutingConfig(env)?.timeoutMs, 30000);
});
test("relay routing backend exposes TS fallback header only for enabled auto bifrost fallback", () => {
const config = getBifrostRoutingConfig({
BIFROST_BASE_URL: "http://127.0.0.1:8080",
});
assert.equal(getRoutingFallbackHeader("auto", config), "bifrost");
assert.equal(getRoutingFallbackHeader("ts", config), undefined);
assert.equal(getRoutingFallbackHeader("bifrost", config), undefined);
assert.equal(
getRoutingFallbackHeader(
"auto",
getBifrostRoutingConfig({
BIFROST_BASE_URL: "http://127.0.0.1:8080",
BIFROST_ENABLED: "0",
})
),
undefined
);
assert.equal(getRoutingFallbackHeader("auto", null), undefined);
});
test("relay routing backend keeps strict bifrost failures out of auto fallback accounting", () => {
const config = getBifrostRoutingConfig({
BIFROST_BASE_URL: "http://127.0.0.1:8080",
});
assert.equal(shouldTryBifrost("bifrost", config), true);
assert.equal(getRoutingFallbackHeader("bifrost", config), undefined);
assert.equal(resolveRelayRoutingBackend({ OMNIROUTE_RELAY_BACKEND: "bifrost" }), "bifrost");
});
test("relay routing backend auto mode tries bifrost for manifest-eligible providers", () => {
const config = getBifrostRoutingConfig({
BIFROST_BASE_URL: "http://127.0.0.1:8080",
});
assert.deepEqual(
shouldTryBifrostForRequest("auto", config, { model: "openai/gpt-4.1" }, () => ({
eligible: true,
reasons: [],
})),
{ tryBifrost: true }
);
});
test("relay routing backend auto mode skips bifrost for manifest-ineligible providers", () => {
const config = getBifrostRoutingConfig({
BIFROST_BASE_URL: "http://127.0.0.1:8080",
});
assert.deepEqual(
shouldTryBifrostForRequest("auto", config, { model: "cw/claude-sonnet-4.6" }, () => ({
eligible: false,
reasons: ["custom executor: claude-web"],
})),
{ tryBifrost: false, fallbackReason: "bifrost-ineligible" }
);
});
test("relay routing backend auto mode keeps unknown providers on TS fallback", () => {
const config = getBifrostRoutingConfig({
BIFROST_BASE_URL: "http://127.0.0.1:8080",
});
assert.deepEqual(
shouldTryBifrostForRequest("auto", config, { model: "unknown/model" }, () => null),
{ tryBifrost: false, fallbackReason: "bifrost-provider-unknown" }
);
});
test("relay routing backend strict bifrost bypasses manifest eligibility", () => {
const config = getBifrostRoutingConfig({
BIFROST_BASE_URL: "http://127.0.0.1:8080",
});
assert.deepEqual(
shouldTryBifrostForRequest("bifrost", config, { model: "cw/claude-sonnet-4.6" }, () => ({
eligible: false,
reasons: ["custom executor: claude-web"],
})),
{ tryBifrost: true }
);
});
@@ -0,0 +1,46 @@
import test from "node:test";
import assert from "node:assert/strict";
import { finalizeReadableStream } from "../../../../src/app/api/v1/relay/chat/completions/streamFinalizer.ts";
test("finalizeReadableStream finalizes once after the wrapped stream completes", async () => {
const finalized: unknown[] = [];
const stream = finalizeReadableStream(
new ReadableStream<Uint8Array>({
start(controller) {
controller.enqueue(new TextEncoder().encode("hello"));
controller.close();
},
}),
(error) => finalized.push(error)
);
assert.equal(await new Response(stream).text(), "hello");
assert.deepEqual(finalized, [undefined]);
});
test("finalizeReadableStream finalizes once when the consumer cancels", async () => {
const finalized: unknown[] = [];
let cancelReason: unknown;
const stream = finalizeReadableStream(
new ReadableStream<Uint8Array>({
start(controller) {
controller.enqueue(new TextEncoder().encode("chunk"));
},
cancel(reason) {
cancelReason = reason;
},
}),
(error) => finalized.push(error)
);
const reader = stream.getReader();
const first = await reader.read();
assert.equal(new TextDecoder().decode(first.value), "chunk");
await reader.cancel("client disconnected");
await reader.cancel("second cancel");
assert.equal(cancelReason, "client disconnected");
assert.deepEqual(finalized, ["client disconnected"]);
});
@@ -0,0 +1,91 @@
import { describe, test } from "node:test";
import assert from "node:assert/strict";
import { z } from "zod";
import { validatedJsonBody } from "@/shared/validation/helpers";
function makeRequest(body: string, contentType = "application/json"): Request {
return new Request("http://localhost/test", {
method: "POST",
headers: { "content-type": contentType },
body,
});
}
describe("validatedJsonBody", () => {
const schema = z.object({
name: z.string().min(1),
count: z.number().int().nonnegative(),
});
test("returns the parsed and validated data on success", async () => {
const result = await validatedJsonBody(makeRequest('{"name":"hello","count":3}'), schema);
assert.equal(result.success, true);
if (result.success) {
assert.deepEqual(result.data, { name: "hello", count: 3 });
}
});
test("returns a 400 with structured details when the body fails Zod validation", async () => {
const result = await validatedJsonBody(makeRequest('{"name":"","count":-1}'), schema);
assert.equal(result.success, false);
if (!result.success) {
assert.equal(result.response.status, 400);
const body = await result.response.json();
assert.equal(body.error.message, "Invalid request");
assert.ok(Array.isArray(body.error.details));
const fields = body.error.details.map((d: { field: string }) => d.field);
assert.ok(fields.includes("name"));
assert.ok(fields.includes("count"));
}
});
test("returns a 400 with a body-parse failure for malformed JSON", async () => {
const result = await validatedJsonBody(makeRequest("not json at all"), schema);
assert.equal(result.success, false);
if (!result.success) {
assert.equal(result.response.status, 400);
const body = await result.response.json();
assert.deepEqual(body, {
error: {
message: "Invalid request",
details: [{ field: "body", message: "Invalid JSON body" }],
},
});
}
});
test("returns a 400 for an empty body", async () => {
const result = await validatedJsonBody(makeRequest(""), schema);
assert.equal(result.success, false);
if (!result.success) {
assert.equal(result.response.status, 400);
}
});
test("returns a 400 when required fields are missing entirely", async () => {
const result = await validatedJsonBody(makeRequest("{}"), schema);
assert.equal(result.success, false);
if (!result.success) {
assert.equal(result.response.status, 400);
const body = await result.response.json();
const fields = body.error.details.map((d: { field: string }) => d.field);
assert.ok(fields.includes("name"));
assert.ok(fields.includes("count"));
}
});
test("preserves the same envelope shape between parse and validate failure", async () => {
const parseFailure = await validatedJsonBody(makeRequest("nope"), schema);
const validateFailure = await validatedJsonBody(makeRequest("{}"), schema);
assert.equal(parseFailure.success, false);
assert.equal(validateFailure.success, false);
if (!parseFailure.success && !validateFailure.success) {
const parseBody = await parseFailure.response.json();
const validateBody = await validateFailure.response.json();
assert.equal(typeof parseBody.error.message, "string");
assert.equal(typeof validateBody.error.message, "string");
assert.ok(Array.isArray(parseBody.error.details));
assert.ok(Array.isArray(validateBody.error.details));
}
});
});
@@ -0,0 +1,167 @@
/**
* Tests for SSRF guard on the webhook URL surface:
*
* - POST /api/webhooks rejects custom/slack/discord webhooks targeting
* loopback / RFC1918 / link-local hosts.
* - PUT /api/webhooks/[id] rejects updates that would point a non-telegram
* webhook at a blocked host.
* - POST /api/webhooks/[id]/test refuses to perform the diagnostic fetch
* when the persisted URL is blocked (defense in depth, in case a row
* bypassed schema validation via direct DB access or older data).
* - kind === "telegram" is exempt — `url` there is a Telegram chat_id, not
* an HTTP URL — so the guard must not block it.
*/
import { describe, it, before, after } 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-webhook-ssrf-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.NODE_ENV = "test";
process.env.DISABLE_SQLITE_AUTO_BACKUP = "true";
const core = await import("../../../../src/lib/db/core.ts");
const { updateSettings } = await import("../../../../src/lib/db/settings.ts");
await updateSettings({ requireLogin: false });
const webhooksRoute = await import("../../../../src/app/api/webhooks/route.ts");
const webhookByIdRoute = await import("../../../../src/app/api/webhooks/[id]/route.ts");
const webhookTestRoute =
await import("../../../../src/app/api/webhooks/[id]/test/route.ts?suite=ssrf-guard");
const { createWebhook } = await import("../../../../src/lib/db/webhooks.ts");
after(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
function jsonRequest(url: string, method: string, body: unknown): Request {
return new Request(url, {
method,
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
}
describe("POST /api/webhooks — SSRF guard on create", () => {
it("rejects custom webhook pointing to 127.0.0.1", async () => {
const req = jsonRequest("http://localhost/api/webhooks", "POST", {
url: "http://127.0.0.1:20128/api/v1/management/proxies",
kind: "custom",
});
const res = await webhooksRoute.POST(req);
assert.equal(res.status, 400, "loopback URL must be rejected");
const body = await res.json();
assert.ok(body.error, "error body must be present");
});
it("rejects slack webhook pointing to link-local metadata host", async () => {
const req = jsonRequest("http://localhost/api/webhooks", "POST", {
url: "http://169.254.169.254/latest/meta-data/iam",
kind: "slack",
});
const res = await webhooksRoute.POST(req);
assert.equal(res.status, 400, "169.254/16 must be rejected");
});
it("rejects discord webhook pointing to RFC1918 (192.168/16)", async () => {
const req = jsonRequest("http://localhost/api/webhooks", "POST", {
url: "http://192.168.0.15:8080/hook",
kind: "discord",
});
const res = await webhooksRoute.POST(req);
assert.equal(res.status, 400, "RFC1918 must be rejected");
});
it("rejects discord webhook with embedded credentials", async () => {
const req = jsonRequest("http://localhost/api/webhooks", "POST", {
url: "https://user:pass@example.com/hook",
kind: "discord",
});
const res = await webhooksRoute.POST(req);
assert.equal(res.status, 400, "embedded credentials must be rejected");
});
it("rejects discord webhook with non-http(s) protocol", async () => {
const req = jsonRequest("http://localhost/api/webhooks", "POST", {
url: "file:///etc/passwd",
kind: "discord",
});
const res = await webhooksRoute.POST(req);
assert.equal(res.status, 400, "non-http protocol must be rejected");
});
it("accepts public https webhook", async () => {
const req = jsonRequest("http://localhost/api/webhooks", "POST", {
url: "https://example.com/hook",
kind: "custom",
});
const res = await webhooksRoute.POST(req);
assert.equal(res.status, 201, "public URL must be accepted");
});
it("does not trip the SSRF guard for telegram kind (url is a chat_id, not HTTP)", async () => {
const req = jsonRequest("http://localhost/api/webhooks", "POST", {
url: "@my-telegram-channel",
kind: "telegram",
});
const res = await webhooksRoute.POST(req);
// Telegram may still be rejected for unrelated reasons (e.g., storage
// encryption not configured). What matters is that the rejection is NOT
// caused by the SSRF/URL guard refinement.
if (res.status === 400) {
const body = await res.json();
const msg = JSON.stringify(body).toLowerCase();
assert.ok(
!/block|private|invalid outbound url|outbound url/.test(msg),
`telegram chat_id must not trip the URL guard: ${msg}`
);
}
});
});
describe("PUT /api/webhooks/[id] — SSRF guard on update", () => {
it("rejects an update that flips a webhook URL to loopback", async () => {
const created = createWebhook({
url: "https://example.com/initial",
events: ["*"],
kind: "custom",
});
const req = jsonRequest(`http://localhost/api/webhooks/${created.id}`, "PUT", {
url: "http://localhost/internal",
});
const res = await webhookByIdRoute.PUT(req, {
params: Promise.resolve({ id: created.id }),
});
assert.equal(res.status, 400, "PUT must reject loopback host");
});
});
describe("POST /api/webhooks/[id]/test — defense in depth on dispatch", () => {
it("does not exfiltrate response body when persisted URL is loopback", async () => {
// Bypass schema by inserting directly via the DB layer.
const stale = createWebhook({
url: "http://127.0.0.1:1/should-be-blocked",
events: ["*"],
kind: "custom",
});
const req = new Request(`http://localhost/api/webhooks/${stale.id}/test`, { method: "POST" });
const res = await webhookTestRoute.POST(req, {
params: Promise.resolve({ id: stale.id }),
});
assert.equal(res.status, 200, "test endpoint still returns a structured diagnostic envelope");
const body = await res.json();
assert.equal(body.delivered, false, "blocked URL must not report a successful delivery");
assert.equal(body.status, 0, "no upstream status from a guarded call");
assert.equal(body.responseBody, "", "no upstream body must be exfiltrated");
assert.ok(
typeof body.error === "string" && /block|private|invalid/i.test(body.error),
`error message must signal the block: ${body.error}`
);
});
});