Files
OmniRoute/tests/integration/proxy-pipeline.test.ts
Diego Rodrigues de Sa e Souza 3ec9ca11b1 Release v3.7.6 (#1803)
* feat(api-keys): add rename support in permissions modal

Add an editable key name field at the top of the permissions modal,
allowing users to rename API keys alongside existing permission settings.

The backend already supported name updates via PATCH /api/keys/:id — this
wires the UI to send the name field and refreshes the key list on success.

Changes:
- Add keyName state and text input to PermissionsModal
- Update handleUpdatePermissions to validate and send name in PATCH body
- Add integration test for rename via PATCH (valid, empty, too-long names)
- Update E2E mock to handle PATCH requests

* chore(release): bump version to 3.7.6

* chore(release): v3.7.6 — merge API key rename feature and sync docs

* chore(release): expand contributor credits to 155 PRs across full project history

- Expanded acknowledgment table from 29 to 53 contributors
- Added 100+ previously uncredited PRs from project inception through v3.7.5
- Moved contributor credits section to v3.7.6 (current release)
- Synced llm.txt version to 3.7.6

* fix: resolve security ReDoS in codex and bugs #1797 #1789

* feat(dashboard): implement remaining v3.7.6 dashboard features and fixes

* fix(xiaomi-mimo): update models to V2.5, fix Token Plan validation and default region (#1823)

Integrated into release/v3.7.6

* fix(dashboard): correct loadPresets ReferenceError in CostOverviewTab

* fix(codex): omit compact client metadata (#1822)

Integrated into release/v3.7.6

* feat(chatgpt-web): support thinking_effort (Standard/Extended) for thinking-capable models (#1821)

Integrated into release/v3.7.6

* Fix endpoint visibility, A2A status, and API catalog (#1806)

Integrated into release/v3.7.6

* fix(analytics): use pure SQL aggregations — no history rows loaded (#1802)

Integrated into release/v3.7.6

* fix(stability): resolve codex input validation, enable combo circuit breaker, and fix broken unit tests

* docs(changelog): update for stability bug fixes #1804 #1805

* fix: clear active requests and recover providers (#1824)

Integrated into release/v3.7.6

* feat: inject fallback tool names to prevent upstream 400 errors (#1775)

* feat: auto-restore probe-failed database to prevent data loss (#1810)

* fix: safely cast inputs to strings before calling trim() to avoid crashes on numeric fields in proxy modal (#1825)

* chore(release): v3.7.6 — final stability patches for production

* test: update expected db probe-failure error message for auto-restore feature

* chore(workflow): mandate implementation plan generation in resolve-issues

* docs(changelog): rewrite v3.7.6 with complete commit-accurate entries

* feat(analytics): add cost-based usage insights and activity streaks

Expand usage analytics to report total cost, per-series cost totals,
API key counts, and current activity streaks using pricing-aware token
calculations.

Also make probe-failed database recovery choose the newest backup by
its embedded timestamp instead of filesystem mtime so auto-restore
selects the intended snapshot reliably.

* fix(mitm): enforce transparent interception on port 443 only

Reject non-443 MITM port updates in the settings API and normalize
stored configuration back to the required transparent interception
port.

Lock the dashboard port field to 443, update the validation copy, and
add integration coverage to prevent stale custom ports from being
accepted or surfaced.

* docs(changelog): update for analytics and mitm features

---------

Co-authored-by: Andrew Munsell <andrew@wizardapps.net>
Co-authored-by: Antigravity Assistant <bot@antigravity.local>
Co-authored-by: Gi99lin <74502520+Gi99lin@users.noreply.github.com>
Co-authored-by: Sergey Morozov <tr0st@bk.ru>
Co-authored-by: payne <baboialex95@gmail.com>
Co-authored-by: Randi <55005611+rdself@users.noreply.github.com>
Co-authored-by: Paijo <14921983+oyi77@users.noreply.github.com>
Co-authored-by: ipanghu <bypanghu@163.com>
2026-04-30 14:08:50 -03:00

391 lines
15 KiB
TypeScript

/**
* Proxy Pipeline Integration Tests — T-3
*
* Tests the proxy pipeline wiring: format detection, credential retry loop,
* circuit breaker integration, and the new Phase 2 modules (DI container,
* prompt versioning, plugin architecture, eval cleanup).
*
* @module tests/integration/proxy-pipeline.test.ts
*/
import { describe, it, beforeEach, afterEach } from "node:test";
import assert from "node:assert/strict";
import { readFileSync, existsSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = join(__dirname, "..", "..");
function readSrc(relPath) {
const full = join(ROOT, "src", relPath);
if (!existsSync(full)) return null;
return readFileSync(full, "utf8");
}
function readOpenSse(relPath) {
const full = join(ROOT, "open-sse", relPath);
if (!existsSync(full)) return null;
return readFileSync(full, "utf8");
}
// ═══════════════════════════════════════════════════
// 1. Chat Handler Pipeline Wiring
// ═══════════════════════════════════════════════════
describe("Chat Pipeline — handleSingleModelChat decomposition", () => {
const src = readSrc("sse/handlers/chat.ts");
const helpersSrc = readSrc("sse/handlers/chatHelpers.ts");
const coreSrc = readOpenSse("handlers/chatCore.ts");
it("should define resolveModelOrError helper", () => {
assert.ok(helpersSrc, "chatHelpers.ts should exist");
assert.match(helpersSrc, /function\s+resolveModelOrError/);
});
it("should define checkPipelineGates helper", () => {
assert.match(helpersSrc, /function\s+checkPipelineGates/);
});
it("should define executeChatWithBreaker helper", () => {
assert.match(helpersSrc, /function\s+executeChatWithBreaker/);
});
it("should keep cost accounting in the core chat pipeline", () => {
assert.ok(coreSrc, "open-sse/handlers/chatCore.ts should exist");
assert.match(coreSrc, /calculateCost\(/);
assert.match(coreSrc, /recordCost\(/);
});
it("handleSingleModelChat should use resolveModelOrError", () => {
// Extract handleSingleModelChat body
assert.match(src, /resolveModelOrError\(modelStr/);
});
it("handleSingleModelChat should use checkPipelineGates", () => {
assert.match(src, /checkPipelineGates\(provider/);
});
it("handleSingleModelChat should use executeChatWithBreaker", () => {
assert.match(src, /executeChatWithBreaker\(/);
});
it("chatCore should record cost for both non-streaming and streaming responses", () => {
assert.match(coreSrc, /if \(apiKeyInfo\?\.id && estimatedCost > 0\)/);
assert.match(coreSrc, /if \(apiKeyInfo\?\.id && streamUsage\)/);
});
});
describe("Chat Pipeline — combo fallback support", () => {
const src = readSrc("sse/handlers/chat.ts");
it("should import handleComboChat", () => {
assert.ok(src, "chat.ts should exist");
assert.match(src, /handleComboChat/);
});
it("should delegate to handleSingleModelChat for each combo model", () => {
assert.match(src, /handleSingleModel.*handleSingleModelChat/s);
});
it("should preflight provider credentials before attempting combo models", () => {
assert.match(src, /getProviderCredentialsWithQuotaPreflight/);
});
});
describe("Chat Pipeline — circuit breaker integration", () => {
const helpersSrc = readSrc("sse/handlers/chatHelpers.ts");
it("should import providerCircuitOpenResponse", () => {
assert.ok(helpersSrc, "chatHelpers.ts should exist");
assert.match(helpersSrc, /providerCircuitOpenResponse/);
});
it("should handle circuit-open responses with retry-after", () => {
assert.match(helpersSrc, /retryAfterMs/);
});
it("should reject requests when circuit is open via structured provider breaker response", () => {
assert.match(helpersSrc, /providerCircuitOpenResponse\(provider,\s*retryAfterSec\)/);
});
});
// ═══════════════════════════════════════════════════
// 2. DI Container (A-5)
// ═══════════════════════════════════════════════════
describe("DI Container — container.ts", () => {
let container;
beforeEach(async () => {
const mod = await import("../../src/lib/container.ts");
container = mod.container;
});
afterEach(() => {
// Don't reset — keep default registrations
});
it("should export a container singleton", () => {
assert.ok(container);
assert.equal(typeof container.register, "function");
assert.equal(typeof container.resolve, "function");
assert.equal(typeof container.has, "function");
});
it("should register and resolve a custom service", () => {
container.register("testService", () => ({ greeting: "hello" }));
const svc = container.resolve("testService");
assert.deepEqual(svc, { greeting: "hello" });
});
it("should return cached singleton on repeated resolve", () => {
let count = 0;
container.register("counterService", () => ({ value: ++count }));
const a = container.resolve("counterService");
const b = container.resolve("counterService");
assert.strictEqual(a, b);
assert.equal(a.value, 1);
});
it("should throw on resolving unregistered service", () => {
assert.throws(() => container.resolve("nonExistent"), /No factory registered/);
});
it("should have default registrations", () => {
const names = container.list();
assert.ok(names.includes("settings"), "should have settings");
assert.ok(names.includes("db"), "should have db");
assert.ok(names.includes("encryption"), "should have encryption");
assert.ok(names.includes("policyEngine"), "should have policyEngine");
assert.ok(names.includes("circuitBreaker"), "should have circuitBreaker");
assert.ok(names.includes("telemetry"), "should have telemetry");
});
it("should support re-registration (overwrite)", () => {
container.register("testOverwrite", () => "v1");
assert.equal(container.resolve("testOverwrite"), "v1");
container.register("testOverwrite", () => "v2");
assert.equal(container.resolve("testOverwrite"), "v2");
});
});
// ═══════════════════════════════════════════════════
// 3. Plugin Architecture (L-8)
// ═══════════════════════════════════════════════════
describe("Plugin Architecture — plugins/index.ts", () => {
let plugins;
beforeEach(async () => {
plugins = await import("../../src/lib/plugins/index.ts");
plugins.resetPlugins();
});
afterEach(() => {
plugins.resetPlugins();
});
it("should register and list plugins", () => {
plugins.registerPlugin({
name: "test-logger",
priority: 10,
onRequest: () => {},
});
const list = plugins.listPlugins();
assert.equal(list.length, 1);
assert.equal(list[0].name, "test-logger");
assert.equal(list[0].priority, 10);
assert.deepEqual(list[0].hooks, ["onRequest"]);
});
it("should sort plugins by priority", () => {
plugins.registerPlugin({ name: "low", priority: 200 });
plugins.registerPlugin({ name: "high", priority: 1 });
plugins.registerPlugin({ name: "mid", priority: 50 });
const list = plugins.listPlugins();
assert.deepEqual(
list.map((p) => p.name),
["high", "mid", "low"]
);
});
it("should run onRequest hooks in order", async () => {
const order = [];
plugins.registerPlugin({
name: "first",
priority: 1,
onRequest: () => {
order.push("first");
},
});
plugins.registerPlugin({
name: "second",
priority: 2,
onRequest: () => {
order.push("second");
},
});
const ctx = { requestId: "r1", body: {}, model: "test", metadata: {} };
await plugins.runOnRequest(ctx);
assert.deepEqual(order, ["first", "second"]);
});
it("should support request blocking", async () => {
plugins.registerPlugin({
name: "blocker",
priority: 1,
onRequest: () => ({ blocked: true, response: { error: "denied" } }),
});
plugins.registerPlugin({
name: "never-runs",
priority: 2,
onRequest: () => {
throw new Error("should not run");
},
});
const ctx = { requestId: "r2", body: {}, model: "test", metadata: {} };
const result = await plugins.runOnRequest(ctx);
assert.equal(result.blocked, true);
assert.deepEqual(result.response, { error: "denied" });
});
it("should enable/disable plugins at runtime", () => {
plugins.registerPlugin({
name: "toggle-me",
onRequest: () => {},
});
assert.ok(plugins.setPluginEnabled("toggle-me", false));
const list = plugins.listPlugins();
assert.equal(list[0].enabled, false);
});
it("should unregister plugins", () => {
plugins.registerPlugin({ name: "removable" });
assert.equal(plugins.listPlugins().length, 1);
assert.ok(plugins.unregisterPlugin("removable"));
assert.equal(plugins.listPlugins().length, 0);
});
it("should run onResponse hooks", async () => {
plugins.registerPlugin({
name: "response-modifier",
onResponse: (_ctx, response) => ({ ...response, modified: true }),
});
const ctx = { requestId: "r3", body: {}, model: "test", metadata: {} };
const result = await plugins.runOnResponse(ctx, { data: "original" });
assert.equal(result.modified, true);
assert.equal(result.data, "original");
});
it("should run onError hooks and allow recovery", async () => {
plugins.registerPlugin({
name: "error-handler",
onError: (_ctx, _error) => ({ recovered: true }),
});
const ctx = { requestId: "r4", body: {}, model: "test", metadata: {} };
const result = await plugins.runOnError(ctx, new Error("test error"));
assert.deepEqual(result, { recovered: true });
});
it("should return null from onError if no recovery", async () => {
const ctx = { requestId: "r5", body: {}, model: "test", metadata: {} };
const result = await plugins.runOnError(ctx, new Error("unhandled"));
assert.equal(result, null);
});
});
// ═══════════════════════════════════════════════════
// 4. Prompt Template Versioning (L-6)
// ═══════════════════════════════════════════════════
describe("Prompt Template Versioning — prompts.ts module existence", () => {
it("prompts.ts should exist", () => {
const full = join(ROOT, "src", "lib", "db", "prompts.ts");
assert.ok(existsSync(full), "prompts.ts should exist");
});
it("should export CRUD functions", () => {
const src = readFileSync(join(ROOT, "src", "lib", "db", "prompts.ts"), "utf8");
assert.match(src, /export function savePrompt/);
assert.match(src, /export function getActivePrompt/);
assert.match(src, /export function getPromptVersion/);
assert.match(src, /export function listPromptVersions/);
assert.match(src, /export function listPrompts/);
assert.match(src, /export function rollbackPrompt/);
assert.match(src, /export function renderPrompt/);
});
it("should define PromptTemplate interface", () => {
const src = readFileSync(join(ROOT, "src", "lib", "db", "prompts.ts"), "utf8");
assert.match(src, /export interface PromptTemplate/);
});
it("should use content hashing for deduplication", () => {
const src = readFileSync(join(ROOT, "src", "lib", "db", "prompts.ts"), "utf8");
assert.match(src, /content_hash/);
assert.match(src, /sha256/);
});
});
// ═══════════════════════════════════════════════════
// 5. Eval cleanup (Task 28)
// ═══════════════════════════════════════════════════
describe("Eval cleanup — orphaned scheduler module", () => {
it("scheduler.ts should remain deleted", () => {
const full = join(ROOT, "src", "lib", "evals", "scheduler.ts");
assert.equal(existsSync(full), false, "scheduler.ts should stay removed");
});
});
// ═══════════════════════════════════════════════════
// 6. Migration Runner (E-5)
// ═══════════════════════════════════════════════════
describe("Migration System — files exist", () => {
it("migrationRunner.ts should exist", () => {
const full = join(ROOT, "src", "lib", "db", "migrationRunner.ts");
assert.ok(existsSync(full), "migrationRunner.ts should exist");
});
it("001_initial_schema.sql should exist", () => {
const full = join(ROOT, "src", "lib", "db", "migrations", "001_initial_schema.sql");
assert.ok(existsSync(full), "001_initial_schema.sql should exist");
});
it("core.ts should reference migration runner", () => {
const src = readSrc("lib/db/core.ts");
assert.ok(src);
assert.match(src, /runMigrations/);
assert.match(src, /_omniroute_migrations/);
});
});
// ═══════════════════════════════════════════════════
// 7. CORS Configuration (L-5)
// ═══════════════════════════════════════════════════
describe("CORS — centralized configuration", () => {
it("shared/utils/cors.ts should exist", () => {
const full = join(ROOT, "src", "shared", "utils", "cors.ts");
assert.ok(existsSync(full), "shared/utils/cors.ts should exist");
});
it("should export CORS_HEADERS without a wildcard origin", () => {
const src = readSrc("shared/utils/cors.ts");
assert.match(src, /CORS_HEADERS/);
// Extract the CORS_HEADERS object body (between { and }) to avoid matching JSDoc comments
const objMatch = src.match(/CORS_HEADERS\s*=\s*\{([^}]+)\}/);
assert.ok(objMatch, "CORS_HEADERS object should be found");
assert.doesNotMatch(objMatch[1], /Access-Control-Allow-Origin/);
});
});