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,231 @@
/**
* ServiceSupervisor unit tests.
*
* Uses a real Node.js child process (`node -e "..."`) to test lifecycle
* without mocking child_process — this gives realistic signal/exit behavior.
*
* A tiny HTTP health server is spawned inline for health-check tests.
*/
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 http from "node:http";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-supervisor-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.NODE_ENV = "test";
process.env.DISABLE_SQLITE_AUTO_BACKUP = "true";
// Import DB core first to trigger migration (creates version_manager with new columns)
const core = await import("../../../src/lib/db/core.ts");
// Seed the tool rows needed by tests
const db = core.getDbInstance();
db.prepare(
`INSERT OR IGNORE INTO version_manager (tool, status, port, auto_start, auto_update, provider_expose)
VALUES ('test-svc', 'stopped', 29999, 0, 0, 0)`
).run();
db.prepare(
`INSERT OR IGNORE INTO version_manager (tool, status, port, auto_start, auto_update, provider_expose)
VALUES ('test-crash', 'stopped', 29998, 0, 0, 0)`
).run();
db.prepare(
`INSERT OR IGNORE INTO version_manager (tool, status, port, auto_start, auto_update, provider_expose)
VALUES ('test-lock', 'stopped', 29997, 0, 0, 0)`
).run();
db.prepare(
`INSERT OR IGNORE INTO version_manager (tool, status, port, auto_start, auto_update, provider_expose)
VALUES ('test-adopt', 'stopped', 29996, 0, 0, 0)`
).run();
const { ServiceSupervisor } = await import("../../../src/lib/services/ServiceSupervisor.ts");
/** Starts a tiny HTTP health server on the given port that always returns 200. */
function startHealthServer(port: number): http.Server {
const server = http.createServer((_, res) => res.writeHead(200).end("ok"));
server.listen(port);
return server;
}
/** Config for a service that logs "tick" every second and stays alive. */
function tickConfig(tool: string, port: number) {
return {
tool,
port,
spawnArgs: () => ({
command: process.execPath,
args: ["-e", "setInterval(() => console.log('tick'), 500)"],
env: { ...process.env },
cwd: process.cwd(),
}),
healthUrl: () => `http://127.0.0.1:${port}/health`,
healthIntervalMs: 500,
stopTimeoutMs: 3_000,
logsBufferBytes: 1_048_576,
};
}
test.after(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
test("start spawns process and captures logs in ring buffer", async () => {
const healthServer = startHealthServer(29999);
const sup = new ServiceSupervisor(tickConfig("test-svc", 29999));
try {
const status = await sup.start();
assert.equal(status.state, "running");
assert.ok(status.pid !== null, "pid should be set");
// Wait briefly to let ticks accumulate
await new Promise((r) => setTimeout(r, 600));
const snap = sup.getRingBuffer().snapshot();
assert.ok(snap.length > 0, "ring buffer should have log entries");
assert.ok(
snap.some((e) => e.line.includes("tick")),
"should capture stdout lines"
);
} finally {
await sup.stop();
healthServer.close();
}
});
test("stop sends SIGTERM and waits, then SIGKILL if needed", async () => {
const healthServer = startHealthServer(29999);
const sup = new ServiceSupervisor({
...tickConfig("test-svc", 29999),
stopTimeoutMs: 500,
});
try {
await sup.start();
const status = await sup.stop();
assert.equal(status.state, "stopped");
assert.equal(status.pid, null);
} finally {
healthServer.close();
}
});
test("crash sets state=error and lastError (no auto-restart)", async () => {
const healthServer = startHealthServer(29998);
const crashConfig = {
...tickConfig("test-crash", 29998),
spawnArgs: () => ({
command: process.execPath,
// Exit after 1.5s — health server is up, so start() can return "running" first
args: ["-e", "setTimeout(() => process.exit(1), 1500)"],
env: { ...process.env },
cwd: process.cwd(),
}),
healthIntervalMs: 300,
};
const sup = new ServiceSupervisor(crashConfig);
const stateChanges: string[] = [];
sup.on("stateChange", (s) => stateChanges.push(s.state));
try {
await sup.start();
assert.equal(sup.getStatus().state, "running");
// Poll for the crash to be detected (process exits at 1.5s, health checker detects it
// within ~3 intervals). A fixed sleep flakes under CPU contention because the child's
// exit timer and the health-check intervals all slip; poll with a generous deadline.
const deadline = Date.now() + 10_000;
while (sup.getStatus().state !== "error" && Date.now() < deadline) {
await new Promise((r) => setTimeout(r, 100));
}
const status = sup.getStatus();
assert.equal(status.state, "error", "state should be error after crash");
assert.ok(status.lastError !== null, "lastError should be set");
assert.ok(
!stateChanges.filter((s) => s === "starting").length ||
stateChanges[stateChanges.length - 1] !== "starting",
"supervisor must not restart after crash"
);
} finally {
healthServer.close();
}
});
test("restart is atomic (concurrent calls serialize)", async () => {
const healthServer = startHealthServer(29999);
const sup = new ServiceSupervisor(tickConfig("test-svc", 29999));
try {
await sup.start();
// Fire 3 concurrent restarts — all should resolve without throwing
const [s1, s2, s3] = await Promise.all([sup.restart(), sup.restart(), sup.restart()]);
assert.equal(s1.state, "running");
assert.equal(s2.state, "running");
assert.equal(s3.state, "running");
const final = sup.getStatus();
assert.equal(final.state, "running");
} finally {
await sup.stop();
healthServer.close();
}
});
test("does NOT auto-restart on crash", async () => {
const healthServer = startHealthServer(29998);
const crashConfig = {
...tickConfig("test-crash", 29998),
spawnArgs: () => ({
command: process.execPath,
// Exit after 1.5s — same as crash test above
args: ["-e", "setTimeout(() => process.exit(2), 1500)"],
env: { ...process.env },
cwd: process.cwd(),
}),
healthIntervalMs: 300,
};
const sup = new ServiceSupervisor(crashConfig);
try {
await sup.start();
// Wait for crash + one more health interval
await new Promise((r) => setTimeout(r, 2_200));
const status = sup.getStatus();
// After crash: state must be "error" or "stopped", never "starting" again
assert.ok(
status.state === "error" || status.state === "stopped",
`supervisor should not auto-restart: state was "${status.state}"`
);
} finally {
healthServer.close();
}
});
// #6205: when probeBeforeSpawn is enabled and a healthy instance already serves
// the port, the supervisor ADOPTS it (marks running, no child spawned) instead
// of spawning a duplicate that would die with EADDRINUSE.
test("#6205: probeBeforeSpawn adopts a healthy existing instance (no spawn)", async () => {
const healthServer = startHealthServer(29996);
const cfg = { ...tickConfig("test-adopt", 29996), probeBeforeSpawn: true };
const sup = new ServiceSupervisor(cfg);
try {
const status = await sup.start();
assert.equal(status.state, "running", "adopted instance is marked running");
assert.equal(status.pid, null, "no child process is spawned when adopting");
// No child means no captured stdout ticks.
await new Promise((r) => setTimeout(r, 300));
assert.equal(sup.getRingBuffer().snapshot().length, 0, "no logs — nothing was spawned");
} finally {
await sup.stop();
healthServer.close();
}
});
@@ -0,0 +1,27 @@
import test from "node:test";
import assert from "node:assert/strict";
import { isLocalOnlyPath } from "../../../src/server/authz/routeGuard.ts";
test("isLocalOnlyPath: /api/services/bifrost/start is local-only", () => {
assert.equal(isLocalOnlyPath("/api/services/bifrost/start"), true);
});
test("isLocalOnlyPath: /api/services/bifrost/install is local-only", () => {
assert.equal(isLocalOnlyPath("/api/services/bifrost/install"), true);
});
test("isLocalOnlyPath: /api/services/bifrost/status is local-only", () => {
assert.equal(isLocalOnlyPath("/api/services/bifrost/status"), true);
});
test("isLocalOnlyPath: /api/services/bifrost/stop is local-only", () => {
assert.equal(isLocalOnlyPath("/api/services/bifrost/stop"), true);
});
test("isLocalOnlyPath: /api/services/bifrost/auto-start is local-only", () => {
assert.equal(isLocalOnlyPath("/api/services/bifrost/auto-start"), true);
});
test("isLocalOnlyPath: /api/services/bifrost/logs is local-only", () => {
assert.equal(isLocalOnlyPath("/api/services/bifrost/logs"), true);
});
@@ -0,0 +1,98 @@
/**
* Unit tests for §4b routing-layer wiring:
* getBifrostRoutingConfig uses supervised instance port when BIFROST_BASE_URL is unset.
*/
import test from "node:test";
import assert from "node:assert/strict";
import { registerSupervisor, unregisterSupervisor } from "../../../src/lib/services/registry.ts";
import { ServiceSupervisor } from "../../../src/lib/services/ServiceSupervisor.ts";
import { getBifrostRoutingConfig } from "../../../src/app/api/v1/relay/chat/completions/routingBackend.ts";
test("getBifrostRoutingConfig: returns null when BIFROST_BASE_URL unset and no supervised service", () => {
const result = getBifrostRoutingConfig({} as NodeJS.ProcessEnv);
assert.equal(result, null);
});
test("getBifrostRoutingConfig: uses BIFROST_BASE_URL when set (explicit env wins)", () => {
const result = getBifrostRoutingConfig({
BIFROST_BASE_URL: "http://localhost:9999",
} as NodeJS.ProcessEnv);
assert.ok(result !== null);
assert.equal(result?.baseUrl, "http://localhost:9999");
});
test("getBifrostRoutingConfig: uses supervised port when BIFROST_BASE_URL unset and bifrost running", () => {
// Register a stub supervisor whose getStatus() reports running on port 8080
const stub = {
getStatus: () => ({
tool: "bifrost",
state: "running" as const,
port: 8080,
health: "healthy" as const,
pid: 1234,
startedAt: new Date().toISOString(),
lastError: null,
}),
} as unknown as ServiceSupervisor;
registerSupervisor(stub);
try {
const result = getBifrostRoutingConfig({} as NodeJS.ProcessEnv);
assert.ok(result !== null, "should return config when supervised instance is running");
assert.equal(result?.baseUrl, "http://127.0.0.1:8080");
assert.equal(result?.enabled, true);
} finally {
unregisterSupervisor("bifrost");
}
});
test("getBifrostRoutingConfig: explicit BIFROST_BASE_URL overrides supervised port", () => {
const stub = {
getStatus: () => ({
tool: "bifrost",
state: "running" as const,
port: 8080,
health: "healthy" as const,
pid: 1234,
startedAt: new Date().toISOString(),
lastError: null,
}),
} as unknown as ServiceSupervisor;
registerSupervisor(stub);
try {
const result = getBifrostRoutingConfig({
BIFROST_BASE_URL: "http://remote-host:9999",
} as NodeJS.ProcessEnv);
assert.ok(result !== null);
// Explicit env wins
assert.equal(result?.baseUrl, "http://remote-host:9999");
} finally {
unregisterSupervisor("bifrost");
}
});
test("getBifrostRoutingConfig: stopped supervised instance does NOT provide baseUrl", () => {
const stub = {
getStatus: () => ({
tool: "bifrost",
state: "stopped" as const,
port: 8080,
health: "unknown" as const,
pid: null,
startedAt: null,
lastError: null,
}),
} as unknown as ServiceSupervisor;
registerSupervisor(stub);
try {
const result = getBifrostRoutingConfig({} as NodeJS.ProcessEnv);
assert.equal(result, null, "stopped supervisor should not yield a baseUrl");
} finally {
unregisterSupervisor("bifrost");
}
});
@@ -0,0 +1,300 @@
/**
* Tests for comboMetrics memory management — eviction and TTL behavior.
*
* Verifies that:
* - Recording and retrieval work correctly
* - MAX_METRICS_ENTRIES (500) cap is enforced via eviction of oldest entries
* - Shadow metrics are isolated from production metrics
* - resetComboMetrics and resetAllComboMetrics clear state correctly
* - getAllComboMetrics returns all entries
* - recordComboIntent tracks intent counts
*/
import test from "node:test";
import assert from "node:assert/strict";
const mod = await import("../../../open-sse/services/comboMetrics.ts");
const {
recordComboRequest,
recordComboShadowRequest,
getComboMetrics,
getAllComboMetrics,
resetComboMetrics,
resetAllComboMetrics,
recordComboIntent,
} = mod;
// Reset all state before each test group to avoid cross-test pollution.
resetAllComboMetrics();
// ─── Basic record + retrieve ────────────────────────────────────────────────
test("recordComboRequest: basic recording and retrieval", () => {
resetAllComboMetrics();
recordComboRequest("test-combo", "gpt-4", {
success: true,
latencyMs: 100,
});
const metrics = getComboMetrics("test-combo");
assert.ok(metrics, "metrics should exist for recorded combo");
assert.equal(metrics.totalRequests, 1, "should have 1 total request");
assert.equal(metrics.totalSuccesses, 1, "should have 1 success");
assert.equal(metrics.totalFailures, 0, "should have 0 failures");
assert.equal(metrics.successRate, 100, "success rate should be 100%");
assert.equal(metrics.avgLatencyMs, 100, "avg latency should be 100ms");
});
test("recordComboRequest: tracks per-model metrics", () => {
resetAllComboMetrics();
recordComboRequest("combo-1", "gpt-4", { success: true, latencyMs: 100 });
recordComboRequest("combo-1", "claude-3", { success: false, latencyMs: 200 });
const metrics = getComboMetrics("combo-1");
assert.ok(metrics);
assert.equal(metrics.totalRequests, 2, "should have 2 total requests");
assert.equal(metrics.successRate, 50, "success rate should be 50%");
assert.ok(metrics.byModel["gpt-4"], "should have gpt-4 model metrics");
assert.ok(metrics.byModel["claude-3"], "should have claude-3 model metrics");
assert.equal(metrics.byModel["gpt-4"].successRate, 100);
assert.equal(metrics.byModel["claude-3"].successRate, 0);
});
test("recordComboRequest: handles null model string gracefully", () => {
resetAllComboMetrics();
recordComboRequest("combo-null", null, { success: true, latencyMs: 50 });
const metrics = getComboMetrics("combo-null");
assert.ok(metrics, "metrics should exist even with null model");
assert.equal(metrics.totalRequests, 1);
// null model should not create per-model entries
assert.equal(Object.keys(metrics.byModel).length, 0, "should have no model entries");
});
test("recordComboRequest: tracks fallback count", () => {
resetAllComboMetrics();
recordComboRequest("combo-fb", "gpt-4", {
success: true,
latencyMs: 200,
fallbackCount: 2,
});
const metrics = getComboMetrics("combo-fb");
assert.ok(metrics);
assert.equal(metrics.totalFallbacks, 2, "should track 2 fallbacks");
// fallbackRate = (totalFallbacks / totalRequests) * 100 = (2/1) * 100 = 200
assert.equal(metrics.fallbackRate, 200, "fallback rate should be 200% (2 fallbacks on 1 request)");
});
// ─── getComboMetrics returns null for unknown combos ─────────────────────────
test("getComboMetrics: returns null for unknown combo", () => {
const metrics = getComboMetrics("nonexistent-combo-xyz");
assert.equal(metrics, null, "unknown combo should return null");
});
// ─── resetComboMetrics ───────────────────────────────────────────────────────
test("resetComboMetrics: clears a specific combo", () => {
resetAllComboMetrics();
recordComboRequest("to-keep", "gpt-4", { success: true, latencyMs: 100 });
recordComboRequest("to-reset", "gpt-4", { success: true, latencyMs: 100 });
resetComboMetrics("to-reset");
assert.ok(getComboMetrics("to-keep"), "to-keep should still exist");
assert.equal(getComboMetrics("to-reset"), null, "to-reset should be cleared");
});
// ─── resetAllComboMetrics ────────────────────────────────────────────────────
test("resetAllComboMetrics: clears all production and shadow metrics", () => {
recordComboRequest("combo-a", "gpt-4", { success: true, latencyMs: 100 });
recordComboShadowRequest("shadow-a", "gpt-4", { success: true, latencyMs: 100 });
assert.ok(getComboMetrics("combo-a"), "combo-a should exist before reset");
assert.ok(getComboMetrics("shadow-a"), "shadow-a should exist before reset");
resetAllComboMetrics();
assert.equal(getComboMetrics("combo-a"), null, "combo-a should be cleared");
assert.equal(getComboMetrics("shadow-a"), null, "shadow-a should be cleared");
});
// ─── getAllComboMetrics ───────────────────────────────────────────────────────
test("getAllComboMetrics: returns all production and shadow entries", () => {
resetAllComboMetrics();
recordComboRequest("combo-x", "gpt-4", { success: true, latencyMs: 100 });
recordComboRequest("combo-y", "claude-3", { success: false, latencyMs: 200 });
recordComboShadowRequest("shadow-x", "gpt-4", { success: true, latencyMs: 150 });
const all = getAllComboMetrics();
assert.ok(all["combo-x"], "should include combo-x");
assert.ok(all["combo-y"], "should include combo-y");
assert.ok(all["shadow-x"], "should include shadow-x from shadow metrics");
});
// ─── Shadow metrics ──────────────────────────────────────────────────────────
test("recordComboShadowRequest: tracks shadow metrics separately from production", () => {
resetAllComboMetrics();
recordComboRequest("prod-combo", "gpt-4", { success: true, latencyMs: 100 });
recordComboShadowRequest("prod-combo", "claude-3", { success: false, latencyMs: 200 });
const metrics = getComboMetrics("prod-combo");
assert.ok(metrics);
// Production sees only gpt-4
assert.equal(metrics.totalRequests, 1, "production should have 1 request");
assert.equal(metrics.successRate, 100, "production success rate should be 100%");
// Shadow is separate
assert.ok(metrics.shadow, "shadow metrics should be present");
assert.equal(metrics.shadow.totalRequests, 1, "shadow should have 1 request");
assert.equal(metrics.shadow.successRate, 0, "shadow success rate should be 0%");
});
test("recordComboShadowRequest: shadow-only combo returns in getAllComboMetrics", () => {
resetAllComboMetrics();
recordComboShadowRequest("shadow-only", "gpt-4", { success: true, latencyMs: 50 });
const all = getAllComboMetrics();
assert.ok(all["shadow-only"], "shadow-only combo should appear in getAllComboMetrics");
});
// ─── recordComboIntent ───────────────────────────────────────────────────────
test("recordComboIntent: tracks intent counts on existing combo", () => {
resetAllComboMetrics();
recordComboRequest("intent-combo", "gpt-4", { success: true, latencyMs: 100 });
recordComboIntent("intent-combo", "chat");
recordComboIntent("intent-combo", "chat");
recordComboIntent("intent-combo", "code");
const metrics = getComboMetrics("intent-combo");
assert.ok(metrics);
assert.equal(metrics.intentCounts["chat"], 2, "should count 2 chat intents");
assert.equal(metrics.intentCounts["code"], 1, "should count 1 code intent");
});
test("recordComboIntent: creates combo entry if not yet recorded", () => {
resetAllComboMetrics();
recordComboIntent("new-intent-combo", "search");
const metrics = getComboMetrics("new-intent-combo");
assert.ok(metrics, "combo should be created by recordComboIntent");
assert.equal(metrics.intentCounts["search"], 1);
});
// ─── Eviction: MAX_METRICS_ENTRIES cap ───────────────────────────────────────
test("eviction: inserting a new combo at capacity evicts the oldest entry", () => {
resetAllComboMetrics();
const MAX = 500;
// Fill to capacity with unique combos
for (let i = 0; i < MAX; i++) {
recordComboRequest(`fill-${i}`, "gpt-4", { success: true, latencyMs: 10 });
}
// All 500 should be present
assert.ok(getComboMetrics("fill-0"), "first inserted combo should exist at capacity");
assert.ok(getComboMetrics(`fill-${MAX - 1}`), "last inserted combo should exist at capacity");
// Insert one more — should trigger eviction of the oldest entry
recordComboRequest("new-after-capacity", "gpt-4", { success: true, latencyMs: 50 });
const all = getAllComboMetrics();
const totalProduction = Object.keys(all).filter((k) => {
const m = all[k];
return m && m.totalRequests > 0;
}).length;
// Map size should not exceed MAX (the new one replaced the oldest)
assert.ok(totalProduction <= MAX, `production combos (${totalProduction}) should not exceed cap (${MAX})`);
assert.ok(
getComboMetrics("new-after-capacity"),
"newly inserted combo should exist after eviction"
);
});
test("eviction: shadow metrics respect their own MAX_METRICS_ENTRIES cap", () => {
resetAllComboMetrics();
const MAX = 500;
// Fill shadow metrics to capacity
for (let i = 0; i < MAX; i++) {
recordComboShadowRequest(`shadow-fill-${i}`, "gpt-4", {
success: true,
latencyMs: 10,
});
}
assert.ok(
getComboMetrics("shadow-fill-0"),
"first shadow combo should exist at capacity"
);
// Insert one more shadow — should trigger eviction
recordComboShadowRequest("shadow-after-capacity", "gpt-4", {
success: true,
latencyMs: 50,
});
assert.ok(
getComboMetrics("shadow-after-capacity"),
"newly inserted shadow combo should exist after eviction"
);
});
test("eviction: shadow overflow does not delete production metrics with the same name", () => {
resetAllComboMetrics();
const MAX = 500;
recordComboRequest("shared-combo", "gpt-4", { success: true, latencyMs: 10 });
for (let i = 0; i < MAX; i++) {
recordComboShadowRequest(i === 0 ? "shared-combo" : "shadow-fill-" + i, "gpt-4", {
success: true,
latencyMs: 10,
});
}
recordComboShadowRequest("shadow-after-capacity", "gpt-4", {
success: true,
latencyMs: 50,
});
const metrics = getComboMetrics("shared-combo");
assert.ok(metrics, "production metrics must survive shadow-only eviction");
assert.equal(metrics.productionTraffic, true);
assert.equal(metrics.totalRequests, 1);
});
test("eviction: recordComboIntent respects MAX_METRICS_ENTRIES cap", () => {
resetAllComboMetrics();
const MAX = 500;
// Fill production metrics to capacity
for (let i = 0; i < MAX; i++) {
recordComboRequest(`intent-fill-${i}`, "gpt-4", { success: true, latencyMs: 10 });
}
// Adding intent for a NEW combo should trigger eviction
recordComboIntent("intent-after-capacity", "chat");
assert.ok(
getComboMetrics("intent-after-capacity"),
"newly created intent combo should exist after eviction"
);
});
test("eviction: updating an existing combo at capacity does NOT evict", () => {
resetAllComboMetrics();
const MAX = 500;
// Fill to capacity
for (let i = 0; i < MAX; i++) {
recordComboRequest(`update-${i}`, "gpt-4", { success: true, latencyMs: 10 });
}
// Update an existing combo — should NOT trigger eviction since it's not a new key
recordComboRequest("update-0", "gpt-4", { success: false, latencyMs: 50 });
const metrics = getComboMetrics("update-0");
assert.ok(metrics, "updated combo should still exist");
assert.equal(metrics.totalRequests, 2, "updated combo should have 2 requests");
});
// ─── Strategy tracking ───────────────────────────────────────────────────────
test("recordComboRequest: stores the routing strategy", () => {
resetAllComboMetrics();
recordComboRequest("strat-combo", "gpt-4", {
success: true,
latencyMs: 100,
strategy: "weighted",
});
const metrics = getComboMetrics("strat-combo");
assert.ok(metrics);
assert.equal(metrics.strategy, "weighted", "should store the strategy");
});
test("recordComboRequest: defaults strategy to 'priority'", () => {
resetAllComboMetrics();
recordComboRequest("default-strat", "gpt-4", { success: true, latencyMs: 100 });
const metrics = getComboMetrics("default-strat");
assert.ok(metrics);
assert.equal(metrics.strategy, "priority", "default strategy should be 'priority'");
});
+392
View File
@@ -0,0 +1,392 @@
/**
* T-07 — embed proxy route handler tests.
*
* Tests GET/POST/PUT/PATCH/DELETE handlers in
* /dashboard/providers/services/[name]/embed/[[...path]]/route.ts.
*
* Uses registerSupervisor to inject fake supervisors (ESM live bindings
* can't be reassigned, so direct module patching is not possible).
*/
import { describe, it, afterEach } from "node:test";
import assert from "node:assert/strict";
import { registerSupervisor, unregisterSupervisor } from "../../../src/lib/services/registry.ts";
import type { ServiceSupervisor } from "../../../src/lib/services/ServiceSupervisor.ts";
import {
GET,
POST,
PUT,
PATCH,
DELETE,
HEAD,
OPTIONS,
} from "../../../src/app/(dashboard)/dashboard/providers/services/[name]/embed/[[...path]]/route.ts";
const originalFetch = globalThis.fetch;
afterEach(() => {
globalThis.fetch = originalFetch;
unregisterSupervisor("9router");
});
// ─── helpers ────────────────────────────────────────────────────────────────
function makeFakeParams(
name: string,
path: string[]
): { params: Promise<{ name: string; path: string[] }> } {
return { params: Promise.resolve({ name, path }) };
}
function registerFake(state: string, port: number): void {
const fake = {
getStatus: () => ({
tool: "9router",
state,
port,
pid: null,
health: "unknown" as const,
startedAt: null,
lastError: null,
}),
};
registerSupervisor(fake as unknown as ServiceSupervisor);
}
// ─── tests ───────────────────────────────────────────────────────────────────
describe("embed proxy route", () => {
it("returns 404 for unknown service", async () => {
// No supervisor registered — getSupervisor returns null.
const req = new Request("http://localhost/dashboard/providers/services/unknown/embed/");
const resp = await GET(req, makeFakeParams("unknown", []));
assert.equal(resp.status, 404);
});
it("returns 503 when service exists but is not running", async () => {
registerFake("stopped", 20130);
const req = new Request("http://localhost/dashboard/providers/services/9router/embed/");
const resp = await GET(req, makeFakeParams("9router", []));
assert.equal(resp.status, 503);
});
it("proxies GET to the upstream service", async () => {
registerFake("running", 20130);
let capturedUrl = "";
globalThis.fetch = async (input: string | URL | Request) => {
capturedUrl = String(input);
return new Response("<html>9router UI</html>", {
status: 200,
headers: { "content-type": "text/html" },
});
};
const req = new Request(
"http://localhost/dashboard/providers/services/9router/embed/ui/index.html"
);
const resp = await GET(req, makeFakeParams("9router", ["ui", "index.html"]));
assert.equal(resp.status, 200);
assert.ok(capturedUrl.startsWith("http://127.0.0.1:20130/ui/index.html"));
assert.ok((await resp.text()).includes("9router UI"));
});
it("forwards query string to upstream", async () => {
registerFake("running", 20130);
let capturedUrl = "";
globalThis.fetch = async (input: string | URL | Request) => {
capturedUrl = String(input);
return new Response("{}", { status: 200 });
};
const req = new Request(
"http://localhost/dashboard/providers/services/9router/embed/api/models?page=2"
);
await GET(req, makeFakeParams("9router", ["api", "models"]));
assert.ok(capturedUrl.includes("?page=2"));
});
it("proxies POST and forwards body", async () => {
registerFake("running", 20130);
let capturedMethod = "";
globalThis.fetch = async (_input: string | URL | Request, init?: RequestInit) => {
capturedMethod = init?.method ?? "UNKNOWN";
return new Response('{"ok":true}', { status: 200 });
};
const req = new Request("http://localhost/dashboard/providers/services/9router/embed/api/v1", {
method: "POST",
body: JSON.stringify({ test: 1 }),
headers: { "content-type": "application/json" },
});
const resp = await POST(req, makeFakeParams("9router", ["api", "v1"]));
assert.equal(resp.status, 200);
assert.equal(capturedMethod, "POST");
});
it("strips hop-by-hop headers from the upstream response", async () => {
registerFake("running", 20130);
globalThis.fetch = async () =>
new Response("body", {
status: 200,
headers: {
"content-type": "text/plain",
"transfer-encoding": "chunked",
connection: "keep-alive",
"x-custom": "kept",
},
});
const req = new Request("http://localhost/dashboard/providers/services/9router/embed/");
const resp = await GET(req, makeFakeParams("9router", []));
assert.equal(resp.headers.get("x-custom"), "kept");
assert.equal(resp.headers.get("transfer-encoding"), null);
assert.equal(resp.headers.get("connection"), null);
});
it("returns 502 on upstream network error", async () => {
registerFake("running", 20130);
globalThis.fetch = async () => {
throw new Error("ECONNREFUSED");
};
const req = new Request("http://localhost/dashboard/providers/services/9router/embed/");
const resp = await GET(req, makeFakeParams("9router", []));
assert.equal(resp.status, 502);
});
it("PUT, PATCH, DELETE are handled", async () => {
registerFake("running", 20130);
globalThis.fetch = async (_input: string | URL | Request, init?: RequestInit) =>
new Response(null, { status: 204 });
const params = makeFakeParams("9router", ["resource", "1"]);
const reqUrl = "http://localhost/dashboard/providers/services/9router/embed/resource/1";
assert.equal((await PUT(new Request(reqUrl, { method: "PUT" }), params)).status, 204);
assert.equal((await PATCH(new Request(reqUrl, { method: "PATCH" }), params)).status, 204);
assert.equal((await DELETE(new Request(reqUrl, { method: "DELETE" }), params)).status, 204);
});
// ─── G-05: cookie/auth strip + response header strip + HTML rewrite ──────────
it("G-05: strips cookie header before forwarding to upstream", async () => {
registerFake("running", 20130);
let capturedHeaders: Record<string, string> = {};
globalThis.fetch = async (_input: string | URL | Request, init?: RequestInit) => {
for (const [k, v] of new Headers(init?.headers as HeadersInit).entries()) {
capturedHeaders[k.toLowerCase()] = v;
}
return new Response("ok", { status: 200 });
};
const req = new Request("http://localhost/dashboard/providers/services/9router/embed/", {
headers: { cookie: "session=abc123; jwt=secret" },
});
await GET(req, makeFakeParams("9router", []));
assert.equal(capturedHeaders["cookie"], undefined, "cookie must not be forwarded upstream");
});
it("G-05: sets Authorization: Bearer on upstream request", async () => {
registerFake("running", 20130);
let capturedAuth: string | undefined;
globalThis.fetch = async (_input: string | URL | Request, init?: RequestInit) => {
capturedAuth = new Headers(init?.headers as HeadersInit).get("authorization") ?? undefined;
return new Response("ok", { status: 200 });
};
const req = new Request("http://localhost/dashboard/providers/services/9router/embed/");
await GET(req, makeFakeParams("9router", []));
assert.ok(capturedAuth, "authorization header must be present");
assert.ok(capturedAuth!.startsWith("Bearer "), "authorization must be a Bearer token");
});
it("G-05: strips client Authorization before forwarding, injects service key instead", async () => {
registerFake("running", 20130);
let capturedAuth: string | undefined;
globalThis.fetch = async (_input: string | URL | Request, init?: RequestInit) => {
capturedAuth = new Headers(init?.headers as HeadersInit).get("authorization") ?? undefined;
return new Response("ok", { status: 200 });
};
const req = new Request("http://localhost/dashboard/providers/services/9router/embed/", {
headers: { authorization: "Bearer client-token-should-not-leak" },
});
await GET(req, makeFakeParams("9router", []));
assert.ok(capturedAuth, "authorization header must be set");
assert.notEqual(
capturedAuth,
"Bearer client-token-should-not-leak",
"client authorization must not be forwarded as-is"
);
});
it("G-05: strips set-cookie from upstream response", async () => {
registerFake("running", 20130);
globalThis.fetch = async () =>
new Response("ok", {
status: 200,
headers: { "set-cookie": "session=upstream; Path=/", "content-type": "text/plain" },
});
const resp = await GET(
new Request("http://localhost/dashboard/providers/services/9router/embed/"),
makeFakeParams("9router", [])
);
assert.equal(resp.headers.get("set-cookie"), null, "set-cookie must be stripped from response");
});
it("G-05: strips x-frame-options from upstream response", async () => {
registerFake("running", 20130);
globalThis.fetch = async () =>
new Response("ok", {
status: 200,
headers: { "x-frame-options": "DENY", "content-type": "text/plain" },
});
const resp = await GET(
new Request("http://localhost/dashboard/providers/services/9router/embed/"),
makeFakeParams("9router", [])
);
assert.equal(resp.headers.get("x-frame-options"), null, "x-frame-options must be stripped");
});
it("G-05: strips content-security-policy from upstream response", async () => {
registerFake("running", 20130);
globalThis.fetch = async () =>
new Response("ok", {
status: 200,
headers: {
"content-security-policy": "default-src 'none'",
"content-type": "text/plain",
},
});
const resp = await GET(
new Request("http://localhost/dashboard/providers/services/9router/embed/"),
makeFakeParams("9router", [])
);
assert.equal(
resp.headers.get("content-security-policy"),
null,
"content-security-policy must be stripped"
);
});
it("G-05: strips cross-origin-* headers from upstream response", async () => {
registerFake("running", 20130);
globalThis.fetch = async () =>
new Response("ok", {
status: 200,
headers: {
"cross-origin-embedder-policy": "require-corp",
"cross-origin-opener-policy": "same-origin",
"cross-origin-resource-policy": "same-site",
"content-type": "text/plain",
},
});
const resp = await GET(
new Request("http://localhost/dashboard/providers/services/9router/embed/"),
makeFakeParams("9router", [])
);
assert.equal(
resp.headers.get("cross-origin-embedder-policy"),
null,
"cross-origin-embedder-policy must be stripped"
);
assert.equal(
resp.headers.get("cross-origin-opener-policy"),
null,
"cross-origin-opener-policy must be stripped"
);
assert.equal(
resp.headers.get("cross-origin-resource-policy"),
null,
"cross-origin-resource-policy must be stripped"
);
});
it("G-05: HTML response is rewritten — contains injected <base href>", async () => {
registerFake("running", 20130);
globalThis.fetch = async () =>
new Response('<html><head></head><body><a href="/dashboard">link</a></body></html>', {
status: 200,
headers: { "content-type": "text/html; charset=utf-8" },
});
const resp = await GET(
new Request("http://localhost/dashboard/providers/services/9router/embed/"),
makeFakeParams("9router", [])
);
const body = await resp.text();
assert.ok(
body.includes('<base href="/dashboard/providers/services/9router/embed/">'),
`Expected <base href> in rewritten HTML. Got: ${body.substring(0, 200)}`
);
});
it("G-05: HTML response rewrites path-absolute links to go through proxy", async () => {
registerFake("running", 20130);
globalThis.fetch = async () =>
new Response('<html><head></head><body><a href="/ui/page">x</a></body></html>', {
status: 200,
headers: { "content-type": "text/html" },
});
const resp = await GET(
new Request("http://localhost/dashboard/providers/services/9router/embed/"),
makeFakeParams("9router", [])
);
const body = await resp.text();
assert.ok(
body.includes('href="/dashboard/providers/services/9router/embed/ui/page"'),
`Expected rewritten href. Got: ${body.substring(0, 300)}`
);
});
it("G-05: JSON response is NOT rewritten (streaming pass-through)", async () => {
registerFake("running", 20130);
const jsonPayload = '{"models":["gpt-4","claude-3"]}';
globalThis.fetch = async () =>
new Response(jsonPayload, {
status: 200,
headers: { "content-type": "application/json" },
});
const resp = await GET(
new Request("http://localhost/dashboard/providers/services/9router/embed/api/models"),
makeFakeParams("9router", ["api", "models"])
);
const body = await resp.text();
assert.equal(body, jsonPayload, "JSON response must pass through unchanged");
});
it("G-05: HEAD method is handled", async () => {
registerFake("running", 20130);
globalThis.fetch = async () =>
new Response(null, { status: 200, headers: { "content-type": "text/html" } });
const req = new Request("http://localhost/dashboard/providers/services/9router/embed/", {
method: "HEAD",
});
const resp = await HEAD(req, makeFakeParams("9router", []));
assert.equal(resp.status, 200);
});
it("G-05: OPTIONS method is handled", async () => {
registerFake("running", 20130);
globalThis.fetch = async () =>
new Response(null, {
status: 204,
headers: { allow: "GET, HEAD, POST, OPTIONS" },
});
const req = new Request("http://localhost/dashboard/providers/services/9router/embed/", {
method: "OPTIONS",
});
const resp = await OPTIONS(req, makeFakeParams("9router", []));
assert.equal(resp.status, 204);
});
});
+245
View File
@@ -0,0 +1,245 @@
/**
* T-08 — embedWsProxy unit tests.
*
* Tests the internal helper functions of embedWsProxy.ts without starting
* a real server (avoids port binding in CI). Focuses on:
* - writeError writes a valid HTTP error response to the socket
* - proxyUpgrade rejects unknown services (404)
* - proxyUpgrade rejects non-running services (503)
* - proxyUpgrade connects to the right upstream port for known services
* - G-06: rejects 51st concurrent connection with 503
* - G-06: strips cookie/authorization/origin from upgrade headers
* - G-06: injects Bearer apiKey into upgrade headers
*/
import { describe, it, afterEach } from "node:test";
import assert from "node:assert/strict";
import net from "node:net";
import {
registerSupervisor,
unregisterSupervisor,
getSupervisor,
} from "../../../src/lib/services/registry.ts";
const registryPublicApi = await import("../../../src/lib/services/registry.ts");
import type { ServiceSupervisor } from "../../../src/lib/services/ServiceSupervisor.ts";
import {
activeConnections,
registerConnection,
unregisterConnection,
buildUpstreamHeaders,
MAX_CONNECTIONS_PER_SERVICE,
} from "../../../src/lib/services/embedWsProxy.ts";
afterEach(() => {
unregisterSupervisor("9router");
// Clean up any connections left by G-06 tests
activeConnections.delete("test-service");
activeConnections.delete("9router");
});
// ─── helpers ────────────────────────────────────────────────────────────────
function registerFake(state: string, port: number): void {
registerSupervisor({
getStatus: () => ({
tool: "9router",
state,
port,
pid: null,
health: "unknown" as const,
startedAt: null,
lastError: null,
}),
} as unknown as ServiceSupervisor);
}
/** Creates a mock socket that captures written bytes and emits "connect". */
function makeSocket(): { socket: net.Socket; received: Buffer[] } {
const received: Buffer[] = [];
const socket = new net.Socket();
(socket as { write: (chunk: Buffer | string) => void }).write = (chunk: Buffer | string) => {
received.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
};
(socket as { end: (chunk?: Buffer | string) => void }).end = (chunk?: Buffer | string) => {
if (chunk) received.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
};
Object.defineProperty(socket, "writable", { get: () => true });
Object.defineProperty(socket, "destroyed", { get: () => false });
return { socket, received };
}
/** Reads all received buffers as a single string. */
function joined(received: Buffer[]): string {
return Buffer.concat(received).toString();
}
// ─── tests ───────────────────────────────────────────────────────────────────
describe("embedWsProxy", () => {
it("registry public surface excludes unused supervisor listing helper", () => {
assert.equal("listSupervisors" in registryPublicApi, false);
});
it("idempotent — initEmbedWsProxy does not bind twice", async () => {
// Reset the global flag so we can test it from scratch
const prev = globalThis.__omnirouteEmbedWsStarted;
globalThis.__omnirouteEmbedWsStarted = true;
const { initEmbedWsProxy } = await import("../../../src/lib/services/embedWsProxy.ts");
// Should return immediately without creating a server (already started)
assert.doesNotThrow(() => initEmbedWsProxy());
globalThis.__omnirouteEmbedWsStarted = prev;
});
it("PATH_RE: /9router/path correctly identifies name and rest", () => {
// Test the path regex logic by simulating what proxyUpgrade does.
const PATH_RE = /^\/([^/?#]+)(\/.*)?$/;
const m1 = PATH_RE.exec("/9router/ui/index.html");
assert.ok(m1);
assert.equal(m1[1], "9router");
assert.equal(m1[2], "/ui/index.html");
const m2 = PATH_RE.exec("/9router");
assert.ok(m2);
assert.equal(m2[1], "9router");
assert.equal(m2[2], undefined);
assert.equal(PATH_RE.exec("/"), null);
assert.equal(PATH_RE.exec(""), null);
});
it("writeError sends a well-formed HTTP error response", () => {
const { socket, received } = makeSocket();
// Simulate what writeError does (same logic as the module)
const status = 404;
const message = "Service 'foo' not found";
const body = Buffer.from(JSON.stringify({ error: message }), "utf8");
const lines = [
`HTTP/1.1 ${status} Not Found`,
"Connection: close",
"Content-Type: application/json; charset=utf-8",
`Content-Length: ${body.length}`,
"",
"",
];
socket.write(lines.join("\r\n"));
socket.end(body);
const raw = joined(received);
assert.ok(raw.startsWith("HTTP/1.1 404 Not Found\r\n"), "starts with status line");
assert.ok(raw.includes("Content-Type: application/json"), "has content-type");
assert.ok(raw.includes(message), "body contains message");
});
it("getSupervisor lookup fails for unregistered name → null", () => {
assert.equal(getSupervisor("nonexistent"), null);
});
it("service registered as stopped is detectable via getStatus", () => {
registerFake("stopped", 20130);
const sup = getSupervisor("9router");
assert.ok(sup !== null);
const status = sup.getStatus();
assert.equal(status.state, "stopped");
assert.equal(status.port, 20130);
});
it("service registered as running is detectable via getStatus", () => {
registerFake("running", 20130);
const sup = getSupervisor("9router");
assert.ok(sup !== null);
assert.equal(sup.getStatus().state, "running");
});
// ─── G-06 tests ──────────────────────────────────────────────────────────
it("G-06: rejects 51st concurrent connection with 503", () => {
const serviceName = "test-service";
// Fill up to the limit
const sockets: net.Socket[] = [];
for (let i = 0; i < MAX_CONNECTIONS_PER_SERVICE; i++) {
const { socket } = makeSocket();
const accepted = registerConnection(serviceName, socket);
assert.ok(accepted, `connection ${i + 1} should be accepted`);
sockets.push(socket);
}
// The 51st should be rejected
const { socket: socket51, received: received51 } = makeSocket();
const rejected = registerConnection(serviceName, socket51);
assert.equal(rejected, false, "51st connection must be rejected");
// The response written to the 51st socket must be a 503
const raw = joined(received51);
assert.ok(raw.startsWith("HTTP/1.1 503"), "rejected socket gets 503 status line");
assert.ok(raw.includes("connection limit"), "503 body mentions connection limit");
// Clean up
for (const s of sockets) {
unregisterConnection(serviceName, s);
}
});
it("G-06: strips cookie, authorization, and origin from upgrade headers", () => {
const rawHeaders = [
"Host",
"localhost:3000",
"Connection",
"Upgrade",
"Upgrade",
"websocket",
"Cookie",
"session=abc123",
"Authorization",
"Bearer client-token",
"Origin",
"http://localhost:3000",
"Sec-WebSocket-Key",
"dGhlIHNhbXBsZSBub25jZQ==",
"Sec-WebSocket-Version",
"13",
];
const headers = buildUpstreamHeaders(rawHeaders, 20130, "nr_injectedkey");
const headerStr = headers.join("\r\n").toLowerCase();
assert.ok(!headerStr.includes("cookie:"), "cookie must be stripped");
assert.ok(
!headerStr.includes("bearer client-token"),
"original authorization must be stripped"
);
assert.ok(!headerStr.includes("origin:"), "origin must be stripped");
// Non-stripped headers must remain
assert.ok(headerStr.includes("upgrade: websocket"), "upgrade header must be preserved");
assert.ok(headerStr.includes("sec-websocket-key:"), "sec-websocket-key must be preserved");
});
it("G-06: injects Bearer apiKey into upgrade headers replacing any client Authorization", () => {
const apiKey = "nr_testapikey1234";
const rawHeaders = [
"Host",
"localhost",
"Authorization",
"Bearer old-client-token",
"Upgrade",
"websocket",
];
const headers = buildUpstreamHeaders(rawHeaders, 20130, apiKey);
const authHeaders = headers.filter((h) => h.toLowerCase().startsWith("authorization:"));
// Must have exactly one Authorization header
assert.equal(authHeaders.length, 1, "exactly one Authorization header must be present");
assert.ok(
authHeaders[0].includes(`Bearer ${apiKey}`),
`Authorization must be 'Bearer ${apiKey}', got: ${authHeaders[0]}`
);
});
});
@@ -0,0 +1,189 @@
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 previousDataDir = process.env.DATA_DIR;
const previousDisableSqliteAutoBackup = process.env.DISABLE_SQLITE_AUTO_BACKUP;
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-emergency-fallback-"));
process.env.DATA_DIR = tmpDir;
process.env.DISABLE_SQLITE_AUTO_BACKUP = "true";
const core = await import("../../../src/lib/db/core.ts");
const { setFeatureFlagOverride, removeFeatureFlagOverride } =
await import("../../../src/lib/db/featureFlags.ts");
const {
shouldUseFallback,
isEmergencyFallbackEnvEnabled,
EMERGENCY_FALLBACK_CONFIG,
resetEmergencyFallbackEnvCache,
setEmergencyFallbackFeatureFlagResolverForTest,
} = await import("../../../open-sse/services/emergencyFallback.ts");
function restoreEnv(name: string, value: string | undefined) {
if (value === undefined) {
delete process.env[name];
} else {
process.env[name] = value;
}
}
function resetTestState() {
core.resetDbInstance();
fs.rmSync(tmpDir, { recursive: true, force: true });
fs.mkdirSync(tmpDir, { recursive: true });
delete process.env.OMNIROUTE_EMERGENCY_FALLBACK;
resetEmergencyFallbackEnvCache();
setEmergencyFallbackFeatureFlagResolverForTest(null);
}
test.beforeEach(() => {
resetTestState();
});
test.afterEach(() => {
resetEmergencyFallbackEnvCache();
setEmergencyFallbackFeatureFlagResolverForTest(null);
delete process.env.OMNIROUTE_EMERGENCY_FALLBACK;
});
test.after(() => {
core.resetDbInstance();
fs.rmSync(tmpDir, { recursive: true, force: true });
restoreEnv("DATA_DIR", previousDataDir);
restoreEnv("DISABLE_SQLITE_AUTO_BACKUP", previousDisableSqliteAutoBackup);
});
function withEnv(value: string | undefined, fn: () => void) {
const previous = process.env.OMNIROUTE_EMERGENCY_FALLBACK;
if (value === undefined) {
delete process.env.OMNIROUTE_EMERGENCY_FALLBACK;
} else {
process.env.OMNIROUTE_EMERGENCY_FALLBACK = value;
}
resetEmergencyFallbackEnvCache();
try {
fn();
} finally {
restoreEnv("OMNIROUTE_EMERGENCY_FALLBACK", previous);
resetEmergencyFallbackEnvCache();
}
}
function withFeatureFlagOverride(value: string, fn: () => void) {
try {
setFeatureFlagOverride("OMNIROUTE_EMERGENCY_FALLBACK", value);
resetEmergencyFallbackEnvCache();
fn();
} finally {
removeFeatureFlagOverride("OMNIROUTE_EMERGENCY_FALLBACK");
resetEmergencyFallbackEnvCache();
}
}
test("emergency fallback stays enabled when the env switch is unset (default behavior)", () => {
withEnv(undefined, () => {
assert.equal(isEmergencyFallbackEnvEnabled(), true);
const decision = shouldUseFallback(402, "", false);
assert.equal(decision.shouldFallback, true);
if (decision.shouldFallback) {
assert.equal(decision.provider, EMERGENCY_FALLBACK_CONFIG.provider);
assert.equal(decision.model, EMERGENCY_FALLBACK_CONFIG.model);
}
});
});
test("budget keywords trigger fallback when the env switch is unset", () => {
withEnv(undefined, () => {
const decision = shouldUseFallback(429, "All accounts quota exceeded", false);
assert.equal(decision.shouldFallback, true);
});
});
test("OMNIROUTE_EMERGENCY_FALLBACK=false disables the 402 redirect", () => {
withEnv("false", () => {
assert.equal(isEmergencyFallbackEnvEnabled(), false);
const decision = shouldUseFallback(402, "", false);
assert.equal(decision.shouldFallback, false);
assert.match(decision.reason, /OMNIROUTE_EMERGENCY_FALLBACK/);
});
});
test("OMNIROUTE_EMERGENCY_FALLBACK=0 disables the budget-keyword redirect", () => {
withEnv("0", () => {
const decision = shouldUseFallback(429, "quota exceeded for account", false);
assert.equal(decision.shouldFallback, false);
assert.match(decision.reason, /OMNIROUTE_EMERGENCY_FALLBACK/);
});
});
test("DB feature flag override can disable an env-enabled fallback", () => {
withEnv("true", () => {
withFeatureFlagOverride("false", () => {
const decision = shouldUseFallback(402, "", false);
assert.equal(isEmergencyFallbackEnvEnabled(), false);
assert.equal(decision.shouldFallback, false);
assert.match(decision.reason, /OMNIROUTE_EMERGENCY_FALLBACK/);
});
});
});
test("DB feature flag override can enable an env-disabled fallback", () => {
withEnv("false", () => {
withFeatureFlagOverride("true", () => {
const decision = shouldUseFallback(402, "", false);
assert.equal(isEmergencyFallbackEnvEnabled(), true);
assert.equal(decision.shouldFallback, true);
});
});
});
test("raw env fallback is used when feature flag resolution throws", () => {
withEnv("0", () => {
const warnings: unknown[][] = [];
const previousWarn = console.warn;
console.warn = (...args: unknown[]) => {
warnings.push(args);
};
setEmergencyFallbackFeatureFlagResolverForTest(() => {
throw new Error("feature flag store unavailable");
});
try {
assert.equal(isEmergencyFallbackEnvEnabled(), false);
const decision = shouldUseFallback(402, "", false);
assert.equal(decision.shouldFallback, false);
assert.match(decision.reason, /OMNIROUTE_EMERGENCY_FALLBACK/);
assert.equal(warnings.length, 1);
assert.match(String(warnings[0]?.[0]), /Feature flag resolution failed/);
} finally {
console.warn = previousWarn;
}
});
});
test("explicit truthy values keep the fallback enabled", () => {
withEnv("true", () => {
assert.equal(isEmergencyFallbackEnvEnabled(), true);
assert.equal(shouldUseFallback(402, "", false).shouldFallback, true);
});
});
test("env switch does not override config.enabled=false", () => {
withEnv("true", () => {
const decision = shouldUseFallback(402, "", false, {
...EMERGENCY_FALLBACK_CONFIG,
enabled: false,
});
assert.equal(decision.shouldFallback, false);
});
});
test("tool-bearing requests are still skipped regardless of env switch", () => {
withEnv(undefined, () => {
const decision = shouldUseFallback(402, "", true);
assert.equal(decision.shouldFallback, false);
assert.match(decision.reason, /tools/);
});
});
@@ -0,0 +1,552 @@
/**
* G-12: End-to-end shape consistency tests for embedded service routes.
*
* Tests the response shape of the 7 core routes for both 9router and cliproxy
* by importing handlers directly and mocking their dependencies.
*
* No real HTTP server required — handlers are invoked as plain async functions.
*
* Covered routes:
* 9router: status (GET), start (POST), stop (POST), install (POST)
* cliproxy: status (GET), start (POST), stop (POST), install (POST)
*/
import { describe, it, before, after, mock } from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
// ---------------------------------------------------------------------------
// Isolated test DB
// ---------------------------------------------------------------------------
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-e2e-shape-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.NODE_ENV = "test";
process.env.DISABLE_SQLITE_AUTO_BACKUP = "true";
process.env.NINEROUTER_PORT = "20130";
// Boot DB before any route imports so the schema is ready.
const core = await import("../../../src/lib/db/core.ts");
const { upsertVersionManagerTool } = await import("../../../src/lib/db/versionManager.ts");
// Seed both services as "stopped" (installed) so lifecycle routes proceed.
await upsertVersionManagerTool({ tool: "9router", status: "stopped" });
await upsertVersionManagerTool({ tool: "cliproxy", status: "stopped" });
// ---------------------------------------------------------------------------
// Minimal mock supervisor that shapes match what the handlers produce
// ---------------------------------------------------------------------------
const makeRunningStatus = (tool: string, port: number) => ({
tool,
state: "running" as const,
pid: 12345,
port,
health: "healthy" as const,
startedAt: new Date().toISOString(),
lastError: null,
});
const makeStoppedStatus = (tool: string, port: number) => ({
tool,
state: "stopped" as const,
pid: null,
port,
health: "unknown" as const,
startedAt: null,
lastError: null,
});
// ---------------------------------------------------------------------------
// Helper: parse a Response body safely
// ---------------------------------------------------------------------------
async function jsonBody(res: Response): Promise<unknown> {
return res.json();
}
// ---------------------------------------------------------------------------
// Helper: assert common error shape { error: { message, type }, requestId }
// ---------------------------------------------------------------------------
function assertErrorShape(body: unknown): void {
const b = body as Record<string, unknown>;
assert.ok("error" in b, "Error response must have 'error' field");
const err = b.error as Record<string, unknown>;
assert.ok(typeof err.message === "string", "error.message must be a string");
assert.ok(typeof err.type === "string", "error.type must be a string");
assert.ok(typeof b.requestId === "string", "Error response must have 'requestId' field");
// Hard rule #12: no stack trace exposure
const bodyStr = JSON.stringify(body);
assert.ok(!bodyStr.includes("at /"), "Error body must not expose stack trace");
}
// ---------------------------------------------------------------------------
// Helper: assert ServiceStatus shape { tool, state, pid, port, health, ... }
// ---------------------------------------------------------------------------
function assertServiceStatusShape(body: unknown, expectedTool: string): void {
const b = body as Record<string, unknown>;
assert.ok(typeof b.tool === "string", `body.tool must be string, got ${typeof b.tool}`);
assert.equal(b.tool, expectedTool, `body.tool must be '${expectedTool}'`);
assert.ok(typeof b.state === "string", `body.state must be string, got ${typeof b.state}`);
assert.ok(
b.pid === null || typeof b.pid === "number",
`body.pid must be null or number, got ${typeof b.pid}`
);
assert.ok(typeof b.port === "number", `body.port must be number, got ${typeof b.port}`);
assert.ok(typeof b.health === "string", `body.health must be string, got ${typeof b.health}`);
assert.ok(
b.startedAt === null || typeof b.startedAt === "string",
`body.startedAt must be null or string, got ${typeof b.startedAt}`
);
assert.ok(
b.lastError === null || typeof b.lastError === "string",
`body.lastError must be null or string, got ${typeof b.lastError}`
);
}
// ---------------------------------------------------------------------------
// 9router routes
// ---------------------------------------------------------------------------
describe("9router — response shapes", () => {
describe("GET /api/services/9router/status", () => {
it("200 response has enriched status shape including installedVersion and apiKeyMasked", async () => {
const { GET } =
await import("../../../src/app/api/services/9router/status/route.ts?t=shape-status-1");
const res = await GET();
assert.equal(res.status, 200);
const body = await jsonBody(res);
const b = body as Record<string, unknown>;
assertServiceStatusShape(body, "9router");
// 9router/status extends ServiceStatus with extra fields
assert.ok("installedVersion" in b, "9router status must include installedVersion");
assert.ok("updateAvailable" in b, "9router status must include updateAvailable");
assert.ok("apiKeyMasked" in b, "9router status must include apiKeyMasked");
assert.ok("autoStart" in b, "9router status must include autoStart");
assert.ok("providerExpose" in b, "9router status must include providerExpose");
assert.ok(typeof b.updateAvailable === "boolean", "updateAvailable must be boolean");
});
});
describe("POST /api/services/9router/start", () => {
it("200 response has ServiceStatus shape when supervisor.start() resolves", async () => {
const { getSupervisor, registerSupervisor } =
await import("../../../src/lib/services/registry.ts");
const { ServiceSupervisor } = await import("../../../src/lib/services/ServiceSupervisor.ts");
// Register a mock supervisor that returns running status immediately.
const sup = new ServiceSupervisor({
tool: "9router",
port: 20130,
spawnArgs: () => ({
command: process.execPath,
args: ["-e", "setTimeout(() => {}, 60000)"],
env: process.env,
cwd: process.cwd(),
}),
healthUrl: () => "http://127.0.0.1:20130/api/health",
healthIntervalMs: 500,
stopTimeoutMs: 500,
logsBufferBytes: 1_048_576,
});
mock.method(sup, "start", async () => makeRunningStatus("9router", 20130));
registerSupervisor(sup);
const { POST } =
await import("../../../src/app/api/services/9router/start/route.ts?t=shape-start-1");
const res = await POST();
assert.equal(res.status, 200);
const body = await jsonBody(res);
assertServiceStatusShape(body, "9router");
assert.equal((body as Record<string, unknown>).state, "running");
mock.restoreAll();
});
it("409 response has error shape when service is not_installed", async () => {
await upsertVersionManagerTool({ tool: "9router", status: "not_installed" });
const { POST } =
await import("../../../src/app/api/services/9router/start/route.ts?t=shape-start-2");
const res = await POST();
assert.equal(res.status, 409);
const body = await jsonBody(res);
assertErrorShape(body);
const b = body as Record<string, unknown>;
const err = b.error as Record<string, unknown>;
assert.equal(err.type, "conflict");
// Restore for other tests
await upsertVersionManagerTool({ tool: "9router", status: "stopped" });
});
it("503 response has error shape when supervisor throws", async () => {
const { registerSupervisor } = await import("../../../src/lib/services/registry.ts");
const { ServiceSupervisor } = await import("../../../src/lib/services/ServiceSupervisor.ts");
const sup = new ServiceSupervisor({
tool: "9router",
port: 20130,
spawnArgs: () => ({
command: process.execPath,
args: ["-e", ""],
env: process.env,
cwd: process.cwd(),
}),
healthUrl: () => "http://127.0.0.1:20130/api/health",
healthIntervalMs: 500,
stopTimeoutMs: 500,
logsBufferBytes: 1_048_576,
});
mock.method(sup, "start", async () => {
throw new Error("ENOENT: spawn failed");
});
registerSupervisor(sup);
const { POST } =
await import("../../../src/app/api/services/9router/start/route.ts?t=shape-start-3");
const res = await POST();
assert.equal(res.status, 503);
const body = await jsonBody(res);
assertErrorShape(body);
const b = body as Record<string, unknown>;
const err = b.error as Record<string, unknown>;
assert.equal(err.type, "server_error");
mock.restoreAll();
});
});
describe("POST /api/services/9router/stop", () => {
it("200 response has { tool, state } shape when supervisor is absent", async () => {
// No supervisor registered — stop falls back gracefully.
const { POST } =
await import("../../../src/app/api/services/9router/stop/route.ts?t=shape-stop-1");
const res = await POST();
assert.equal(res.status, 200);
const body = await jsonBody(res);
const b = body as Record<string, unknown>;
assert.ok(typeof b.tool === "string", "stop response must have tool");
assert.equal(b.tool, "9router");
assert.ok(typeof b.state === "string", "stop response must have state");
assert.ok(
["stopped", "running", "error"].includes(b.state as string),
`unexpected state: ${b.state}`
);
});
it("200 response is ServiceStatus shape when supervisor.stop() resolves", async () => {
const { registerSupervisor } = await import("../../../src/lib/services/registry.ts");
const { ServiceSupervisor } = await import("../../../src/lib/services/ServiceSupervisor.ts");
const sup = new ServiceSupervisor({
tool: "9router",
port: 20130,
spawnArgs: () => ({
command: process.execPath,
args: ["-e", "setTimeout(() => {}, 60000)"],
env: process.env,
cwd: process.cwd(),
}),
healthUrl: () => "http://127.0.0.1:20130/api/health",
healthIntervalMs: 500,
stopTimeoutMs: 500,
logsBufferBytes: 1_048_576,
});
mock.method(sup, "stop", async () => makeStoppedStatus("9router", 20130));
registerSupervisor(sup);
const { POST } =
await import("../../../src/app/api/services/9router/stop/route.ts?t=shape-stop-2");
const res = await POST();
assert.equal(res.status, 200);
const body = await jsonBody(res);
assertServiceStatusShape(body, "9router");
assert.equal((body as Record<string, unknown>).state, "stopped");
mock.restoreAll();
});
});
describe("POST /api/services/9router/install", () => {
it("400 response has error shape for invalid JSON", async () => {
const { POST } =
await import("../../../src/app/api/services/9router/install/route.ts?t=shape-install-1");
const req = new Request("http://localhost/api/services/9router/install", {
method: "POST",
body: "not-json",
headers: { "Content-Type": "application/json" },
});
const res = await POST(req);
assert.equal(res.status, 400);
const body = await jsonBody(res);
assertErrorShape(body);
});
it("200 response has { ok: true, installedVersion, installPath, durationMs } when install succeeds (mock)", async () => {
// We test the shape by verifying what install() returns when mocked.
// A successful InstallResult is: { installedVersion, installPath, durationMs }
// The route wraps it as: { ok: true, ...result }
const expectedShape = {
ok: true,
installedVersion: "0.4.59",
installPath: "/home/user/.omniroute/bin",
durationMs: 1200,
};
// Shape assertion (structural, not calling install which spawns npm):
assert.strictEqual(expectedShape.ok, true);
assert.ok(typeof expectedShape.installedVersion === "string");
assert.ok(typeof expectedShape.installPath === "string");
assert.ok(typeof expectedShape.durationMs === "number");
});
});
});
// ---------------------------------------------------------------------------
// cliproxy routes
// ---------------------------------------------------------------------------
describe("cliproxy — response shapes", () => {
describe("GET /api/services/cliproxy/status", () => {
it("200 response has ServiceStatus shape plus installedVersion and updateAvailable", async () => {
const { GET } =
await import("../../../src/app/api/services/cliproxy/status/route.ts?t=shape-cliproxy-status-1");
const res = await GET();
assert.equal(res.status, 200);
const body = await jsonBody(res);
const b = body as Record<string, unknown>;
assertServiceStatusShape(body, "cliproxy");
assert.ok("installedVersion" in b, "cliproxy status must include installedVersion");
assert.ok("updateAvailable" in b, "cliproxy status must include updateAvailable");
assert.ok("autoStart" in b, "cliproxy status must include autoStart");
assert.ok(typeof b.updateAvailable === "boolean", "updateAvailable must be boolean");
});
});
describe("POST /api/services/cliproxy/start", () => {
it("200 response has ServiceStatus shape when supervisor.start() resolves", async () => {
const { registerSupervisor } = await import("../../../src/lib/services/registry.ts");
const { ServiceSupervisor } = await import("../../../src/lib/services/ServiceSupervisor.ts");
const sup = new ServiceSupervisor({
tool: "cliproxy",
port: 8317,
spawnArgs: () => ({
command: process.execPath,
args: ["-e", "setTimeout(() => {}, 60000)"],
env: process.env,
cwd: process.cwd(),
}),
healthUrl: () => "http://127.0.0.1:8317/v1/models",
healthIntervalMs: 500,
stopTimeoutMs: 500,
logsBufferBytes: 1_048_576,
});
mock.method(sup, "start", async () => makeRunningStatus("cliproxy", 8317));
registerSupervisor(sup);
const { POST } =
await import("../../../src/app/api/services/cliproxy/start/route.ts?t=shape-cliproxy-start-1");
const res = await POST();
assert.equal(res.status, 200);
const body = await jsonBody(res);
assertServiceStatusShape(body, "cliproxy");
assert.equal((body as Record<string, unknown>).state, "running");
mock.restoreAll();
});
it("409 response has error shape when service is not_installed", async () => {
await upsertVersionManagerTool({ tool: "cliproxy", status: "not_installed" });
const { POST } =
await import("../../../src/app/api/services/cliproxy/start/route.ts?t=shape-cliproxy-start-2");
const res = await POST();
assert.equal(res.status, 409);
const body = await jsonBody(res);
assertErrorShape(body);
const err = (body as Record<string, unknown>).error as Record<string, unknown>;
assert.equal(err.type, "conflict");
// Restore
await upsertVersionManagerTool({ tool: "cliproxy", status: "stopped" });
});
});
describe("POST /api/services/cliproxy/stop", () => {
it("200 response has { tool, state } shape when supervisor is absent", async () => {
const { POST } =
await import("../../../src/app/api/services/cliproxy/stop/route.ts?t=shape-cliproxy-stop-1");
const res = await POST();
assert.equal(res.status, 200);
const body = await jsonBody(res);
const b = body as Record<string, unknown>;
assert.ok(typeof b.tool === "string", "cliproxy stop response must have tool");
assert.equal(b.tool, "cliproxy");
assert.ok(typeof b.state === "string", "cliproxy stop response must have state");
});
it("200 response is ServiceStatus shape when supervisor.stop() resolves", async () => {
const { registerSupervisor } = await import("../../../src/lib/services/registry.ts");
const { ServiceSupervisor } = await import("../../../src/lib/services/ServiceSupervisor.ts");
const sup = new ServiceSupervisor({
tool: "cliproxy",
port: 8317,
spawnArgs: () => ({
command: process.execPath,
args: ["-e", "setTimeout(() => {}, 60000)"],
env: process.env,
cwd: process.cwd(),
}),
healthUrl: () => "http://127.0.0.1:8317/v1/models",
healthIntervalMs: 500,
stopTimeoutMs: 500,
logsBufferBytes: 1_048_576,
});
mock.method(sup, "stop", async () => makeStoppedStatus("cliproxy", 8317));
registerSupervisor(sup);
const { POST } =
await import("../../../src/app/api/services/cliproxy/stop/route.ts?t=shape-cliproxy-stop-2");
const res = await POST();
assert.equal(res.status, 200);
const body = await jsonBody(res);
assertServiceStatusShape(body, "cliproxy");
assert.equal((body as Record<string, unknown>).state, "stopped");
mock.restoreAll();
});
});
describe("POST /api/services/cliproxy/install", () => {
it("400 response has error shape for invalid JSON", async () => {
const { POST } =
await import("../../../src/app/api/services/cliproxy/install/route.ts?t=shape-cliproxy-install-1");
const req = new Request("http://localhost/api/services/cliproxy/install", {
method: "POST",
body: "not-json",
headers: { "Content-Type": "application/json" },
});
const res = await POST(req);
assert.equal(res.status, 400);
const body = await jsonBody(res);
assertErrorShape(body);
});
it("200 response shape is { ok: true, installedVersion, installPath, durationMs } (structural)", () => {
// Structural assertion — avoids spawning npm install in tests.
const expectedShape = {
ok: true,
installedVersion: "6.1.2",
installPath: "/home/user/.omniroute/bin",
durationMs: 800,
};
assert.strictEqual(expectedShape.ok, true);
assert.ok(typeof expectedShape.installedVersion === "string");
assert.ok(typeof expectedShape.installPath === "string");
assert.ok(typeof expectedShape.durationMs === "number");
});
});
});
// ---------------------------------------------------------------------------
// Cross-service shape consistency
// ---------------------------------------------------------------------------
describe("Cross-service shape consistency", () => {
it("both services produce identical top-level keys in 200 status response", async () => {
const { GET: getRouter } =
await import("../../../src/app/api/services/9router/status/route.ts?t=shape-cross-1");
const { GET: getCliproxy } =
await import("../../../src/app/api/services/cliproxy/status/route.ts?t=shape-cross-2");
const [r1, r2] = await Promise.all([getRouter(), getCliproxy()]);
assert.equal(r1.status, 200);
assert.equal(r2.status, 200);
const b1 = (await r1.json()) as Record<string, unknown>;
const b2 = (await r2.json()) as Record<string, unknown>;
// Both must have these shared keys
const sharedKeys = [
"tool",
"state",
"pid",
"port",
"health",
"startedAt",
"lastError",
"installedVersion",
"updateAvailable",
"autoStart",
];
for (const key of sharedKeys) {
assert.ok(key in b1, `9router status missing key: ${key}`);
assert.ok(key in b2, `cliproxy status missing key: ${key}`);
}
});
it("stop routes for both services return { tool, state } at minimum", async () => {
const { POST: stop9r } =
await import("../../../src/app/api/services/9router/stop/route.ts?t=shape-cross-stop-1");
const { POST: stopCp } =
await import("../../../src/app/api/services/cliproxy/stop/route.ts?t=shape-cross-stop-2");
const [r1, r2] = await Promise.all([stop9r(), stopCp()]);
assert.equal(r1.status, 200);
assert.equal(r2.status, 200);
const b1 = (await r1.json()) as Record<string, unknown>;
const b2 = (await r2.json()) as Record<string, unknown>;
assert.ok("tool" in b1, "9router stop must have tool");
assert.ok("state" in b1, "9router stop must have state");
assert.ok("tool" in b2, "cliproxy stop must have tool");
assert.ok("state" in b2, "cliproxy stop must have state");
assert.equal(b1.tool, "9router");
assert.equal(b2.tool, "cliproxy");
});
it("error responses never contain stack trace (hard rule #12)", async () => {
// Trigger a 400 from install with invalid JSON on both services.
const { POST: install9r } =
await import("../../../src/app/api/services/9router/install/route.ts?t=shape-cross-err-1");
const { POST: installCp } =
await import("../../../src/app/api/services/cliproxy/install/route.ts?t=shape-cross-err-2");
const badReq = () =>
new Request("http://localhost/install", {
method: "POST",
body: "bad json!!",
headers: { "Content-Type": "application/json" },
});
const [r1, r2] = await Promise.all([install9r(badReq()), installCp(badReq())]);
for (const res of [r1, r2]) {
const text = await res.text();
assert.ok(!text.includes("at /"), `Stack trace leaked in response: ${text.slice(0, 200)}`);
}
});
});
// ---------------------------------------------------------------------------
// Cleanup
// ---------------------------------------------------------------------------
after(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
@@ -0,0 +1,323 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
getModelRpd,
getModelRpm,
incrementRequestCount,
getDailyRequestCount,
getMinuteRequestCount,
isRpdExhausted,
isRpmExhausted,
resetCounters,
} from "../../../open-sse/services/geminiRateLimitTracker.ts";
test.beforeEach(() => {
resetCounters();
});
// ── getModelRpd ──────────────────────────────────────────────────────────────
test("getModelRpd returns known RPD for exact model match", () => {
assert.equal(getModelRpd("gemini-2.5-flash"), 20);
});
test("getModelRpd returns known RPD for model with gemini/ prefix", () => {
assert.equal(getModelRpd("gemini/gemini-2.5-flash"), 20);
});
test("getModelRpd returns 0 for model with zero RPD (Gemini 2.5 Pro)", () => {
assert.equal(getModelRpd("gemini-2.5-pro"), 0);
});
test("getModelRpd returns 0 for unknown model", () => {
assert.equal(getModelRpd("gemini/fake-model-not-in-list"), 0);
});
test("getModelRpd returns 0 for empty string", () => {
assert.equal(getModelRpd(""), 0);
});
test("getModelRpd matches gemma-4-31b-it via suffix fallback", () => {
// The JSON has "gemma-4-31b-it", and callers may pass "gemma-4-31b-it"
assert.equal(getModelRpd("gemma-4-31b-it"), 1500);
});
test("getModelRpd strips gemma- prefix correctly for gemma models", () => {
// stripModelPrefix("gemini/gemma-4-31b-it") → "gemma-4-31b-it"
assert.equal(getModelRpd("gemini/gemma-4-31b-it"), 1500);
});
test("getModelRpd handles image-generation models (no RPM value, -1)", () => {
// RPD is 25 for imagen models; RPM is -1 in the JSON
assert.equal(getModelRpd("imagen-4-generate"), 25);
});
test("getModelRpd handles models with unlimited RPD (-1)", () => {
// gemini-3.5-live-translate has rpd: -1
assert.equal(getModelRpd("gemini-3.5-live-translate"), 0);
});
test("getModelRpd returns 0 for null input", () => {
assert.equal(getModelRpd(null as unknown as string), 0);
});
test("getModelRpd returns 0 for undefined input", () => {
assert.equal(getModelRpd(undefined as unknown as string), 0);
});
// ── incrementRequestCount / getDailyRequestCount ─────────────────────────────
test("incrementRequestCount starts at 1 for first request", () => {
incrementRequestCount("gemini-2.5-flash");
assert.equal(getDailyRequestCount("gemini-2.5-flash"), 1);
});
test("incrementRequestCount increments sequentially", () => {
incrementRequestCount("gemini-2.5-flash");
incrementRequestCount("gemini-2.5-flash");
incrementRequestCount("gemini-2.5-flash");
assert.equal(getDailyRequestCount("gemini-2.5-flash"), 3);
});
test("incrementRequestCount treats gemini/ prefix and bare name as same model", () => {
incrementRequestCount("gemini-2.5-flash");
incrementRequestCount("gemini/gemini-2.5-flash");
assert.equal(getDailyRequestCount("gemini-2.5-flash"), 2);
assert.equal(getDailyRequestCount("gemini/gemini-2.5-flash"), 2);
});
test("models have independent counters", () => {
incrementRequestCount("gemini-2.5-flash");
incrementRequestCount("gemini-2.5-flash");
incrementRequestCount("gemma-4-31b-it");
assert.equal(getDailyRequestCount("gemini-2.5-flash"), 2);
assert.equal(getDailyRequestCount("gemma-4-31b-it"), 1);
});
test("getDailyRequestCount returns 0 for model with no requests", () => {
assert.equal(getDailyRequestCount("gemini-2.5-flash"), 0);
});
test("incrementRequestCount does nothing for empty model ID", () => {
incrementRequestCount("");
assert.equal(getDailyRequestCount("gemini-2.5-flash"), 0);
});
test("resetCounters clears all state between tests", () => {
incrementRequestCount("gemini-2.5-flash");
incrementRequestCount("gemma-4-31b-it");
resetCounters();
assert.equal(getDailyRequestCount("gemini-2.5-flash"), 0);
assert.equal(getDailyRequestCount("gemma-4-31b-it"), 0);
});
// ── isRpdExhausted ───────────────────────────────────────────────────────────
test("isRpdExhausted returns false when count is below RPD limit", () => {
// gemini-2.5-flash has RPD=20
for (let i = 0; i < 19; i++) incrementRequestCount("gemini-2.5-flash");
assert.equal(isRpdExhausted("gemini-2.5-flash"), false);
});
test("isRpdExhausted returns true when count equals RPD limit", () => {
// gemini-2.5-flash has RPD=20
for (let i = 0; i < 20; i++) incrementRequestCount("gemini-2.5-flash");
assert.equal(isRpdExhausted("gemini-2.5-flash"), true);
});
test("isRpdExhausted returns true when count exceeds RPD limit", () => {
for (let i = 0; i < 25; i++) incrementRequestCount("gemini-2.5-flash");
assert.equal(isRpdExhausted("gemini-2.5-flash"), true);
});
test("isRpdExhausted returns false for model with RPD=0", () => {
// gemini-2.5-pro has rpd=0
incrementRequestCount("gemini-2.5-pro");
assert.equal(isRpdExhausted("gemini-2.5-pro"), false);
});
test("isRpdExhausted returns false for unknown model", () => {
incrementRequestCount("gemini/unknown-model");
assert.equal(isRpdExhausted("gemini/unknown-model"), false);
});
test("isRpdExhausted returns false for model with unlimited RPD (-1, no data)", () => {
// gemini-3.5-live-translate has rpd: -1
incrementRequestCount("gemini-3.5-live-translate");
assert.equal(isRpdExhausted("gemini-3.5-live-translate"), false);
});
test("isRpdExhausted works with gemini/ prefix for the model", () => {
for (let i = 0; i < 20; i++) incrementRequestCount("gemini/gemini-2.5-flash");
assert.equal(isRpdExhausted("gemini/gemini-2.5-flash"), true);
});
// ── Integration: Gemma 4 RPM scenario ────────────────────────────────────────
test("Gemma 4: 15 RPM hits never trigger quota_exhausted (RPD=1500)", () => {
// Gemma 4 has RPD=1500, so 15 RPM hits should not trigger RPD exhaustion
for (let i = 0; i < 15; i++) incrementRequestCount("gemini/gemma-4-31b-it");
assert.equal(isRpdExhausted("gemini/gemma-4-31b-it"), false);
assert.equal(getDailyRequestCount("gemini/gemma-4-31b-it"), 15);
});
test("Gemma 4: RPD exhaustion requires 1500 requests", () => {
for (let i = 0; i < 1499; i++) incrementRequestCount("gemini/gemma-4-31b-it");
assert.equal(isRpdExhausted("gemini/gemma-4-31b-it"), false);
incrementRequestCount("gemini/gemma-4-31b-it");
assert.equal(isRpdExhausted("gemini/gemma-4-31b-it"), true);
});
// ── Integration: Gemini 2.5 Flash RPD scenario ───────────────────────────────
test("Gemini 2.5 Flash: first 19 requests do NOT exhaust RPD, 20th does", () => {
for (let i = 0; i < 19; i++) incrementRequestCount("gemini-2.5-flash");
assert.equal(isRpdExhausted("gemini-2.5-flash"), false, "19 requests < 20 RPD");
incrementRequestCount("gemini-2.5-flash");
assert.equal(isRpdExhausted("gemini-2.5-flash"), true, "20 requests = 20 RPD");
});
test("Gemini 2.5 Flash: excess requests stay exhausted", () => {
for (let i = 0; i < 22; i++) incrementRequestCount("gemini-2.5-flash");
assert.equal(isRpdExhausted("gemini-2.5-flash"), true);
assert.equal(getDailyRequestCount("gemini-2.5-flash"), 22);
});
// ── getModelRpm ───────────────────────────────────────────────────────────────
test("getModelRpm returns known RPM for exact model match", () => {
assert.equal(getModelRpm("gemini-2.5-flash"), 5);
});
test("getModelRpm returns known RPM for model with gemini/ prefix", () => {
assert.equal(getModelRpm("gemini/gemini-2.5-flash"), 5);
});
test("getModelRpm returns 0 for model with zero RPM", () => {
assert.equal(getModelRpm("gemini-2.5-pro"), 0);
});
test("getModelRpm returns 0 for unknown model", () => {
assert.equal(getModelRpm("gemini/fake-model-not-in-list"), 0);
});
test("getModelRpm returns 0 for empty string", () => {
assert.equal(getModelRpm(""), 0);
});
test("getModelRpm returns 0 for models with RPM=-1 (imagen)", () => {
assert.equal(getModelRpm("imagen-4-generate"), 0);
});
test("getModelRpm returns 0 for null input", () => {
assert.equal(getModelRpm(null as unknown as string), 0);
});
// ── incrementMinuteRequestCount / getMinuteRequestCount ───────────────────────
test("getMinuteRequestCount returns 0 before first request", () => {
assert.equal(getMinuteRequestCount("gemini-2.5-flash"), 0);
});
test("incrementMinuteRequestCount starts at 1", () => {
incrementRequestCount("gemini-2.5-flash");
assert.equal(getMinuteRequestCount("gemini-2.5-flash"), 1);
});
test("incrementMinuteRequestCount increments with each call", () => {
incrementRequestCount("gemini-2.5-flash");
incrementRequestCount("gemini-2.5-flash");
incrementRequestCount("gemini-2.5-flash");
assert.equal(getMinuteRequestCount("gemini-2.5-flash"), 3);
});
test("incrementMinuteRequestCount normalizes gemini/ prefix", () => {
incrementRequestCount("gemini-2.5-flash");
incrementRequestCount("gemini/gemini-2.5-flash");
assert.equal(getMinuteRequestCount("gemini-2.5-flash"), 2);
});
test("minute counters are independent per model", () => {
incrementRequestCount("gemini-2.5-flash");
incrementRequestCount("gemini-2.5-flash");
incrementRequestCount("gemma-4-31b-it");
assert.equal(getMinuteRequestCount("gemini-2.5-flash"), 2);
assert.equal(getMinuteRequestCount("gemma-4-31b-it"), 1);
});
test("incrementRequestCount does nothing for empty model ID", () => {
incrementRequestCount("");
assert.equal(getMinuteRequestCount("gemini-2.5-flash"), 0);
});
test("resetCounters clears minute windows", () => {
incrementRequestCount("gemini-2.5-flash");
incrementRequestCount("gemma-4-31b-it");
resetCounters();
assert.equal(getMinuteRequestCount("gemini-2.5-flash"), 0);
assert.equal(getMinuteRequestCount("gemma-4-31b-it"), 0);
});
// ── isRpmExhausted ────────────────────────────────────────────────────────────
test("isRpmExhausted returns false below RPM limit", () => {
// gemini-2.5-flash has RPM=5
for (let i = 0; i < 4; i++) incrementRequestCount("gemini-2.5-flash");
assert.equal(isRpmExhausted("gemini-2.5-flash"), false);
});
test("isRpmExhausted returns true at RPM limit", () => {
for (let i = 0; i < 5; i++) incrementRequestCount("gemini-2.5-flash");
assert.equal(isRpmExhausted("gemini-2.5-flash"), true);
});
test("isRpmExhausted returns true above RPM limit", () => {
for (let i = 0; i < 8; i++) incrementRequestCount("gemini-2.5-flash");
assert.equal(isRpmExhausted("gemini-2.5-flash"), true);
});
test("isRpmExhausted returns false for model with RPM=0", () => {
incrementRequestCount("gemini-2.5-pro");
assert.equal(isRpmExhausted("gemini-2.5-pro"), false);
});
test("isRpmExhausted returns false for unknown model", () => {
incrementRequestCount("gemini/unknown-model");
assert.equal(isRpmExhausted("gemini/unknown-model"), false);
});
test("isRpmExhausted returns false for model with RPM=-1 (imagen)", () => {
incrementRequestCount("imagen-4-generate");
assert.equal(isRpmExhausted("imagen-4-generate"), false);
});
test("isRpmExhausted works with gemini/ prefix", () => {
for (let i = 0; i < 5; i++) incrementRequestCount("gemini/gemini-2.5-flash");
assert.equal(isRpmExhausted("gemini/gemini-2.5-flash"), true);
});
// ── Integration: RPM + RPD work independently ─────────────────────────────────
test("RPM and RPD limits are tracked independently", () => {
// Gemma 4: RPM=15, RPD=1500
// After 15 requests, RPM is exhausted but RPD is not
for (let i = 0; i < 15; i++) incrementRequestCount("gemini/gemma-4-31b-it");
assert.equal(isRpmExhausted("gemini/gemma-4-31b-it"), true);
assert.equal(isRpdExhausted("gemini/gemma-4-31b-it"), false);
});
test("Gemini 2.5 Flash: RPM=5 always hits before RPD=20", () => {
// After 5 requests, RPM is exhausted; after 20, RPD is exhausted
for (let i = 0; i < 5; i++) incrementRequestCount("gemini-2.5-flash");
assert.equal(isRpmExhausted("gemini-2.5-flash"), true);
assert.equal(isRpdExhausted("gemini-2.5-flash"), false);
incrementRequestCount("gemini-2.5-flash"); // 6
incrementRequestCount("gemini-2.5-flash"); // 7
incrementRequestCount("gemini-2.5-flash"); // 8
// Still at RPD=8 which is < 20
assert.equal(isRpdExhausted("gemini-2.5-flash"), false);
});
+241
View File
@@ -0,0 +1,241 @@
/**
* Unit tests for src/lib/services/htmlRewriter.ts
*
* Tests HTML rewriting for the embedded-service reverse proxy:
* - <base href> injection
* - Path-absolute URL rewriting in <a>, <link>, <script>, <img>, <form>, <iframe>, <source>
* - Non-rewrite cases: external URLs, protocol-relative, mailto:, javascript:
* - Edge cases: HTML without <head>, empty href, multi-URL srcset
*/
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { rewriteHtml } from "../../../src/lib/services/htmlRewriter.ts";
const PREFIX = "/dashboard/providers/services/9router/embed";
// Helper: assert string contains substring
function assertContains(haystack: string, needle: string, msg?: string): void {
assert.ok(
haystack.includes(needle),
msg ?? `Expected output to contain: ${JSON.stringify(needle)}\nActual: ${haystack}`
);
}
function assertNotContains(haystack: string, needle: string, msg?: string): void {
assert.ok(
!haystack.includes(needle),
msg ?? `Expected output NOT to contain: ${JSON.stringify(needle)}\nActual: ${haystack}`
);
}
// ─── base tag injection ───────────────────────────────────────────────────────
describe("rewriteHtml — <base> injection", () => {
it("inserts <base href> as first child of <head>", () => {
const input = "<html><head></head><body></body></html>";
const out = rewriteHtml(input, PREFIX);
assertContains(out, `<base href="${PREFIX}/">`);
});
it("inserts <base href> before other head children", () => {
const input = "<html><head><title>T</title></head><body></body></html>";
const out = rewriteHtml(input, PREFIX);
const baseIdx = out.indexOf("<base ");
const titleIdx = out.indexOf("<title>");
assert.ok(
baseIdx < titleIdx,
`<base> should appear before <title>; base=${baseIdx}, title=${titleIdx}`
);
});
it("strips trailing slash from prefix before forming base href", () => {
const out = rewriteHtml("<html><head></head><body></body></html>", `${PREFIX}/`);
assertContains(out, `<base href="${PREFIX}/">`);
// Should not have double slash
assertNotContains(out, `<base href="${PREFIX}//">`);
});
it("handles HTML without <head> by creating one", () => {
const out = rewriteHtml("<html><body><p>hello</p></body></html>", PREFIX);
assertContains(out, `<base href="${PREFIX}/">`);
});
it("updates existing <base> href instead of inserting a second one", () => {
const input = '<html><head><base href="/old/"></head><body></body></html>';
const out = rewriteHtml(input, PREFIX);
assertContains(out, `<base href="${PREFIX}/">`);
assertNotContains(out, 'href="/old/"');
// Exactly one <base tag
const matches = out.match(/<base /g);
assert.equal(matches?.length ?? 0, 1, "should have exactly one <base> element");
});
});
// ─── link rewriting ───────────────────────────────────────────────────────────
describe("rewriteHtml — <a href> rewriting", () => {
it("rewrites path-absolute <a href> to prefixed URL", () => {
const input = '<html><head></head><body><a href="/foo">link</a></body></html>';
const out = rewriteHtml(input, PREFIX);
assertContains(out, `href="${PREFIX}/foo"`);
});
it("does NOT rewrite https:// external URL", () => {
const input = '<html><head></head><body><a href="https://example.com/foo">x</a></body></html>';
const out = rewriteHtml(input, PREFIX);
assertContains(out, 'href="https://example.com/foo"');
});
it("does NOT rewrite http:// external URL", () => {
const input = '<html><head></head><body><a href="http://external.com">x</a></body></html>';
const out = rewriteHtml(input, PREFIX);
assertContains(out, 'href="http://external.com"');
});
it("does NOT rewrite protocol-relative URL (//cdn)", () => {
const input = '<html><head></head><body><a href="//cdn.example.com/x">x</a></body></html>';
const out = rewriteHtml(input, PREFIX);
assertContains(out, 'href="//cdn.example.com/x"');
});
it("does NOT rewrite mailto: URL", () => {
const input =
'<html><head></head><body><a href="mailto:admin@example.com">email</a></body></html>';
const out = rewriteHtml(input, PREFIX);
assertContains(out, 'href="mailto:admin@example.com"');
});
it("does NOT rewrite javascript: URL", () => {
const input = '<html><head></head><body><a href="javascript:void(0)">x</a></body></html>';
const out = rewriteHtml(input, PREFIX);
assertContains(out, 'href="javascript:void(0)"');
});
it("does NOT rewrite relative URL (no leading slash)", () => {
const input = '<html><head></head><body><a href="relative/path">x</a></body></html>';
const out = rewriteHtml(input, PREFIX);
assertContains(out, 'href="relative/path"');
});
it("does NOT rewrite anchor-only href", () => {
const input = '<html><head></head><body><a href="#section">x</a></body></html>';
const out = rewriteHtml(input, PREFIX);
assertContains(out, 'href="#section"');
});
});
// ─── other element rewriting ──────────────────────────────────────────────────
describe("rewriteHtml — other element attributes", () => {
it("rewrites <link href='/stylesheet.css'>", () => {
const input =
'<html><head><link rel="stylesheet" href="/styles.css"></head><body></body></html>';
const out = rewriteHtml(input, PREFIX);
assertContains(out, `href="${PREFIX}/styles.css"`);
});
it("rewrites <script src='/app.js'>", () => {
const input = '<html><head></head><body><script src="/app.js"></script></body></html>';
const out = rewriteHtml(input, PREFIX);
assertContains(out, `src="${PREFIX}/app.js"`);
});
it("rewrites <img src='/logo.png'>", () => {
const input = '<html><head></head><body><img src="/logo.png"></body></html>';
const out = rewriteHtml(input, PREFIX);
assertContains(out, `src="${PREFIX}/logo.png"`);
});
it("rewrites <form action='/submit'>", () => {
const input =
'<html><head></head><body><form action="/submit"><button>Go</button></form></body></html>';
const out = rewriteHtml(input, PREFIX);
assertContains(out, `action="${PREFIX}/submit"`);
});
it("rewrites <iframe src='/frame'>", () => {
const input = '<html><head></head><body><iframe src="/frame"></iframe></body></html>';
const out = rewriteHtml(input, PREFIX);
assertContains(out, `src="${PREFIX}/frame"`);
});
it("rewrites <source src='/video.mp4'>", () => {
const input = '<html><head></head><body><source src="/video.mp4"></body></html>';
const out = rewriteHtml(input, PREFIX);
assertContains(out, `src="${PREFIX}/video.mp4"`);
});
it("does NOT rewrite img srcset with commas (multi-URL — v1 limitation)", () => {
const input =
'<html><head></head><body><img srcset="/img.png 1x, /img@2x.png 2x"></body></html>';
const out = rewriteHtml(input, PREFIX);
// Original srcset must be preserved unchanged
assertContains(out, 'srcset="/img.png 1x, /img@2x.png 2x"');
});
});
// ─── non-anchor markup preservation ──────────────────────────────────────────
describe("rewriteHtml — preserves non-affected markup", () => {
it("preserves text content inside elements", () => {
const input = "<html><head></head><body><p>Hello World</p></body></html>";
const out = rewriteHtml(input, PREFIX);
assertContains(out, "Hello World");
});
it("preserves existing non-rewritten attributes", () => {
const input =
'<html><head></head><body><a href="/foo" class="nav" id="link1">x</a></body></html>';
const out = rewriteHtml(input, PREFIX);
assertContains(out, 'class="nav"');
assertContains(out, 'id="link1"');
});
it("preserves data attributes unchanged", () => {
const input =
'<html><head></head><body><div data-url="/foo" data-val="bar"></div></body></html>';
const out = rewriteHtml(input, PREFIX);
// data-url is not in REWRITABLE_ATTRS — stays as-is
assertContains(out, 'data-url="/foo"');
});
it("preserves empty href unchanged", () => {
const input = '<html><head></head><body><a href="">empty</a></body></html>';
const out = rewriteHtml(input, PREFIX);
assertContains(out, 'href=""');
});
});
// ─── edge cases ───────────────────────────────────────────────────────────────
describe("rewriteHtml — edge cases", () => {
it("handles empty HTML string gracefully", () => {
const out = rewriteHtml("", PREFIX);
assertContains(out, `<base href="${PREFIX}/">`);
});
it("handles HTML fragment (no <html> tag) — parse5 auto-completes tree", () => {
const out = rewriteHtml('<a href="/foo">link</a>', PREFIX);
assertContains(out, `<base href="${PREFIX}/">`);
assertContains(out, `href="${PREFIX}/foo"`);
});
it("multiple path-absolute URLs in same document are all rewritten", () => {
const input = [
"<html><head>",
'<link href="/a.css">',
"</head><body>",
'<a href="/page1">1</a>',
'<a href="/page2">2</a>',
'<script src="/app.js"></script>',
"</body></html>",
].join("");
const out = rewriteHtml(input, PREFIX);
assertContains(out, `href="${PREFIX}/a.css"`);
assertContains(out, `href="${PREFIX}/page1"`);
assertContains(out, `href="${PREFIX}/page2"`);
assertContains(out, `src="${PREFIX}/app.js"`);
});
});
@@ -0,0 +1,121 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import {
handleServiceInstall,
readServiceInstallVersion,
} from "../../../src/app/api/services/_shared/installRoute.ts";
import { InstallError } from "../../../src/lib/services/installers/utils.ts";
async function readJson(response: Response) {
return (await response.json()) as Record<string, unknown>;
}
test("readServiceInstallVersion defaults empty requests to latest", async () => {
const result = await readServiceInstallVersion(
new Request("http://localhost/api/services/example/install", { method: "POST" })
);
assert.deepEqual(result, { ok: true, version: "latest" });
});
test("readServiceInstallVersion returns the requested version", async () => {
const result = await readServiceInstallVersion(
new Request("http://localhost/api/services/example/install", {
method: "POST",
body: JSON.stringify({ version: "1.2.3" }),
headers: { "Content-Type": "application/json" },
})
);
assert.deepEqual(result, { ok: true, version: "1.2.3" });
});
test("readServiceInstallVersion rejects malformed versions (#5495 SERVICE_VERSION_PATTERN guard)", async () => {
const result = await readServiceInstallVersion(
new Request("http://localhost/api/services/example/install", {
method: "POST",
body: JSON.stringify({ version: "../../malicious" }),
headers: { "Content-Type": "application/json" },
})
);
assert.equal(result.ok, false);
if (result.ok) throw new Error("expected version validation failure");
assert.equal(result.response.status, 400);
});
test("handleServiceInstall never reaches the installer for a malformed version (#5495)", async () => {
const calls: string[] = [];
const response = await handleServiceInstall(
new Request("http://localhost/api/services/example/install", {
method: "POST",
body: JSON.stringify({ version: "v1; rm -rf /" }),
headers: { "Content-Type": "application/json" },
}),
async (version) => {
calls.push(version);
return { installedVersion: version, installPath: "/tmp/service", durationMs: 1 };
}
);
assert.deepEqual(calls, []);
assert.equal(response.status, 400);
});
test("readServiceInstallVersion preserves invalid JSON error shape", async () => {
const result = await readServiceInstallVersion(
new Request("http://localhost/api/services/example/install", {
method: "POST",
body: "not-json",
headers: { "Content-Type": "application/json" },
})
);
assert.equal(result.ok, false);
if (result.ok) throw new Error("expected parse failure");
assert.equal(result.response.status, 400);
const body = await readJson(result.response);
assert.equal((body.error as Record<string, unknown>).message, "Invalid JSON body");
});
test("handleServiceInstall wraps successful installer results", async () => {
const calls: string[] = [];
const response = await handleServiceInstall(
new Request("http://localhost/api/services/example/install", {
method: "POST",
body: JSON.stringify({ version: "2.0.0" }),
headers: { "Content-Type": "application/json" },
}),
async (version) => {
calls.push(version);
return {
installedVersion: version,
installPath: "/tmp/service",
durationMs: 42,
};
}
);
assert.deepEqual(calls, ["2.0.0"]);
assert.equal(response.status, 200);
assert.deepEqual(await readJson(response), {
ok: true,
installedVersion: "2.0.0",
installPath: "/tmp/service",
durationMs: 42,
});
});
test("handleServiceInstall maps InstallError to its friendly message and status", async () => {
const response = await handleServiceInstall(
new Request("http://localhost/api/services/example/install", { method: "POST" }),
async () => {
throw new InstallError("raw command failed", "Friendly install failure", 503);
}
);
assert.equal(response.status, 503);
const body = await readJson(response);
assert.equal((body.error as Record<string, unknown>).message, "Friendly install failure");
});
@@ -0,0 +1,152 @@
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 { execSync } from "node:child_process";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-bifrost-installer-"));
const FAKE_BIN_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-bifrost-fake-bin-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.NODE_ENV = "test";
process.env.DISABLE_SQLITE_AUTO_BACKUP = "true";
const originalPath = process.env.PATH ?? "";
process.env.PATH = `${FAKE_BIN_DIR}:${originalPath}`;
const INSTALL_DIR = path.join(TEST_DATA_DIR, "services", "bifrost");
const fakeNpmScript = `#!/bin/sh
set -e
CMD="$1"
shift
if [ "$CMD" = "install" ]; then
PREFIX=""
while [ $# -gt 0 ]; do
if [ "$1" = "--prefix" ]; then PREFIX="$2"; shift 2; else shift; fi
done
if [ -z "$PREFIX" ]; then PREFIX="$npm_config_prefix"; fi
PKG_DIR="$PREFIX/node_modules/@maximhq/bifrost"
mkdir -p "$PKG_DIR"
echo '{"name":"@maximhq/bifrost","version":"1.6.3"}' > "$PKG_DIR/package.json"
touch "$PKG_DIR/bin.js"
exit 0
fi
if [ "$CMD" = "view" ]; then
echo "1.6.3"
exit 0
fi
exit 0
`;
const fakeNpmPath = path.join(FAKE_BIN_DIR, "npm");
fs.writeFileSync(fakeNpmPath, fakeNpmScript, { mode: 0o755 });
execSync("which npm", { env: process.env });
// DB bootstrap (must be before bifrost import due to db/core eager init)
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 ('bifrost', 'not_installed', 8080, 0, 1, 1)`
).run();
const {
install,
update,
getInstalledVersion,
getLatestVersion,
resolveSpawnArgs,
BIFROST_DEFAULT_PORT,
BIFROST_INSTALL_DIR,
} = await import("../../../../src/lib/services/installers/bifrost.ts");
test.after(() => {
process.env.PATH = originalPath;
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.rmSync(FAKE_BIN_DIR, { recursive: true, force: true });
});
test("BIFROST_DEFAULT_PORT is 8080", () => {
assert.equal(BIFROST_DEFAULT_PORT, 8080);
});
test("install creates host package.json structure", async () => {
const result = await install("1.6.3");
const hostPkg = path.join(BIFROST_INSTALL_DIR, "package.json");
assert.ok(fs.existsSync(hostPkg), "host package.json should exist");
const parsedHost = JSON.parse(fs.readFileSync(hostPkg, "utf8")) as {
name: string;
private: boolean;
};
assert.equal(parsedHost.name, "omniroute-bifrost-host");
assert.ok(parsedHost.private);
assert.equal(result.installedVersion, "1.6.3");
assert.equal(result.installPath, BIFROST_INSTALL_DIR);
assert.ok(result.durationMs >= 0);
});
test("getInstalledVersion reads from node_modules/@maximhq/bifrost/package.json", async () => {
const ver = await getInstalledVersion();
assert.equal(ver, "1.6.3", "should read version from installed package");
});
test("update calls npm install with latest (idempotent)", async () => {
const result = await update();
assert.equal(result.installedVersion, "1.6.3");
});
test("getLatestVersion returns version string from npm view", async () => {
const ver = await getLatestVersion();
assert.equal(ver, "1.6.3");
});
test("resolveSpawnArgs shape: command is node, bin.js path, Go single-dash flags", () => {
const args = resolveSpawnArgs(8080);
assert.equal(args.command, process.execPath, "command must be current node binary");
assert.ok(args.args[0]?.includes("bin.js"), "args[0] should point to bin.js");
// Go-style single-dash flags
const portIdx = args.args.indexOf("-port");
assert.ok(portIdx !== -1, "must have -port flag");
assert.equal(args.args[portIdx + 1], "8080");
const hostIdx = args.args.indexOf("-host");
assert.ok(hostIdx !== -1, "must have -host flag");
assert.equal(args.args[hostIdx + 1], "127.0.0.1");
const appDirIdx = args.args.indexOf("-app-dir");
assert.ok(appDirIdx !== -1, "must have -app-dir flag");
assert.ok(args.args[appDirIdx + 1]?.includes("bifrost"), "-app-dir must point into bifrost dir");
const logLevelIdx = args.args.indexOf("-log-level");
assert.ok(logLevelIdx !== -1, "must have -log-level flag");
assert.equal(args.args[logLevelIdx + 1], "warn");
// BIFROST_TRANSPORT_VERSION must be set in env
assert.ok(
typeof args.env.BIFROST_TRANSPORT_VERSION === "string" &&
args.env.BIFROST_TRANSPORT_VERSION.length > 0,
"BIFROST_TRANSPORT_VERSION must be set in env"
);
});
test("resolveSpawnArgs with different port passes correct -port value", () => {
const args = resolveSpawnArgs(9090);
const portIdx = args.args.indexOf("-port");
assert.ok(portIdx !== -1);
assert.equal(args.args[portIdx + 1], "9090");
});
test("INSTALL_DIR constant points into DATA_DIR/services/bifrost", () => {
assert.ok(BIFROST_INSTALL_DIR.includes("bifrost"), "install dir must include 'bifrost'");
assert.ok(
BIFROST_INSTALL_DIR.startsWith(TEST_DATA_DIR),
"install dir must be under TEST_DATA_DIR"
);
assert.equal(INSTALL_DIR, BIFROST_INSTALL_DIR);
});
@@ -0,0 +1,108 @@
/**
* T-11 — cliproxy installer unit tests.
*
* All tests are pure-logic: no real file I/O, no network, no DB.
* We test:
* - getInstalledVersion parses directory name from binary path
* - install() wires the version_manager row correctly
* - resolveSpawnArgs builds correct command/args/env
* - path traversal in configPath is impossible (no user input)
*/
import { describe, it, beforeEach, afterEach, mock } from "node:test";
import assert from "node:assert/strict";
import path from "node:path";
import os from "node:os";
// ── helpers ──────────────────────────────────────────────────────────────────
/**
* Build a fake binary path that mimics what binaryManager returns:
* $DATA_DIR/bin/cliproxyapi-{version}/cli-proxy-api
*/
function fakeBinaryPath(version: string, dataDir = "/fake"): string {
return path.join(dataDir, "bin", `cliproxyapi-${version}`, "cli-proxy-api");
}
// ── getInstalledVersion ───────────────────────────────────────────────────────
describe("getInstalledVersion", () => {
it("returns version extracted from parent directory name", () => {
// The function reads the dirname of the binary path and regex-matches it.
// We replicate that logic here to verify it handles various versions.
function extractVersion(binaryPath: string): string | null {
const dirName = path.basename(path.dirname(binaryPath));
const m = dirName.match(/^cliproxyapi-(.+)$/);
return m ? m[1] : null;
}
assert.equal(extractVersion(fakeBinaryPath("1.0.0")), "1.0.0");
assert.equal(extractVersion(fakeBinaryPath("2.3.14")), "2.3.14");
assert.equal(extractVersion(fakeBinaryPath("0.0.1-beta")), "0.0.1-beta");
});
it("returns null when directory does not match expected pattern", () => {
function extractVersion(binaryPath: string): string | null {
const dirName = path.basename(path.dirname(binaryPath));
const m = dirName.match(/^cliproxyapi-(.+)$/);
return m ? m[1] : null;
}
assert.equal(extractVersion("/some/other/dir/cli-proxy-api"), null);
assert.equal(extractVersion("/bin/cli-proxy-api"), null);
});
});
// ── resolveSpawnArgs ──────────────────────────────────────────────────────────
describe("resolveSpawnArgs", () => {
it("uses the correct binary symlink path and config flag", () => {
// Simulate what resolveSpawnArgs does without hitting the real filesystem.
// The key contract: command is at $DATA_DIR/bin/cliproxyapi and args are ["-c", configPath].
const fakeDataDir = "/fake";
const port = 8317;
const binDir = path.join(fakeDataDir, "bin");
const configDir = path.join(fakeDataDir, "services", "cliproxy");
const expectedCommand = path.join(binDir, "cliproxyapi");
const expectedConfigPath = path.join(configDir, "config.yaml");
// Verify path construction logic
assert.equal(expectedCommand, "/fake/bin/cliproxyapi");
assert.equal(expectedConfigPath, "/fake/services/cliproxy/config.yaml");
assert.ok(port > 0 && port < 65536);
});
it("config.yaml content includes the correct port and host", () => {
const port = 9000;
const configContent = `port: ${port}\nhost: 127.0.0.1\nlog_level: warn\n`;
assert.ok(configContent.includes(`port: ${port}`));
assert.ok(configContent.includes("host: 127.0.0.1"));
assert.ok(configContent.includes("log_level: warn"));
});
it("CLIPROXY_DEFAULT_PORT is 8317", async () => {
// Verify the exported constant directly
const { CLIPROXY_DEFAULT_PORT } =
await import("../../../../src/lib/services/installers/cliproxy.ts");
assert.equal(CLIPROXY_DEFAULT_PORT, 8317);
});
});
// ── path safety ───────────────────────────────────────────────────────────────
describe("path safety", () => {
it("config path stays within DATA_DIR — no user-controlled input reaches path.join", () => {
// resolveSpawnArgs(port: number) takes only a numeric port.
// All paths are derived from DATA_DIR + constants, never from user input.
// This test documents that contract.
const port = 8317;
assert.equal(typeof port, "number", "port must always be a number, not a string");
// The config.yaml line uses String(port) which cannot contain path separators
const portStr = String(port);
assert.ok(!portStr.includes("/"), "port string cannot contain path separator");
assert.ok(!portStr.includes(".."), "port string cannot contain traversal");
});
});
+105
View File
@@ -0,0 +1,105 @@
/**
* Mux installer unit tests.
*
* All tests are pure-logic: no real file I/O, no network, no DB.
* resolveSpawnArgs() performs fs.mkdirSync as a side effect (creating
* MUX_ROOT under DATA_DIR), so — mirroring cliproxy.test.ts — we replicate
* its pure argument-building contract here instead of invoking the real
* function, keeping this suite side-effect-free.
*/
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import path from "node:path";
// ── exported constants ────────────────────────────────────────────────────────
describe("mux installer — exports", () => {
it("MUX_DEFAULT_PORT is 8322", async () => {
const { MUX_DEFAULT_PORT } = await import("../../../../src/lib/services/installers/mux.ts");
assert.equal(MUX_DEFAULT_PORT, 8322);
});
it("MUX_PACKAGE is the npm package name 'mux'", async () => {
const { MUX_PACKAGE } = await import("../../../../src/lib/services/installers/mux.ts");
assert.equal(MUX_PACKAGE, "mux");
});
});
// ── getInstalledVersion ───────────────────────────────────────────────────────
describe("getInstalledVersion", () => {
it("reads version from node_modules/mux/package.json", () => {
// Replicates the logic in getInstalledVersion(): reads a JSON file at a
// DATA_DIR-scoped, non-user-controlled path and pulls out `.version`.
const fakePkg = JSON.stringify({ name: "mux", version: "0.27.0" });
const parsed = JSON.parse(fakePkg) as { version?: string };
assert.equal(parsed.version, "0.27.0");
});
});
// ── resolveSpawnArgs (pure argument-building contract) ─────────────────────────
describe("resolveSpawnArgs — argument-building contract", () => {
const MUX_INSTALL_DIR = path.join("/fake", "services", "mux");
function buildArgs(apiKey: string, port: number) {
const serverPath = path.join(MUX_INSTALL_DIR, "node_modules", "mux", "dist", "cli", "index.js");
return {
command: "node",
args: [serverPath, "server", "--host", "127.0.0.1", "--port", String(port)],
env: { MUX_SERVER_AUTH_TOKEN: apiKey },
cwd: MUX_INSTALL_DIR,
};
}
it("binds host to 127.0.0.1 explicitly — never 0.0.0.0", () => {
const spawnArgs = buildArgs("mx_fake_token", 8322);
const hostIdx = spawnArgs.args.indexOf("--host");
assert.ok(hostIdx !== -1);
assert.equal(spawnArgs.args[hostIdx + 1], "127.0.0.1");
});
it("passes the port via --port flag as a string", () => {
const spawnArgs = buildArgs("mx_fake_token", 9001);
const portIdx = spawnArgs.args.indexOf("--port");
assert.ok(portIdx !== -1);
assert.equal(spawnArgs.args[portIdx + 1], "9001");
});
it("invokes the 'server' subcommand", () => {
const spawnArgs = buildArgs("mx_fake_token", 8322);
assert.ok(spawnArgs.args.includes("server"));
});
it("passes the auth token via MUX_SERVER_AUTH_TOKEN env var, never as an argv entry", () => {
const token = "mx_super_secret_token_value";
const spawnArgs = buildArgs(token, 8322);
assert.equal(spawnArgs.env.MUX_SERVER_AUTH_TOKEN, token);
assert.ok(
!spawnArgs.args.some((a) => a.includes(token)),
"token must never appear in argv (would leak via `ps`)"
);
});
it("targets the installed server entry point under node_modules/mux/dist/cli", () => {
const spawnArgs = buildArgs("mx_fake_token", 8322);
assert.ok(spawnArgs.args[0].endsWith(path.join("dist", "cli", "index.js")));
assert.ok(spawnArgs.args[0].includes(path.join("node_modules", "mux")));
});
});
// ── path safety ───────────────────────────────────────────────────────────────
describe("path safety", () => {
it("resolveSpawnArgs takes only (apiKey: string, port: number) — no arbitrary path input", () => {
// resolveSpawnArgs never accepts a user-controlled path; every filesystem
// path it builds is derived from DATA_DIR + static path segments.
const port = 8322;
assert.equal(typeof port, "number", "port must always be a number, not a string");
const portStr = String(port);
assert.ok(!portStr.includes("/"), "port string cannot contain path separator");
assert.ok(!portStr.includes(".."), "port string cannot contain traversal");
});
});
@@ -0,0 +1,184 @@
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 { execSync } from "node:child_process";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-installer-"));
const FAKE_BIN_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-fake-bin-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.NODE_ENV = "test";
process.env.DISABLE_SQLITE_AUTO_BACKUP = "true";
// Prepend fake bin dir to PATH so our fake `npm` is found by runNpm
const originalPath = process.env.PATH ?? "";
process.env.PATH = `${FAKE_BIN_DIR}:${originalPath}`;
// Fake npm script: for `install` — creates expected package.json;
// for `view` — prints a version; for other args — no-op.
const INSTALL_DIR = path.join(TEST_DATA_DIR, "services", "9router");
const fakeNpmScript = `#!/bin/sh
set -e
CMD="$1"
shift
if [ "$CMD" = "install" ]; then
# Resolve the install prefix like real npm: an explicit --prefix arg wins,
# otherwise fall back to the npm_config_prefix env var (#5379 passes the
# prefix via env instead of argv so paths with spaces survive the win shell).
PREFIX=""
while [ $# -gt 0 ]; do
if [ "$1" = "--prefix" ]; then PREFIX="$2"; shift 2; else shift; fi
done
if [ -z "$PREFIX" ]; then PREFIX="$npm_config_prefix"; fi
PKG_DIR="$PREFIX/node_modules/9router"
mkdir -p "$PKG_DIR/app"
echo '{"name":"9router","version":"0.4.59"}' > "$PKG_DIR/package.json"
touch "$PKG_DIR/app/server.js"
exit 0
fi
if [ "$CMD" = "view" ]; then
echo "0.4.59"
exit 0
fi
exit 0
`;
const fakeNpmPath = path.join(FAKE_BIN_DIR, "npm");
fs.writeFileSync(fakeNpmPath, fakeNpmScript, { mode: 0o755 });
// Verify fake npm is on PATH
execSync("which npm", { env: process.env });
// DB bootstrap (must be before ninerouter import due to db/core eager init)
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', 'not_installed', 20130, 0, 1, 1)`
).run();
const {
install,
update,
uninstall,
getInstalledVersion,
getLatestVersion,
resolveSpawnArgs,
NINEROUTER_INSTALL_DIR,
} = await import("../../../../src/lib/services/installers/ninerouter.ts");
test.after(() => {
process.env.PATH = originalPath;
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.rmSync(FAKE_BIN_DIR, { recursive: true, force: true });
});
test("install creates package.json structure", async () => {
const result = await install("0.4.59");
// Host package.json must exist
const hostPkg = path.join(NINEROUTER_INSTALL_DIR, "package.json");
assert.ok(fs.existsSync(hostPkg), "host package.json should exist");
const parsedHost = JSON.parse(fs.readFileSync(hostPkg, "utf8")) as {
name: string;
private: boolean;
};
assert.equal(parsedHost.name, "omniroute-9router-host");
assert.ok(parsedHost.private);
assert.equal(result.installedVersion, "0.4.59");
assert.equal(result.installPath, NINEROUTER_INSTALL_DIR);
assert.ok(result.durationMs >= 0);
});
test("install captures real version from node_modules/9router/package.json", async () => {
const ver = await getInstalledVersion();
assert.equal(ver, "0.4.59", "should read version from installed package");
});
test("update calls npm install with latest (idempotent)", async () => {
const result = await update();
assert.equal(result.installedVersion, "0.4.59");
});
test("uninstall removes node_modules and marks not_installed in DB", async () => {
const nmDir = path.join(NINEROUTER_INSTALL_DIR, "node_modules");
// Ensure node_modules exists from previous install tests
assert.ok(fs.existsSync(nmDir), "node_modules should exist before uninstall");
await uninstall();
assert.ok(!fs.existsSync(nmDir), "node_modules should be removed");
const { getVersionManagerTool } = await import("../../../../src/lib/db/versionManager.ts");
const row = await getVersionManagerTool("9router");
assert.equal(row?.status, "not_installed");
assert.equal(row?.installedVersion, null);
assert.equal(row?.binaryPath, null);
// package.json host file should remain (preserves metadata)
assert.ok(
fs.existsSync(path.join(NINEROUTER_INSTALL_DIR, "package.json")),
"host package.json should be kept after uninstall"
);
});
test("getLatestVersion returns version string from npm view", async () => {
const ver = await getLatestVersion();
assert.equal(ver, "0.4.59");
});
test("resolveSpawnArgs returns expected env and command", () => {
const args = resolveSpawnArgs("sk-test-api-key", 20130);
assert.equal(args.command, process.execPath, "command must be current node binary");
assert.equal(args.args[0], "--max-old-space-size=6144");
assert.ok(args.args[1]?.includes("server.js"), "args[1] should point to server.js");
assert.equal(args.env.PORT, "20130");
assert.equal(args.env.HOSTNAME, "127.0.0.1");
assert.equal(args.env.API_KEY_SECRET, "sk-test-api-key");
assert.ok(args.env.DATA_DIR?.includes("9router"), "DATA_DIR should be scoped to 9router");
assert.equal(args.env.NODE_ENV, "production");
assert.equal(args.env.DISABLE_MITM, "true");
assert.equal(args.env.DISABLE_TUNNEL, "true");
});
test("EACCES error returns friendly InstallError", async () => {
const { InstallError } = await import("../../../../src/lib/services/installers/utils.ts");
// Simulate EACCES by making the install dir read-only
const lockedDir = path.join(TEST_DATA_DIR, "locked-services", "9router");
fs.mkdirSync(lockedDir, { recursive: true });
fs.chmodSync(path.join(TEST_DATA_DIR, "locked-services"), 0o444);
process.env.DATA_DIR = path.join(TEST_DATA_DIR, "locked-services");
try {
await import("../../../../src/lib/services/installers/ninerouter.ts?locked=1");
} catch {
// Re-import after DATA_DIR change fails due to caching — that's fine
}
process.env.DATA_DIR = TEST_DATA_DIR;
// Restore for cleanup
fs.chmodSync(path.join(TEST_DATA_DIR, "locked-services"), 0o755);
// At minimum, InstallError should carry httpStatus and friendly fields
const err = new InstallError("raw error", "friendly message", 403);
assert.equal(err.httpStatus, 403);
assert.equal(err.friendly, "friendly message");
assert.ok(err instanceof Error);
assert.equal(err.name, "InstallError");
});
test("InstallError timeout shape has correct httpStatus 504", async () => {
const { InstallError: IE } = await import("../../../../src/lib/services/installers/utils.ts");
const err = new IE("npm process killed", "Instalação demorou demais. Tente novamente.", 504);
assert.equal(err.httpStatus, 504);
assert.equal(err.friendly, "Instalação demorou demais. Tente novamente.");
assert.ok(err instanceof Error);
assert.equal(err.name, "InstallError");
});
@@ -0,0 +1,74 @@
/**
* Regression tests for #5379 — `spawn EINVAL` installing embedded services
* (9Router / CLIProxy) on Windows + Node.js 24+.
*
* Node 24 no longer lets `child_process.execFile()` run `.cmd` batch files on
* Windows without a shell (nodejs/node#52554). npm on Windows is `npm.cmd`, so
* `runNpm()` threw `EINVAL` immediately. The fix flips `shell` on win32.
*
* Because `shell: true` makes the shell — not execFile — parse the command line,
* NO runtime value may be interpolated into argv (Hard Rule #13). The install
* `--prefix` (a DATA_DIR path that can contain spaces, e.g.
* `C:\Users\John Doe\.omniroute\…`) is therefore passed via the
* `npm_config_prefix` environment variable instead of an argv entry, and the
* user-supplied install `version` is constrained by SERVICE_VERSION_PATTERN.
*/
import test from "node:test";
import assert from "node:assert/strict";
import {
buildNpmExecOptions,
SERVICE_VERSION_PATTERN,
} from "../../../../src/lib/services/installers/utils.ts";
test("buildNpmExecOptions: win32 enables shell so npm.cmd runs on Node 24 (#5379)", () => {
const opts = buildNpmExecOptions("win32", { timeoutMs: 1000 });
assert.equal(opts.shell, true);
});
test("buildNpmExecOptions: non-win32 platforms never enable shell", () => {
for (const platform of ["linux", "darwin", "freebsd"] as NodeJS.Platform[]) {
const opts = buildNpmExecOptions(platform, { timeoutMs: 1000 });
assert.equal(opts.shell, undefined, `${platform} must not use a shell`);
}
});
test("buildNpmExecOptions: prefix is passed via npm_config_prefix env, never argv (Hard Rule #13)", () => {
const prefix = "C:\\Users\\John Doe\\.omniroute\\services\\9router";
const opts = buildNpmExecOptions("win32", { timeoutMs: 1000, prefix });
assert.equal(opts.env.npm_config_prefix, prefix);
});
test("buildNpmExecOptions: without a prefix npm_config_prefix is left untouched", () => {
const inherited = process.env.npm_config_prefix;
const opts = buildNpmExecOptions("linux", { timeoutMs: 1000 });
assert.equal(opts.env.npm_config_prefix, inherited);
});
test("buildNpmExecOptions: carries cwd, timeout and maxBuffer through", () => {
const opts = buildNpmExecOptions("linux", { cwd: "/tmp/install", timeoutMs: 4242 });
assert.equal(opts.cwd, "/tmp/install");
assert.equal(opts.timeout, 4242);
assert.equal(opts.maxBuffer, 10 * 1024 * 1024);
});
test("SERVICE_VERSION_PATTERN: accepts dist-tags and semver", () => {
for (const v of ["latest", "next", "1.2.3", "1.2.3-beta.1", "1.2.3+build.5", "0.4.59"]) {
assert.ok(SERVICE_VERSION_PATTERN.test(v), `${v} should be valid`);
}
});
test("SERVICE_VERSION_PATTERN: rejects shell metacharacters (injection guard)", () => {
for (const v of [
"latest && calc",
"1.2.3; rm -rf /",
"$(whoami)",
"`id`",
"a|b",
"a b",
"",
"-flag",
]) {
assert.equal(SERVICE_VERSION_PATTERN.test(v), false, `${JSON.stringify(v)} must be rejected`);
}
});
+193
View File
@@ -0,0 +1,193 @@
/**
* T-04 lifecycle endpoint tests.
*
* Tests HTTP handlers directly (bypassing Next.js routing) with a real
* ServiceSupervisor whose start/stop methods are patched via mock.fn().
*/
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 { mock } from "node:test";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-lifecycle-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.NODE_ENV = "test";
process.env.DISABLE_SQLITE_AUTO_BACKUP = "true";
// Bootstrap DB
const core = await import("../../../src/lib/db/core.ts");
const db = core.getDbInstance();
// Seed service rows
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, getVersionManagerTool } =
await import("../../../src/lib/db/versionManager.ts");
// Set version so installer functions have something to read
await updateVersionManagerTool("9router", { installedVersion: "0.4.59", status: "stopped" });
const { ServiceSupervisor } = await import("../../../src/lib/services/ServiceSupervisor.ts");
const { registerSupervisor, getSupervisor } = await import("../../../src/lib/services/registry.ts");
/** Creates a minimal ServiceSupervisor with fake spawnArgs (no real process). */
function makeFakeSup(tool: string) {
return new ServiceSupervisor({
tool,
port: 20130,
spawnArgs: () => ({
command: process.execPath,
args: ["-e", "setTimeout(() => {}, 60000)"],
env: process.env,
cwd: process.cwd(),
}),
healthUrl: () => `http://127.0.0.1:20130/api/health`,
healthIntervalMs: 500,
stopTimeoutMs: 500,
logsBufferBytes: 1_048_576,
});
}
test.after(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
// ─── status endpoint ────────────────────────────────────────────────────────
test("GET /status returns not_installed when no installer version", async () => {
await updateVersionManagerTool("9router", { installedVersion: null, status: "not_installed" });
const { GET } = await import("../../../src/app/api/services/9router/status/route.ts?t=status-1");
const resp = await GET();
assert.equal(resp.status, 200);
const body = await resp.json();
assert.ok(["not_installed", "unknown", "stopped"].includes(body.state as string));
// restore
await updateVersionManagerTool("9router", { installedVersion: "0.4.59", status: "stopped" });
});
test("GET /status returns enriched shape", async () => {
const { GET } = await import("../../../src/app/api/services/9router/status/route.ts?t=status-2");
const resp = await GET();
assert.equal(resp.status, 200);
const body = await resp.json();
assert.ok("state" in body, "should have state");
assert.ok("installedVersion" in body, "should have installedVersion");
assert.ok("updateAvailable" in body, "should have updateAvailable");
assert.ok("apiKeyMasked" in body, "should have apiKeyMasked");
});
// ─── start endpoint ─────────────────────────────────────────────────────────
test("POST /start returns 409 when not_installed", async () => {
await updateVersionManagerTool("9router", { status: "not_installed", installedVersion: null });
const { POST } = await import("../../../src/app/api/services/9router/start/route.ts?t=start-1");
const resp = await POST();
assert.equal(resp.status, 409);
const body = await resp.json();
assert.ok(body.error?.message?.includes("instalado"), "should explain not installed");
// restore
await updateVersionManagerTool("9router", { status: "stopped", installedVersion: "0.4.59" });
});
test("POST /start returns 200 when already running", async () => {
const sup = makeFakeSup("9router");
// Patch start() to return immediately with running state
const origStart = sup.start.bind(sup);
void origStart;
mock.method(sup, "start", async () => ({
tool: "9router",
state: "running" as const,
pid: 12345,
port: 20130,
health: "healthy" as const,
startedAt: new Date().toISOString(),
lastError: null,
}));
registerSupervisor(sup);
await updateVersionManagerTool("9router", { status: "stopped", installedVersion: "0.4.59" });
const { POST } = await import("../../../src/app/api/services/9router/start/route.ts?t=start-2");
const resp = await POST();
assert.equal(resp.status, 200);
const body = await resp.json();
assert.equal(body.state, "running");
mock.restoreAll();
});
// ─── stop endpoint ───────────────────────────────────────────────────────────
test("POST /stop returns stopped even if supervisor absent", async () => {
// Remove from registry by re-importing fresh
const { POST } = await import("../../../src/app/api/services/9router/stop/route.ts?t=stop-1");
const resp = await POST();
assert.equal(resp.status, 200);
const body = await resp.json();
assert.ok(["stopped", "running", "error"].includes(body.state as string));
});
// ─── auto-start endpoint ─────────────────────────────────────────────────────
test("POST /auto-start toggles DB field", async () => {
const { POST } = await import("../../../src/app/api/services/9router/auto-start/route.ts");
const reqEnable = new Request("http://localhost", {
method: "POST",
body: JSON.stringify({ enabled: true }),
});
const resp1 = await POST(reqEnable);
assert.equal(resp1.status, 204);
const row1 = await getVersionManagerTool("9router");
assert.ok(row1?.autoStart === true, "autoStart should be true after enable");
const reqDisable = new Request("http://localhost", {
method: "POST",
body: JSON.stringify({ enabled: false }),
});
const resp2 = await POST(reqDisable);
assert.equal(resp2.status, 204);
const row2 = await getVersionManagerTool("9router");
assert.ok(row2?.autoStart === false, "autoStart should be false after disable");
});
test("POST /auto-start returns 400 for invalid body", async () => {
const { POST } =
await import("../../../src/app/api/services/9router/auto-start/route.ts?t=as-bad");
const req = new Request("http://localhost", {
method: "POST",
body: JSON.stringify({ enabled: "yes" }), // should be boolean
});
const resp = await POST(req);
assert.equal(resp.status, 400);
});
// ─── rotate-key endpoint ─────────────────────────────────────────────────────
test("POST /rotate-key does not leak plain key in response", async () => {
const { POST } = await import("../../../src/app/api/services/9router/rotate-key/route.ts");
const resp = await POST();
assert.equal(resp.status, 200);
const body = await resp.json();
assert.ok(body.keyRotated === true, "keyRotated should be true");
assert.ok(!("key" in body), "response must not contain 'key' field");
assert.ok(!("apiKey" in body), "response must not contain 'apiKey' field");
assert.ok(!("plainKey" in body), "response must not contain 'plainKey' field");
// Ensure no string in the response looks like our key prefix
const bodyStr = JSON.stringify(body);
assert.ok(!bodyStr.includes("nr_"), "response must not contain plain key value");
});
+265
View File
@@ -0,0 +1,265 @@
/**
* T-09 — SSE logs endpoint tests.
*
* Tests the GET /api/services/[name]/logs handler directly.
* Uses a real RingBuffer pushed with synthetic log lines.
*/
import test from "node:test";
import assert from "node:assert/strict";
import { RingBuffer } from "../../../src/lib/services/ringBuffer.ts";
import type { LogLine } from "../../../src/lib/services/types.ts";
// ─── helpers ────────────────────────────────────────────────────────────────
function makeLogLine(line: string, stream: "stdout" | "stderr" = "stdout"): LogLine {
return { ts: Date.now(), stream, line };
}
/**
* Minimal request factory for the SSE route handler.
* Returns a Request whose signal can be aborted via the returned AbortController.
*/
function makeRequest(searchParams: Record<string, string> = {}): {
req: Request;
abort: AbortController;
} {
const abort = new AbortController();
const qs = new URLSearchParams(searchParams).toString();
const url = `http://localhost/api/services/9router/logs${qs ? `?${qs}` : ""}`;
const req = new Request(url, { signal: abort.signal });
return { req, abort };
}
/**
* Reads all SSE events from a Response body until the stream ends or
* the caller aborts. Returns parsed {event, data} pairs.
*/
async function drainEvents(
resp: Response,
opts: { maxEvents?: number; abort?: AbortController } = {}
): Promise<Array<{ event: string; data: unknown }>> {
const { maxEvents = 50, abort } = opts;
const events: Array<{ event: string; data: unknown }> = [];
const reader = resp.body!.getReader();
const decoder = new TextDecoder();
let buf = "";
let currentEvent = "message";
while (events.length < maxEvents) {
const { value, done } = await reader.read();
if (done) break;
buf += decoder.decode(value, { stream: true });
const parts = buf.split("\n\n");
buf = parts.pop() ?? "";
for (const block of parts) {
const lines = block.split("\n");
let eventName = currentEvent;
let dataLine = "";
for (const line of lines) {
if (line.startsWith("event: ")) eventName = line.slice(7).trim();
else if (line.startsWith("data: ")) dataLine = line.slice(6);
}
if (dataLine) {
try {
events.push({ event: eventName, data: JSON.parse(dataLine) });
} catch {
events.push({ event: eventName, data: dataLine });
}
}
currentEvent = "message";
}
}
abort?.abort();
reader.cancel().catch(() => {});
return events;
}
// ─── import route handler after helpers ──────────────────────────────────────
// Registry is module-level state; import it so we can register test supervisors.
const { registerSupervisor, getSupervisor } = await import("../../../src/lib/services/registry.ts");
const { ServiceSupervisor } = await import("../../../src/lib/services/ServiceSupervisor.ts");
function makeTestSupervisor(tool: string): ServiceSupervisor {
return new ServiceSupervisor({
tool,
port: 29999,
spawnArgs: () => ({
command: process.execPath,
args: ["-e", "setTimeout(() => {}, 999999)"],
env: process.env,
cwd: process.cwd(),
}),
healthUrl: () => `http://127.0.0.1:29999/health`,
healthIntervalMs: 500,
stopTimeoutMs: 500,
logsBufferBytes: 1_048_576,
});
}
// ─── tests ───────────────────────────────────────────────────────────────────
test("GET /logs returns 404 for unknown service", async () => {
const { GET } = await import("../../../src/app/api/services/[name]/logs/route.ts");
const { req, abort } = makeRequest();
const resp = await GET(req as any, { params: Promise.resolve({ name: "no-such-service" }) });
abort.abort();
assert.equal(resp.status, 404);
const body = await resp.json();
assert.ok(typeof body.error === "object" && body.error !== null, "error must be an object");
assert.ok(body.error.message?.includes("not found"), "error.message must mention 'not found'");
assert.equal(body.error.type, "not_found");
assert.ok(typeof body.requestId === "string", "requestId must be present");
});
test("GET /logs returns 400 when filter exceeds max length", async () => {
const sup = makeTestSupervisor("9router-filter-test");
registerSupervisor(sup);
const { GET } = await import("../../../src/app/api/services/[name]/logs/route.ts?t=filter-len");
const longFilter = "a".repeat(201);
const { req, abort } = makeRequest({ filter: longFilter });
const resp = await GET(req as any, {
params: Promise.resolve({ name: "9router-filter-test" }),
});
abort.abort();
assert.equal(resp.status, 400);
const body = await resp.json();
assert.ok(typeof body.error === "object" && body.error !== null, "error must be an object");
assert.ok(body.error.message?.includes("filter"), "error.message must mention 'filter'");
assert.equal(body.error.type, "invalid_request");
assert.ok(typeof body.requestId === "string", "requestId must be present");
});
test("GET /logs sends snapshot event with buffered lines", async () => {
const sup = makeTestSupervisor("9router-snap-test");
registerSupervisor(sup);
const rb = sup.getRingBuffer();
rb.push(makeLogLine("line one"));
rb.push(makeLogLine("line two"));
rb.push(makeLogLine("line three"));
const { GET } = await import("../../../src/app/api/services/[name]/logs/route.ts?t=snap");
const { req, abort } = makeRequest();
const resp = await GET(req as any, {
params: Promise.resolve({ name: "9router-snap-test" }),
});
assert.equal(resp.headers.get("Content-Type"), "text/event-stream");
assert.equal(resp.headers.get("X-Accel-Buffering"), "no");
const events = await drainEvents(resp, { maxEvents: 1, abort });
assert.equal(events.length, 1);
assert.equal(events[0].event, "snapshot");
const lines = events[0].data as LogLine[];
assert.equal(lines.length, 3);
assert.equal(lines[0].line, "line one");
});
test("GET /logs applies substring filter to snapshot", async () => {
const sup = makeTestSupervisor("9router-filt-snap");
registerSupervisor(sup);
const rb = sup.getRingBuffer();
rb.push(makeLogLine("[ERROR] something broke"));
rb.push(makeLogLine("[INFO] all good"));
rb.push(makeLogLine("[ERROR] another error"));
const { GET } = await import("../../../src/app/api/services/[name]/logs/route.ts?t=filt-snap");
const { req, abort } = makeRequest({ filter: "ERROR" });
const resp = await GET(req as any, {
params: Promise.resolve({ name: "9router-filt-snap" }),
});
const events = await drainEvents(resp, { maxEvents: 1, abort });
const lines = events[0].data as LogLine[];
assert.equal(lines.length, 2);
assert.ok(lines.every((l) => l.line.includes("ERROR")));
});
test("GET /logs respects tail parameter", async () => {
const sup = makeTestSupervisor("9router-tail-test");
registerSupervisor(sup);
const rb = sup.getRingBuffer();
for (let i = 0; i < 10; i++) rb.push(makeLogLine(`line ${i}`));
const { GET } = await import("../../../src/app/api/services/[name]/logs/route.ts?t=tail");
const { req, abort } = makeRequest({ tail: "3" });
const resp = await GET(req as any, {
params: Promise.resolve({ name: "9router-tail-test" }),
});
const events = await drainEvents(resp, { maxEvents: 1, abort });
const lines = events[0].data as LogLine[];
assert.equal(lines.length, 3);
assert.equal(lines[0].line, "line 7");
assert.equal(lines[2].line, "line 9");
});
test("GET /logs delivers live log events after snapshot", async () => {
const sup = makeTestSupervisor("9router-live-test");
registerSupervisor(sup);
const rb = sup.getRingBuffer();
rb.push(makeLogLine("existing"));
const { GET } = await import("../../../src/app/api/services/[name]/logs/route.ts?t=live");
const { req, abort } = makeRequest();
const resp = await GET(req as any, {
params: Promise.resolve({ name: "9router-live-test" }),
});
// Schedule a live push slightly after the stream starts consuming
setImmediate(() => rb.push(makeLogLine("live event")));
const events = await drainEvents(resp, { maxEvents: 2, abort });
const liveEvents = events.filter((e) => e.event === "log");
assert.equal(liveEvents.length, 1);
assert.equal((liveEvents[0].data as LogLine).line, "live event");
});
test("GET /logs unsubscribes from buffer on abort", async () => {
const sup = makeTestSupervisor("9router-unsub-test");
registerSupervisor(sup);
const rb = sup.getRingBuffer();
const { GET } = await import("../../../src/app/api/services/[name]/logs/route.ts?t=unsub");
const { req, abort } = makeRequest();
const resp = await GET(req as any, {
params: Promise.resolve({ name: "9router-unsub-test" }),
});
// Read snapshot then abort
await drainEvents(resp, { maxEvents: 1, abort });
// After abort, pushing a new line should not increment any internal counter
// (there are no live subscribers). We verify by checking subscriber count via
// a subscribe-then-immediately-unsubscribe round-trip — if our original
// subscriber is gone, count stays at 0 after unsubscribing.
let liveCallCount = 0;
const unsub = rb.subscribe(() => {
liveCallCount++;
});
rb.push(makeLogLine("post-abort line"));
unsub();
assert.equal(liveCallCount, 1, "only the probe subscriber should fire after abort");
});
+202
View File
@@ -0,0 +1,202 @@
/**
* Tests for src/lib/services/modelSync.ts
*
* Uses an isolated DB (DATA_DIR override) and mocks globalThis.fetch.
*/
import { describe, it, before, after, 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-model-sync-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.NODE_ENV = "test";
process.env.DISABLE_SQLITE_AUTO_BACKUP = "true";
// Imports after env is set up so the DB initialises with test path.
const core = await import("../../../src/lib/db/core.ts");
const { syncServiceModels, scheduleServiceModelSync, stopServiceModelSync, getServiceModels } =
await import("../../../src/lib/services/modelSync.ts");
const originalFetch = globalThis.fetch;
afterEach(() => {
globalThis.fetch = originalFetch;
// Always clean up any scheduled sync to avoid cross-test interference.
stopServiceModelSync("9router");
});
after(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
function makeFetch(
status: number,
body: unknown
): (input: string | URL | Request, init?: RequestInit) => Promise<Response> {
return async () =>
new Response(JSON.stringify(body), {
status,
headers: { "Content-Type": "application/json" },
});
}
describe("syncServiceModels", () => {
it("returns model count and persists models on success", async () => {
const models = [
{ id: "9r/gemma-3n-e4b", object: "model", owned_by: "google" },
{ id: "9r/llama-3.3-70b", object: "model", owned_by: "meta" },
];
globalThis.fetch = makeFetch(200, { data: models }) as typeof fetch;
const count = await syncServiceModels("tool-success", "http://127.0.0.1:20130", "nr_test");
assert.equal(count, 2);
const stored = getServiceModels("tool-success");
// Pruning marks available=true; count includes only current sync (no prior state).
const available = stored.filter((m) => m.available !== false);
assert.equal(available.length, 2);
assert.equal(available[0].id, "tool-success/9r/gemma-3n-e4b");
});
it("saves models with 9router/ prefix in id", async () => {
// Upstream returns raw ids without prefix.
globalThis.fetch = makeFetch(200, {
data: [
{ id: "cx/gpt-5-mini", object: "model" },
{ id: "auto/sonnet", object: "model" },
],
}) as typeof fetch;
await syncServiceModels("9router-prefix-test", "http://127.0.0.1:20130", "nr_test");
const stored = getServiceModels("9router-prefix-test");
const available = stored.filter((m) => m.available !== false);
assert.equal(available.length, 2);
assert.equal(
available[0].id,
"9router-prefix-test/cx/gpt-5-mini",
"model id must be prefixed with tool name"
);
assert.equal(
available[1].id,
"9router-prefix-test/auto/sonnet",
"model id must be prefixed with tool name"
);
});
it("does not double-prefix if upstream already returns prefixed ids", async () => {
globalThis.fetch = makeFetch(200, {
data: [{ id: "9router/cx/gpt-5-mini", object: "model" }],
}) as typeof fetch;
await syncServiceModels("9router", "http://127.0.0.1:20130", "nr_test");
const stored = getServiceModels("9router");
const available = stored.filter((m) => m.available !== false);
assert.equal(available.length, 1);
assert.equal(
available[0].id,
"9router/cx/gpt-5-mini",
"already-prefixed ids must not be double-prefixed"
);
});
it("getServiceModels returns prefixed ids", async () => {
globalThis.fetch = makeFetch(200, {
data: [{ id: "cx/gpt-5-mini" }, { id: "auto/sonnet" }],
}) as typeof fetch;
await syncServiceModels("9router-getmodels-test", "http://127.0.0.1:20130", "nr_test");
const stored = getServiceModels("9router-getmodels-test");
assert.ok(
stored.every((m) => m.id.startsWith("9router-getmodels-test/")),
"all returned model ids must start with tool name prefix"
);
});
it("accepts top-level array response format", async () => {
const models = [{ id: "9r/model-a" }, { id: "9r/model-b" }];
globalThis.fetch = makeFetch(200, models) as typeof fetch;
const count = await syncServiceModels("tool-arr", "http://127.0.0.1:20130", "nr_test");
assert.equal(count, 2);
});
it("filters out entries without an id field", async () => {
globalThis.fetch = makeFetch(200, {
data: [{ id: "valid" }, { name: "no-id" }, null, 42],
}) as typeof fetch;
const count = await syncServiceModels("9router", "http://127.0.0.1:20130", "nr_test");
assert.equal(count, 1);
});
it("returns -1 on non-2xx without throwing", async () => {
globalThis.fetch = makeFetch(503, { error: "unavailable" }) as typeof fetch;
const count = await syncServiceModels("9router", "http://127.0.0.1:20130", "nr_test");
assert.equal(count, -1);
});
it("returns -1 on network error without throwing", async () => {
globalThis.fetch = async () => {
throw new Error("ECONNREFUSED");
};
const count = await syncServiceModels("9router", "http://127.0.0.1:20130", "nr_test");
assert.equal(count, -1);
});
});
describe("scheduleServiceModelSync", () => {
it("is idempotent — calling twice does not throw or duplicate timers", async () => {
// Use a fetch that resolves immediately so the immediate sync completes.
globalThis.fetch = makeFetch(200, { data: [{ id: "m1" }] }) as typeof fetch;
assert.doesNotThrow(() => {
scheduleServiceModelSync("9router", "http://127.0.0.1:20130", "nr_test", 60_000);
scheduleServiceModelSync("9router", "http://127.0.0.1:20130", "nr_test", 60_000);
});
});
it("fires an immediate sync", async () => {
let fetchCalls = 0;
globalThis.fetch = async () => {
fetchCalls++;
return new Response(JSON.stringify({ data: [] }), { status: 200 });
};
scheduleServiceModelSync("9router", "http://127.0.0.1:20130", "nr_test", 60_000);
// Yield to the microtask queue so the immediate async sync can run.
await new Promise((resolve) => setImmediate(resolve));
await new Promise((resolve) => setImmediate(resolve));
assert.ok(fetchCalls >= 1, "Immediate sync should have called fetch at least once");
});
});
describe("stopServiceModelSync", () => {
it("does not throw when called for a tool that was never scheduled", () => {
assert.doesNotThrow(() => stopServiceModelSync("never-scheduled"));
});
it("clears the timer after scheduling", async () => {
globalThis.fetch = makeFetch(200, { data: [] }) as typeof fetch;
scheduleServiceModelSync("9router", "http://127.0.0.1:20130", "nr_test", 60_000);
stopServiceModelSync("9router");
// Calling stop again after clear should be a no-op (no double-clear error).
assert.doesNotThrow(() => stopServiceModelSync("9router"));
});
});
@@ -0,0 +1,204 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
recordProviderCooldown,
isProviderInCooldown,
getRemainingCooldownMs,
recordProviderSuccess,
clearCooldownState,
getCooldownEntryCount,
cleanupExpiredCooldownEntries,
} from "../../../open-sse/services/providerCooldownTracker.ts";
import {
resolveResilienceSettings,
DEFAULT_RESILIENCE_SETTINGS,
} from "../../../src/lib/resilience/settings.ts";
test("global provider cooldown is OFF by default (opt-in)", () => {
// This global cross-request cooldown overlaps the existing Connection Cooldown
// / Provider Circuit Breaker layers, so it must default to disabled. Operators
// opt in via PROVIDER_COOLDOWN_ENABLED=true.
assert.equal(DEFAULT_RESILIENCE_SETTINGS.providerCooldown.enabled, false);
// resolving with no stored settings inherits the default (disabled).
assert.equal(resolveResilienceSettings(null).providerCooldown.enabled, false);
// an explicit stored value still wins.
assert.equal(
resolveResilienceSettings({ resilienceSettings: { providerCooldown: { enabled: true } } })
.providerCooldown.enabled,
true
);
});
function makeSettings(minMs = 5000, maxMs = 300000) {
return resolveResilienceSettings({
resilienceSettings: {
providerCooldown: {
enabled: true,
minRetryCooldownMs: minMs,
maxRetryCooldownMs: maxMs,
},
},
});
}
test.beforeEach(() => {
clearCooldownState();
});
test("recordProviderCooldown creates entry for new provider", () => {
const settings = makeSettings();
recordProviderCooldown("openai", "conn-1", settings);
assert.equal(getCooldownEntryCount(), 1);
assert.ok(isProviderInCooldown("openai", "conn-1", settings));
});
test("recordProviderCooldown increments failure count", () => {
const settings = makeSettings();
recordProviderCooldown("openai", "conn-1", settings);
recordProviderCooldown("openai", "conn-1", settings);
recordProviderCooldown("openai", "conn-1", settings);
assert.equal(getCooldownEntryCount(), 1);
assert.ok(isProviderInCooldown("openai", "conn-1", settings));
});
test("isProviderInCooldown returns false for unknown provider", () => {
const settings = makeSettings();
assert.equal(isProviderInCooldown("unknown", "conn-1", settings), false);
});
test("isProviderInCooldown returns false for empty provider", () => {
const settings = makeSettings();
assert.equal(isProviderInCooldown("", "conn-1", settings), false);
});
test("isProviderInCooldown returns false when not recorded", () => {
const settings = makeSettings();
assert.equal(isProviderInCooldown("openai", "conn-1", settings), false);
});
test("getRemainingCooldownMs returns 0 for unknown provider", () => {
const settings = makeSettings();
assert.equal(getRemainingCooldownMs("unknown", "conn-1", settings), 0);
});
test("getRemainingCooldownMs returns 0 when not recorded", () => {
const settings = makeSettings();
assert.equal(getRemainingCooldownMs("openai", "conn-1", settings), 0);
});
test("recordProviderSuccess resets failure count", () => {
const settings = makeSettings();
recordProviderCooldown("openai", "conn-1", settings);
recordProviderCooldown("openai", "conn-1", settings);
assert.ok(isProviderInCooldown("openai", "conn-1", settings));
recordProviderSuccess("openai", "conn-1");
assert.equal(isProviderInCooldown("openai", "conn-1", settings), false);
});
test("recordProviderSuccess does nothing for unknown provider", () => {
recordProviderSuccess("unknown", "conn-1");
assert.equal(getCooldownEntryCount(), 0);
});
test("clearCooldownState removes all entries", () => {
const settings = makeSettings();
recordProviderCooldown("openai", "conn-1", settings);
recordProviderCooldown("anthropic", "conn-2", settings);
assert.equal(getCooldownEntryCount(), 2);
clearCooldownState();
assert.equal(getCooldownEntryCount(), 0);
});
test("cooldown scales exponentially with failure count", () => {
const settings = makeSettings(5000);
recordProviderCooldown("openai", "conn-1", settings);
const remaining1 = getRemainingCooldownMs("openai", "conn-1", settings);
recordProviderCooldown("openai", "conn-1", settings);
const remaining2 = getRemainingCooldownMs("openai", "conn-1", settings);
recordProviderCooldown("openai", "conn-1", settings);
const remaining3 = getRemainingCooldownMs("openai", "conn-1", settings);
assert.ok(remaining1 > 0, "first failure has cooldown");
assert.ok(remaining2 > remaining1, "second failure has longer cooldown");
assert.ok(remaining3 > remaining2, "third failure has even longer cooldown");
});
test("cooldown respects maxRetryCooldownMs cap", () => {
const settings = makeSettings(5000, 20000);
for (let i = 0; i < 10; i++) {
recordProviderCooldown("openai", "conn-1", settings);
}
const remaining = getRemainingCooldownMs("openai", "conn-1", settings);
assert.ok(remaining <= 20000, "cooldown capped at maxRetryCooldownMs");
});
test("cleanup keeps entries for configured long maxRetryCooldownMs", () => {
const settings = makeSettings(5000, 60 * 60 * 1000);
const originalNow = Date.now;
try {
let fakeNow = Date.now();
Date.now = () => fakeNow;
recordProviderCooldown("openai", "conn-1", settings);
assert.equal(getCooldownEntryCount(), 1);
fakeNow += 31 * 60 * 1000;
cleanupExpiredCooldownEntries();
assert.equal(
getCooldownEntryCount(),
1,
"cleanup must not evict entries before configured maxRetryCooldownMs"
);
fakeNow += 30 * 60 * 1000;
cleanupExpiredCooldownEntries();
assert.equal(getCooldownEntryCount(), 0);
} finally {
Date.now = originalNow;
}
});
test("different connections have independent cooldowns", () => {
const settings = makeSettings();
recordProviderCooldown("openai", "conn-1", settings);
recordProviderCooldown("openai", "conn-2", settings);
assert.ok(isProviderInCooldown("openai", "conn-1", settings));
assert.ok(isProviderInCooldown("openai", "conn-2", settings));
recordProviderSuccess("openai", "conn-1");
assert.equal(isProviderInCooldown("openai", "conn-1", settings), false);
assert.ok(isProviderInCooldown("openai", "conn-2", settings));
});
test("provider-only key works without connectionId", () => {
const settings = makeSettings();
recordProviderCooldown("openai", undefined, settings);
assert.ok(isProviderInCooldown("openai", undefined, settings));
assert.equal(isProviderInCooldown("openai", "conn-1", settings), false);
});
test("cooldown entry count tracks correctly", () => {
assert.equal(getCooldownEntryCount(), 0);
const settings = makeSettings();
recordProviderCooldown("openai", "conn-1", settings);
assert.equal(getCooldownEntryCount(), 1);
recordProviderCooldown("anthropic", "conn-2", settings);
assert.equal(getCooldownEntryCount(), 2);
clearCooldownState();
assert.equal(getCooldownEntryCount(), 0);
});
+92
View File
@@ -0,0 +1,92 @@
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 { RingBuffer } from "../../../src/lib/services/ringBuffer.ts";
import type { LogLine } from "../../../src/lib/services/types.ts";
function makeLine(line: string, stream: "stdout" | "stderr" = "stdout"): LogLine {
return { ts: Date.now(), stream, line };
}
test("push respects byte limit (evicts oldest)", () => {
// 100-byte limit: each entry is ~40 bytes overhead + line bytes
const buf = new RingBuffer(100);
buf.push(makeLine("first")); // ~45 bytes
buf.push(makeLine("second")); // ~46 bytes — total ~91
buf.push(makeLine("third")); // would exceed → evict "first"
const snap = buf.snapshot();
assert.ok(!snap.some((e) => e.line === "first"), "first should be evicted");
assert.ok(
snap.some((e) => e.line === "second"),
"second should be retained"
);
assert.ok(
snap.some((e) => e.line === "third"),
"third should be retained"
);
});
test("snapshot returns isolated copy", () => {
const buf = new RingBuffer();
buf.push(makeLine("a"));
const snap1 = buf.snapshot();
buf.push(makeLine("b"));
const snap2 = buf.snapshot();
assert.equal(snap1.length, 1, "first snapshot unchanged");
assert.equal(snap2.length, 2, "second snapshot includes new entry");
});
test("subscribe receives new lines", () => {
const buf = new RingBuffer();
const received: LogLine[] = [];
const unsub = buf.subscribe((l) => received.push(l));
buf.push(makeLine("hello"));
buf.push(makeLine("world"));
assert.equal(received.length, 2);
assert.equal(received[0].line, "hello");
assert.equal(received[1].line, "world");
unsub();
buf.push(makeLine("after-unsub"));
assert.equal(received.length, 2, "no more events after unsubscribe");
});
test("flush writes to file when path set", async () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-ring-"));
const filePath = path.join(tmpDir, "ring.log");
try {
const buf = new RingBuffer();
buf.setFlushPath(filePath);
buf.push(makeLine("line-one"));
buf.push({ ts: Date.now(), stream: "stderr", line: "err-line" });
// Force flush by calling private method indirectly — dispose triggers cleanup
// We access the private method via prototype cast for testing purposes.
(buf as unknown as { flushToDisk: () => void }).flushToDisk();
assert.ok(fs.existsSync(filePath), "flush file should exist");
const content = fs.readFileSync(filePath, "utf8");
assert.ok(content.includes("line-one"), "flush file should contain log entry");
assert.ok(content.includes("[stderr]"), "flush file should contain stderr entry");
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
test("dispose clears subscribers and stops flush timer", () => {
const buf = new RingBuffer();
const received: LogLine[] = [];
buf.subscribe((l) => received.push(l));
buf.dispose();
buf.push(makeLine("after-dispose"));
assert.equal(received.length, 0, "no events after dispose");
});