mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-03 22:02:08 +03:00
* test: resolve typescript strictness complaints in unit tests * Update Claude Code obfuscation to version 2.1.114 (#1403) * fix(cloud-code): scope thinking stripping to executor boundaries (#1401) * fix(cloud-code): scope thinking stripping to executors * fix(cloud-code): guard antigravity normalized body * Update Claude Code obfuscation to version 2.1.114 - Update Claude Code version from 2.1.87 to 2.1.114 - Update X-Stainless-Package-Version from 0.80.0 to 0.81.0 - Add new beta flags: redact-thinking-2026-02-12, advisor-tool-2026-03-01, advanced-tool-use-2025-11-20 - Add missing headers: anthropic-version, anthropic-dangerous-direct-browser-access, x-app, X-Stainless-Timeout - Add all X-Stainless-* headers (Arch, Lang, OS, Runtime, Runtime-Version, Retry-Count) - Fix accept-encoding header: identity -> gzip, deflate, br, zstd - Add connection: keep-alive header - Update tool name mapping: add lsp, apply_patch, websearch These changes ensure that requests from OpenCode through Omniroute are indistinguishable from genuine Claude Code 2.1.114 requests, allowing proper authentication with Anthropic's API without triggering extra credits errors. * fix: resolve CodeQL password hash alert and TruffleHog CI failure --------- Co-authored-by: Randi <55005611+rdself@users.noreply.github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: Nikolay Popov <ekklesio.dev@gmail.com> Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com> * fix(claude-code): scope obfuscation to cli clients and fix tests * docs(workflows): enforce PR merge instead of manual close * docs(changelog): update 3.6.9 notes with missing PR 1403 and fixes * docs(workflows): update generate-release to use full changelog for PR body * fix(tsc): silence baseUrl deprecation warnings for TS 5.5+ * fix(chatcore): apply proactive compression before provider translation (#1406) Integrated into release/v3.6.9 * docs(changelog): add PR 1406 * Makes text visible in dark-mode (#1409) Integrated into release/v3.6.9 * docs(changelog): add PR 1409 * chore: save local work * chore(release): sync version references to 3.6.9 * fix(codex): prevent proactive token refresh consumption and strip background parameter * ci: shard long-running suites and relax timeouts * ci: allow manual CI dispatch for release branches * feat(skills): provider-aware marketplace UX, scored AUTO injection, and memory pipeline hardening (#1411) * fix/400 for GeminiCLI(add "ref" in GEMINI_UNSUPPORTED_SCHEMA_KEYS) * feat(cc-compatible): align request shape with Claude CLI * fix(cc-compatible): add Claude CLI system skeleton for OpenAI input * preserve reasoning when translating chat to responses (#1414) Integrated into release/v3.6.9 * fix(skills): optimize AUTO scoring and include Responses input context (#1418) Integrated into release/v3.6.9 * chore: fix TS errors and update review-prs workflow * fix(api): stop sending unsupported Gemini and Codex parameters Prevent Gemini request translation from injecting default thoughtSignature values that the upstream API strictly validates and rejects. Only preserve real signatures resolved from prior upstream responses, and strip additionalProperties from Gemini function schemas to avoid 400 "Unknown name" errors. Also remove fallback-injected session_id and conversation_id fields before sending Codex requests, and restore compatibility with the legacy OUTBOUND_SSRF_GUARD_ENABLED flag when determining whether private provider URLs are allowed. Updates the Gemini translator and regression tests for issue #1410 and related 400 error cases. * fix(core): stabilization fixes for token refresh, usage translation, and testing - Update Codex token refresh detection logic - Mark provider connections invalid on unrecoverable refresh error - Fix Claude usage translation under-reporting cached tokens - Update test expectations - Update CHANGELOG.md for v3.6.9 * fix(auth): reload fresh token state and unify expiry persistence Refresh checks now re-read the latest stored provider connection before attempting rotation so they do not use stale refresh tokens captured by an earlier sweep. Token updates also persist both expiresAt and tokenExpiresAt across the health check, usage-limit refresh path, and SSE refresh flow. This keeps known token expiry metadata in sync and avoids interval-based refreshes for connections whose tokens are still valid well into the future. * fix: resolve SSRF environment static evaluation bug (#1427) Fix import aliases and strict TS typings for tests and ACP agents. * test: resolve remaining strict type errors in test files * test: fix provider service assertion for anthropic-compatible header * fix(codex): respect openaiStoreEnabled setting during native passthrough (#1432) * fix(codex): fix token refresh unrecoverable detection for expired tokens * fix(ci): restore release v3.6.9 build and flaky tests * fix(cc-compatible): trim default OpenAI system skeleton (#1433) Integrated into release/v3.6.9 * fix: prevent masked API keys from being written to CLI tool configs (#1435) * feat: mark Qwen provider as deprecated and add deprecation warning to CLI tool (#1437) * docs(changelog): comprehensive v3.6.9 update with all 59 commits since v3.6.8 * test(ci): align qwen guide settings assertions * fix(security): resolve CodeQL alert 163 for incomplete URL sanitization in Qwen CLI settings --------- Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com> Co-authored-by: Nikolay Popov <74762779+nikolay-popov-ideogram@users.noreply.github.com> Co-authored-by: Randi <55005611+rdself@users.noreply.github.com> Co-authored-by: Nikolay Popov <ekklesio.dev@gmail.com> Co-authored-by: Paijo <14921983+oyi77@users.noreply.github.com> Co-authored-by: Tim Massey <tim-massey@users.noreply.github.com> Co-authored-by: Paijo <oyi77@users.noreply.github.com> Co-authored-by: dail45 <dail45@yandex.ru> Co-authored-by: R.D. <rogerproself@gmail.com>
315 lines
11 KiB
TypeScript
315 lines
11 KiB
TypeScript
/**
|
|
* E2E Test Suite — OmniRoute Ecosystem
|
|
*
|
|
* 6 scenarios covering MCP, A2A, Auto-Combo, Extension, Stress, and Security.
|
|
* Run with: npm run test:ecosystem
|
|
*/
|
|
|
|
import { describe, it, expect } from "vitest";
|
|
|
|
const BASE_URL = process.env.OMNIROUTE_BASE_URL || "http://localhost:20128";
|
|
const API_KEY = process.env.OMNIROUTE_API_KEY || "";
|
|
const REQUEST_TIMEOUT_MS = Number(process.env.ECOSYSTEM_REQUEST_TIMEOUT_MS || 30000);
|
|
const TEST_TIMEOUT_MS = Number(process.env.ECOSYSTEM_TEST_TIMEOUT_MS || 30000);
|
|
const STRESS_TIMEOUT_MS = Number(process.env.ECOSYSTEM_STRESS_TIMEOUT_MS || 45000);
|
|
|
|
function itCase(name: string, fn: () => Promise<void> | void) {
|
|
return it(name, fn, TEST_TIMEOUT_MS);
|
|
}
|
|
|
|
function itStress(name: string, fn: () => Promise<void> | void) {
|
|
return it(name, fn, STRESS_TIMEOUT_MS);
|
|
}
|
|
|
|
async function apiFetch(path: string, options?: RequestInit) {
|
|
return fetch(`${BASE_URL}${path}`, {
|
|
...options,
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
...(API_KEY ? { Authorization: `Bearer ${API_KEY}` } : {}),
|
|
...(options?.headers || {}),
|
|
},
|
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
});
|
|
}
|
|
|
|
// ─── Scenario 1: MCP Server Complete ─────────────────────────────
|
|
describe("E2E: MCP Server (16 tools)", () => {
|
|
itCase("should respond to health check", async () => {
|
|
const res = await apiFetch("/api/monitoring/health");
|
|
expect(res.ok).toBe(true);
|
|
const data = await res.json();
|
|
expect(data).toHaveProperty("status");
|
|
});
|
|
|
|
itCase("should list combos", async () => {
|
|
const res = await apiFetch("/api/combos");
|
|
expect(res.ok).toBe(true);
|
|
const data = await res.json();
|
|
expect(Array.isArray(data?.combos)).toBe(true);
|
|
});
|
|
|
|
itCase("should return quota data", async () => {
|
|
const res = await apiFetch("/api/usage/quota");
|
|
expect(res.ok).toBe(true);
|
|
const data = await res.json();
|
|
expect(Array.isArray(data?.providers)).toBe(true);
|
|
expect(data).toHaveProperty("meta");
|
|
});
|
|
|
|
itCase("should return usage analytics", async () => {
|
|
const res = await apiFetch("/api/usage/analytics?period=session");
|
|
expect(res.ok).toBe(true);
|
|
});
|
|
|
|
itCase("should return model catalog", async () => {
|
|
const res = await apiFetch("/api/models");
|
|
expect(res.ok).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ─── Scenario 1B: Quota Contract ─────────────────────────────
|
|
describe("E2E: Quota Contract (/api/usage/quota)", () => {
|
|
itCase("should return normalized quota response shape", async () => {
|
|
const res = await apiFetch("/api/usage/quota");
|
|
expect(res.ok).toBe(true);
|
|
|
|
const data = await res.json();
|
|
expect(Array.isArray(data.providers)).toBe(true);
|
|
expect(data).toHaveProperty("meta");
|
|
expect(typeof data.meta.generatedAt).toBe("string");
|
|
expect(typeof data.meta.totalProviders).toBe("number");
|
|
|
|
if (data.providers.length > 0) {
|
|
const p = data.providers[0];
|
|
expect(typeof p.name).toBe("string");
|
|
expect(typeof p.provider).toBe("string");
|
|
expect(typeof p.connectionId).toBe("string");
|
|
expect(typeof p.quotaUsed).toBe("number");
|
|
expect(typeof p.percentRemaining).toBe("number");
|
|
expect(p.percentRemaining).toBeGreaterThanOrEqual(0);
|
|
expect(p.percentRemaining).toBeLessThanOrEqual(100);
|
|
expect(["valid", "expiring", "expired", "refreshing"]).toContain(p.tokenStatus);
|
|
}
|
|
});
|
|
|
|
itCase("should filter quota by provider", async () => {
|
|
const allRes = await apiFetch("/api/usage/quota");
|
|
expect(allRes.ok).toBe(true);
|
|
const allData = await allRes.json();
|
|
if (!Array.isArray(allData.providers) || allData.providers.length === 0) return;
|
|
|
|
const provider = allData.providers[0].provider;
|
|
const filteredRes = await apiFetch(`/api/usage/quota?provider=${encodeURIComponent(provider)}`);
|
|
expect(filteredRes.ok).toBe(true);
|
|
const filteredData = await filteredRes.json();
|
|
expect(filteredData.meta.filters.provider).toBe(provider);
|
|
expect(Array.isArray(filteredData.providers)).toBe(true);
|
|
expect(filteredData.providers.every((p: any) => p.provider === provider)).toBe(true);
|
|
});
|
|
|
|
itCase("should filter quota by connectionId", async () => {
|
|
const allRes = await apiFetch("/api/usage/quota");
|
|
expect(allRes.ok).toBe(true);
|
|
const allData = await allRes.json();
|
|
if (!Array.isArray(allData.providers) || allData.providers.length === 0) return;
|
|
|
|
const connectionId = allData.providers[0].connectionId;
|
|
const filteredRes = await apiFetch(
|
|
`/api/usage/quota?connectionId=${encodeURIComponent(connectionId)}`
|
|
);
|
|
expect(filteredRes.ok).toBe(true);
|
|
const filteredData = await filteredRes.json();
|
|
expect(filteredData.meta.filters.connectionId).toBe(connectionId);
|
|
expect(Array.isArray(filteredData.providers)).toBe(true);
|
|
expect(filteredData.providers.every((p: any) => p.connectionId === connectionId)).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ─── Scenario 2: A2A Server Complete ─────────────────────────────
|
|
describe("E2E: A2A Server (lifecycle)", () => {
|
|
itCase("should serve Agent Card", async () => {
|
|
const res = await apiFetch("/.well-known/agent.json");
|
|
expect(res.ok).toBe(true);
|
|
const card = await res.json();
|
|
expect(card).toHaveProperty("name");
|
|
expect(card).toHaveProperty("skills");
|
|
expect(card).toHaveProperty("version");
|
|
expect(card.capabilities).toHaveProperty("streaming");
|
|
});
|
|
|
|
itCase("should accept message/send via JSON-RPC", async () => {
|
|
const res = await apiFetch("/a2a", {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
jsonrpc: "2.0",
|
|
id: "e2e-1",
|
|
method: "message/send",
|
|
params: {
|
|
skill: "quota-management",
|
|
messages: [{ role: "user", content: "show quota ranking" }],
|
|
},
|
|
}),
|
|
});
|
|
expect(res.ok).toBe(true);
|
|
const data = await res.json();
|
|
expect(data).toHaveProperty("result");
|
|
expect(data.result.task).toHaveProperty("id");
|
|
expect(data.result.task).toHaveProperty("state");
|
|
});
|
|
|
|
itCase("should reject invalid JSON-RPC method", async () => {
|
|
const res = await apiFetch("/a2a", {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
jsonrpc: "2.0",
|
|
id: "e2e-2",
|
|
method: "invalid/method",
|
|
params: {},
|
|
}),
|
|
});
|
|
const data = await res.json();
|
|
expect(data).toHaveProperty("error");
|
|
expect(data.error.code).toBe(-32601);
|
|
});
|
|
});
|
|
|
|
// ─── Scenario 3: Auto-Combo ─────────────────────────────────────
|
|
describe("E2E: Auto-Combo (routing + self-healing)", () => {
|
|
itCase("should create auto-combo", async () => {
|
|
const res = await apiFetch("/api/combos", {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
name: `e2e-auto-test-${Date.now()}`,
|
|
strategy: "auto",
|
|
models: [{ model: "gpt-4" }],
|
|
config: {
|
|
candidatePool: ["anthropic", "google"],
|
|
modePack: "ship-fast",
|
|
},
|
|
}),
|
|
});
|
|
if (!res.ok) console.error("POST /api/combos failed:", await res.text());
|
|
expect(res.ok).toBe(true);
|
|
});
|
|
|
|
itCase("should list auto-combos", async () => {
|
|
const res = await apiFetch("/api/combos");
|
|
if (!res.ok) console.error("GET /api/combos failed:", await res.text());
|
|
expect(res.ok).toBe(true);
|
|
const data = await res.json();
|
|
expect(Array.isArray(data?.combos)).toBe(true);
|
|
const autoCombos = data.combos.filter((c: any) => c.strategy === "auto");
|
|
expect(autoCombos.length).toBeGreaterThanOrEqual(0);
|
|
});
|
|
});
|
|
|
|
// ─── Scenario 4: OpenClaw Integration ────────────────────────────
|
|
describe("E2E: OpenClaw Integration", () => {
|
|
itCase("should return dynamic provider.order", async () => {
|
|
const res = await apiFetch("/api/cli-tools/openclaw/auto-order");
|
|
expect(res.ok).toBe(true);
|
|
const data = await res.json();
|
|
expect(data).toHaveProperty("provider");
|
|
expect(data.provider).toHaveProperty("order");
|
|
expect(Array.isArray(data.provider.order)).toBe(true);
|
|
expect(data.provider).toHaveProperty("allow_fallbacks");
|
|
expect(data).toHaveProperty("source");
|
|
});
|
|
});
|
|
|
|
// ─── Scenario 5: Stress Test ─────────────────────────────────────
|
|
describe("E2E: Stress (100 parallel requests)", () => {
|
|
itStress("should handle 100 health checks in <10s", async () => {
|
|
const start = Date.now();
|
|
const promises = Array.from({ length: 100 }, (_, i) =>
|
|
apiFetch("/api/monitoring/health").then((r) => ({
|
|
ok: r.ok,
|
|
index: i,
|
|
}))
|
|
);
|
|
const results = await Promise.allSettled(promises);
|
|
const elapsed = Date.now() - start;
|
|
|
|
const successful = results.filter((r) => r.status === "fulfilled" && r.value.ok).length;
|
|
|
|
expect(successful).toBeGreaterThanOrEqual(90); // allow 10% failure
|
|
expect(elapsed).toBeLessThan(10_000);
|
|
});
|
|
|
|
itStress("should handle 50 parallel quota checks", async () => {
|
|
const promises = Array.from({ length: 50 }, () =>
|
|
apiFetch("/api/usage/quota").then((r) => r.ok)
|
|
);
|
|
const results = await Promise.allSettled(promises);
|
|
const successful = results.filter((r) => r.status === "fulfilled" && r.value).length;
|
|
expect(successful).toBeGreaterThanOrEqual(40);
|
|
});
|
|
});
|
|
|
|
// ─── Scenario 6: Security ────────────────────────────────────────
|
|
describe("E2E: Security", () => {
|
|
itCase("should handle missing A2A auth according to server configuration", async () => {
|
|
const res = await fetch(`${BASE_URL}/a2a`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
// Intentionally no Authorization header
|
|
body: JSON.stringify({
|
|
jsonrpc: "2.0",
|
|
id: "sec-1",
|
|
method: "message/send",
|
|
params: { skill: "quota-management", messages: [] },
|
|
}),
|
|
});
|
|
if (API_KEY) {
|
|
expect(res.status).toBeGreaterThanOrEqual(401);
|
|
return;
|
|
}
|
|
|
|
expect([200, 400]).toContain(res.status);
|
|
});
|
|
|
|
itCase("should handle invalid API keys according to server configuration", async () => {
|
|
const res = await fetch(`${BASE_URL}/a2a`, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: "Bearer invalid-key-12345",
|
|
},
|
|
body: JSON.stringify({
|
|
jsonrpc: "2.0",
|
|
id: "sec-2",
|
|
method: "message/send",
|
|
params: { skill: "quota-management", messages: [] },
|
|
}),
|
|
});
|
|
if (API_KEY) {
|
|
expect(res.status).toBeGreaterThanOrEqual(401);
|
|
return;
|
|
}
|
|
|
|
expect([200, 400]).toContain(res.status);
|
|
});
|
|
|
|
itCase("should not expose internal errors in API responses", async () => {
|
|
const res = await apiFetch("/api/monitoring/health", {
|
|
method: "PATCH",
|
|
});
|
|
// Should return method error without leaking server internals
|
|
expect(res.status).toBe(405);
|
|
const body = await res.text();
|
|
expect(body.includes("Error:")).toBe(false);
|
|
});
|
|
|
|
itCase("should validate JSON-RPC request format", async () => {
|
|
const res = await apiFetch("/a2a", {
|
|
method: "POST",
|
|
body: "not-json",
|
|
});
|
|
const data = await res.json();
|
|
if (data.error) {
|
|
expect(data.error.code).toBe(-32700); // Parse error
|
|
}
|
|
});
|
|
});
|