Merge remote-tracking branch 'origin/release/v3.8.49' into fix/port-issue-2057-combo-custom-provider-models

# Conflicts:
#	src/app/(dashboard)/dashboard/combos/page.tsx
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-17 10:49:26 -03:00
685 changed files with 34702 additions and 2190 deletions

View File

@@ -0,0 +1,23 @@
// jsdom (unlike real browsers) does not implement `window.matchMedia`. Several
// dashboard components read the OS color-scheme preference via
// `window.matchMedia("(prefers-color-scheme: dark)")` (see
// `src/shared/hooks/useTheme.ts`), so any test that mounts a component using
// that hook (directly or transitively, e.g. via `ProviderIcon`) crashes with
// `TypeError: window.matchMedia is not a function` unless this polyfill runs
// first. Keep this minimal — it only needs to satisfy the subset of the
// MediaQueryList API this codebase actually calls.
if (typeof window !== "undefined" && typeof window.matchMedia !== "function") {
window.matchMedia = (query: string): MediaQueryList => {
const mql = {
matches: false,
media: query,
onchange: null,
addListener: () => {},
removeListener: () => {},
addEventListener: () => {},
removeEventListener: () => {},
dispatchEvent: () => false,
};
return mql as unknown as MediaQueryList;
};
}

View File

@@ -0,0 +1,41 @@
### GET /v1/models autenticado retorna catálogo
GET {{baseUrl}}/v1/models
Authorization: Bearer {{apiKey}}
?? status == 200
?? js response.parsedBody.data.length > 0
### chat completions non-stream com modelo do tier crítico
POST {{baseUrl}}/v1/chat/completions
Authorization: Bearer {{apiKey}}
Content-Type: application/json
{
"model": "{{smokeModel}}",
"messages": [{ "role": "user", "content": "Reply with exactly: OK" }],
"max_tokens": 5,
"stream": false
}
?? status == 200
?? js response.parsedBody.choices[0].message.content.length > 0
### management sem credencial é rejeitado (401)
# /v1/models pode ser público (REQUIRE_API_KEY off na VPS), então o teste de auth
# usa a superfície de management, que exige credencial sempre.
GET {{baseUrl}}/api/keys
?? status == 401
### management com bearer inválido é rejeitado (403)
GET {{baseUrl}}/api/keys
Authorization: Bearer or-invalid-key-homolog
?? status == 403
### health é público e saudável
# httpYac trata o lado direito de `==` como literal — sem aspas.
GET {{baseUrl}}/api/monitoring/health
?? status == 200
?? js response.parsedBody.status == healthy

View File

@@ -0,0 +1,31 @@
import { test, expect } from "@playwright/test";
// Locators confirmados em
// src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx (rota real da
// tela de keys é /dashboard/api-manager, não /dashboard/api-keys):
// - botão "Create API Key" (t("createKey")) abre o modal; o submit do modal tem o mesmo texto
// - <Input placeholder={t("keyNamePlaceholder")}> = "e.g. Production Key"
// - após criar, abre o "Created Key Modal" (t("keyCreated")) — fechar pelo botão t("done")="Done"
// - cada key vira uma linha div.grid-cols-12; o botão de deletar tem title={t("deleteKey")}="Delete key"
// - handleDeleteKey usa window.confirm(t("deleteConfirm")) — não é modal de UI,
// precisa do listener page.on("dialog", ...).
const KEY_NAME = `homolog-ui-${Date.now()}`;
test("cria e revoga uma API key pela UI", async ({ page }) => {
page.on("dialog", (dialog) => dialog.accept());
await page.goto("/dashboard/api-manager");
await page.getByRole("button", { name: "Create API Key" }).first().click();
await page.getByPlaceholder("e.g. Production Key").fill(KEY_NAME);
// segundo "Create API Key" é o submit do modal (o primeiro é o botão que o abriu)
await page.getByRole("button", { name: "Create API Key" }).last().click();
// fecha o modal "API Key Created"
await page.getByRole("button", { name: "Done" }).click();
const row = page.locator("div.grid-cols-12", { hasText: KEY_NAME });
await expect(row).toHaveCount(1);
// revoga a mesma key (cleanup — a suíte não deixa lixo na VPS)
await row.getByTitle("Delete key").click();
await expect(page.locator("div.grid-cols-12", { hasText: KEY_NAME })).toHaveCount(0);
});

View File

@@ -0,0 +1,13 @@
import { test as setup, expect } from "@playwright/test";
import { STORAGE_STATE } from "./playwright.config";
// Locators confirmados em src/app/login/page.tsx: <Input type="password"> dentro de um
// <form onSubmit={handleLogin}> com <Button type="submit">{t("continue")}</Button>.
setup("autentica e salva storageState", async ({ page }) => {
await page.goto("/login");
await page.locator('input[type="password"]').fill(process.env.HOMOLOG_ADMIN_PASSWORD!);
await page.locator('button[type="submit"]').click();
await page.waitForURL(/\/dashboard/);
await expect(page).toHaveURL(/dashboard/);
await page.context().storageState({ path: STORAGE_STATE });
});

View File

@@ -0,0 +1,42 @@
import { defineConfig } from "@playwright/test";
import path from "node:path";
import { fileURLToPath } from "node:url";
const HERE = path.dirname(fileURLToPath(import.meta.url));
export const STORAGE_STATE = path.join(HERE, ".auth", "admin.json");
export default defineConfig({
testDir: ".",
timeout: 60_000,
retries: 1,
// Sem fullyParallel, os 98 testes de routes.spec.ts (mesmo arquivo) rodam
// SERIALIZADOS num único worker (~10min); com ele, distribuem entre os workers.
fullyParallel: true,
workers: 8,
reporter: [
["list"],
[
// outputDir ABSOLUTO: o reporter resolve paths relativos contra o CWD do
// processo (não contra o config) — um path relativo escapava do worktree.
"playwright-ctrf-json-reporter",
{
outputDir: path.resolve(HERE, "..", "..", "..", "homolog-report"),
outputFile: "ui-ctrf.json",
},
],
],
use: {
baseURL: process.env.HOMOLOG_BASE_URL || "http://192.168.0.15:20128",
trace: "retain-on-failure",
screenshot: "only-on-failure",
},
projects: [
{ name: "setup", testMatch: /auth\.setup\.ts/ },
{
name: "homolog",
testMatch: /.*\.spec\.ts/,
dependencies: ["setup"],
use: { storageState: STORAGE_STATE },
},
],
});

View File

@@ -0,0 +1,40 @@
import { test, expect } from "@playwright/test";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
// Descobre as rotas estáticas do dashboard a partir do próprio repo:
// cada page.tsx sob src/app/(dashboard)/dashboard vira uma rota; grupos (x) somem
// do path e rotas dinâmicas [param] são puladas (sem dado real garantido).
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
const BASE = path.join(ROOT, "src", "app", "(dashboard)", "dashboard");
function discoverRoutes(dir: string, prefix = "/dashboard"): string[] {
const routes: string[] = [];
if (fs.existsSync(path.join(dir, "page.tsx"))) routes.push(prefix || "/");
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
if (!e.isDirectory() || e.name.startsWith("[") || e.name.startsWith("_")) continue;
const seg = e.name.startsWith("(") ? "" : `/${e.name}`;
routes.push(...discoverRoutes(path.join(dir, e.name), `${prefix}${seg}`));
}
return [...new Set(routes)];
}
for (const route of discoverRoutes(BASE)) {
test(`rota ${route} carrega sem crash`, async ({ page }) => {
const pageErrors: string[] = [];
page.on("pageerror", (err) => pageErrors.push(err.message));
const res = await page.goto(route, { waitUntil: "domcontentloaded" });
expect(res!.status(), `HTTP em ${route}`).toBeLessThan(400);
// "networkidle" nunca assenta em telas com polling/websocket ao vivo (30s x 98 rotas
// estourava o run inteiro) — "load" + um settle curto e suficiente para hidratar e
// deixar um crash de client component (pageerror / error boundary) aparecer.
await page.waitForLoadState("load", { timeout: 10_000 }).catch(() => {});
await page.waitForTimeout(1_500);
// Error boundary do Next: nunca pode aparecer
await expect(page.locator("text=Application error")).toHaveCount(0);
expect(pageErrors, `pageerror em ${route}: ${pageErrors.join(" | ")}`).toHaveLength(0);
});
}

View File

@@ -2,9 +2,9 @@
* Integration tests for Agent Skills content integrity.
*
* Verifies:
* 1. All 43 skill IDs from catalog have skills/{id}/ folder with SKILL.md.
* 1. All 44 skill IDs from catalog have skills/{id}/ folder with SKILL.md.
* 2. Zero omniroute-* folders remain (post-prune: old omniroute-* skill dirs were removed).
* 3. 10 specific IDs have <!-- skill:custom-start --> ... <!-- skill:custom-end --> blocks:
* 3. 12 specific IDs have <!-- skill:custom-start --> ... <!-- skill:custom-end --> blocks:
* omni-mcp, omni-compression, cli-providers, cli-eval, omni-agents-a2a,
* omni-combos-routing, omni-auth, omni-resilience, omni-inference, cli-serve.
*
@@ -22,6 +22,7 @@ const ALL_IDS = [...API_SKILL_IDS, ...CLI_SKILL_IDS, ...CONFIG_SKILL_IDS] as str
// IDs that must have a custom block
const CUSTOM_BLOCK_IDS = [
"cli-skill-collector",
"omni-mcp",
"omni-compression",
"cli-providers",
@@ -37,7 +38,7 @@ const CUSTOM_BLOCK_IDS = [
// ── §1: All 42 catalog IDs have skills/{id}/SKILL.md ─────────────────────────
test("all 43 catalog IDs have a skills/{id}/ directory", () => {
test("all 44 catalog IDs have a skills/{id}/ directory", () => {
const missing: string[] = [];
for (const id of ALL_IDS) {
const dirPath = path.join(SKILLS_DIR, id);
@@ -48,7 +49,7 @@ test("all 43 catalog IDs have a skills/{id}/ directory", () => {
assert.deepEqual(missing, [], `Missing skill directories: ${missing.join(", ")}`);
});
test("all 43 catalog IDs have a skills/{id}/SKILL.md file", () => {
test("all 44 catalog IDs have a skills/{id}/SKILL.md file", () => {
const missing: string[] = [];
for (const id of ALL_IDS) {
const skillPath = path.join(SKILLS_DIR, id, "SKILL.md");
@@ -113,7 +114,7 @@ for (const id of CUSTOM_BLOCK_IDS) {
// ── Additional integrity checks ───────────────────────────────────────────────
test("exactly 11 skills have custom blocks", () => {
test("exactly 12 skills have custom blocks", () => {
const withCustomBlocks: string[] = [];
for (const id of ALL_IDS) {
const skillPath = path.join(SKILLS_DIR, id, "SKILL.md");
@@ -128,7 +129,7 @@ test("exactly 11 skills have custom blocks", () => {
assert.deepEqual(
withCustomBlocks.sort(),
expectedIds,
`Expected exactly these 11 custom-block IDs: ${expectedIds.join(", ")}\nActual: ${withCustomBlocks.join(", ")}`,
`Expected exactly these 12 custom-block IDs: ${expectedIds.join(", ")}\nActual: ${withCustomBlocks.join(", ")}`,
);
});

View File

@@ -69,8 +69,8 @@ test("every CLI skill ID has skills/<id>/SKILL.md on disk", () => {
assert.deepEqual(missing, [], `Missing CLI SKILL.md files: ${missing.join(", ")}`);
});
test("total skill count is exactly 43 (23 API + 20 CLI)", () => {
assert.equal(API_SKILL_IDS.length + CLI_SKILL_IDS.length, 43);
test("total skill count is exactly 44 (23 API + 21 CLI)", () => {
assert.equal(API_SKILL_IDS.length + CLI_SKILL_IDS.length, 44);
});
// ── §2: Frontmatter validation ────────────────────────────────────────────────
@@ -120,11 +120,11 @@ test("each SKILL.md body is at least 100 chars", () => {
// ── §3: MCP tool omniroute_agent_skills_list ─────────────────────────────────
test("MCP omniroute_agent_skills_list handler returns count 44 (43 + config)", async () => {
test("MCP omniroute_agent_skills_list handler returns count 45 (44 + config)", async () => {
const result = await agentSkillTools.omniroute_agent_skills_list.handler({});
assert.equal(result.count, 44, `Expected 44 but got ${result.count}`);
assert.equal(result.count, 45, `Expected 45 but got ${result.count}`);
assert.ok(Array.isArray(result.skills));
assert.equal(result.skills.length, 44);
assert.equal(result.skills.length, 45);
});
test("MCP omniroute_agent_skills_list result has all 42 IDs", async () => {
@@ -157,9 +157,9 @@ test("A2A list-capabilities artifact content contains 42 skill IDs as table rows
assert.ok(rows.length >= 42, `Expected at least 42 data rows but got ${rows.length}`);
});
test("A2A list-capabilities metadata.totalSkills === 44 (43 + config)", async () => {
test("A2A list-capabilities metadata.totalSkills === 45 (44 + config)", async () => {
const result = await executeListCapabilities(stubTask);
assert.equal(result.metadata.totalSkills, 44);
assert.equal(result.metadata.totalSkills, 45);
});
test("A2A list-capabilities artifact contains all 42 skill IDs", async () => {

View File

@@ -0,0 +1,266 @@
/**
* Integration tests for /api/cli-tools/grok-build-settings
*
* Ported from decolua/9router#2571 ("feat(cli-tools): add Grok Build setup"),
* rebuilt on top of OmniRoute's existing "custom" configType settings pattern
* (auth guard, Zod validation, write-guard, backups, sanitized errors — see
* forge-settings for the sibling implementation this mirrors).
*
* Unlike Forge's full-file overwrite, Grok Build's config.toml can hold other
* user-defined `[model.*]` sections, so the handler surgically upserts only
* the `[model.omniroute]` section and preserves the rest of the file.
*/
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-grok-build-settings-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.API_KEY_SECRET = "test-api-key-secret-grok-build";
process.env.JWT_SECRET = "test-jwt-secret-grok-build";
// Import DB reset helpers (must be before route import)
const core = await import("../../src/lib/db/core.ts");
const localDb = await import("../../src/lib/localDb.ts");
// Import route handlers
const { GET, POST, DELETE } = await import(
"../../src/app/api/cli-tools/grok-build-settings/route.ts"
);
async function resetStorage() {
delete process.env.INITIAL_PASSWORD;
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}
async function enableAuth() {
process.env.INITIAL_PASSWORD = "test-bootstrap";
await localDb.updateSettings({ requireLogin: true, password: "" });
}
test.beforeEach(async () => {
await resetStorage();
});
// ── Test 1: GET without auth when auth is required → 401 ────────────────────
test("grok-build-settings GET: returns 401 when auth required and no token", async () => {
await enableAuth();
const res = await GET(new Request("http://localhost/api/cli-tools/grok-build-settings"));
assert.equal(res.status, 401, `Expected 401, got ${res.status}`);
});
// ── Test 2: GET with valid auth → 200 ────────────────────────────────────────
test("grok-build-settings GET: returns 200 with valid auth (grok not installed on CI)", async () => {
const res = await GET(new Request("http://localhost/api/cli-tools/grok-build-settings"));
assert.equal(res.status, 200, `Expected 200, got ${res.status}`);
const body = await res.json();
assert.ok(
"installed" in body || "config" in body,
"Response should contain installed or config field"
);
});
// ── Test 3: POST with invalid body → 400 ─────────────────────────────────────
test("grok-build-settings POST: 400 when baseUrl is missing", async () => {
const res = await POST(
new Request("http://localhost/api/cli-tools/grok-build-settings", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ apiKey: "sk-test", model: "grok-4.5" }), // missing baseUrl
})
);
assert.equal(res.status, 400, `Expected 400 for missing baseUrl, got ${res.status}`);
const body = await res.json();
assert.ok(body.error !== undefined, "Response should have error field");
});
test("grok-build-settings POST: 400 when model is missing", async () => {
const res = await POST(
new Request("http://localhost/api/cli-tools/grok-build-settings", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ baseUrl: "http://localhost:20128", apiKey: "sk-test" }),
})
);
assert.equal(res.status, 400, `Expected 400 for missing model, got ${res.status}`);
});
// ── Test 4: POST with valid body → surgically upserts [model.omniroute] ─────
test("grok-build-settings POST: writes [model.omniroute] section and preserves existing content", async () => {
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "grok-build-home-"));
const origHome = process.env.HOME;
process.env.HOME = tmpHome;
try {
// Pre-seed a config.toml with an unrelated user model + a non-default value,
// to prove the handler does not clobber content it does not own.
const grokDir = path.join(tmpHome, ".grok");
fs.mkdirSync(grokDir, { recursive: true });
const preExisting = [
"[models]",
'default = "grok-build"',
"",
"[model.custom-thing]",
'model = "some-other-model"',
'base_url = "https://example.test/v1"',
"",
].join("\n");
fs.writeFileSync(path.join(grokDir, "config.toml"), preExisting);
const res = await POST(
new Request("http://localhost/api/cli-tools/grok-build-settings", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
baseUrl: "http://localhost:20128",
apiKey: "sk-test-grok-build-key",
model: "grok-4.5",
}),
})
);
// 200 = success; 403 = write guard active (test env); 500 = backup dir issue
assert.ok([200, 403, 500].includes(res.status), `Unexpected status ${res.status}`);
if (res.status === 200) {
const body = await res.json();
assert.equal(body.success, true, "success should be true on 200");
const configPath = path.join(tmpHome, ".grok", "config.toml");
const content = fs.readFileSync(configPath, "utf-8");
assert.ok(content.includes("[model.omniroute]"), "Config should have [model.omniroute]");
assert.ok(content.includes("http://localhost:20128/v1"), "Config should contain base URL");
assert.ok(content.includes('default = "omniroute"'), "Default should point at our slot");
// The pre-existing unrelated model section must survive untouched.
assert.ok(
content.includes("[model.custom-thing]") &&
content.includes("https://example.test/v1"),
"Pre-existing unrelated [model.*] section must be preserved"
);
// The previous default must be remembered for Reset to restore.
assert.ok(
content.includes('omniroute-prev-default = "grok-build"'),
"Previous default should be remembered as a marker comment"
);
}
} finally {
process.env.HOME = origHome;
fs.rmSync(tmpHome, { recursive: true, force: true });
}
});
// ── Test 5: DELETE → removes only our section and restores previous default ─
test("grok-build-settings DELETE: removes our section, preserves the rest, restores default", async () => {
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "grok-build-home-del-"));
const origHome = process.env.HOME;
process.env.HOME = tmpHome;
try {
const grokDir = path.join(tmpHome, ".grok");
fs.mkdirSync(grokDir, { recursive: true });
const preConfigured = [
"[models]",
'default = "omniroute"',
"",
"# omniroute-prev-default = \"grok-build\"",
"[model.omniroute]",
'model = "grok-4.5"',
'base_url = "http://localhost:20128/v1"',
'name = "OmniRoute"',
'api_backend = "chat_completions"',
'api_key = "sk-test"',
"",
"[model.custom-thing]",
'model = "some-other-model"',
'base_url = "https://example.test/v1"',
"",
].join("\n");
fs.writeFileSync(path.join(grokDir, "config.toml"), preConfigured);
const res = await DELETE(
new Request("http://localhost/api/cli-tools/grok-build-settings", { method: "DELETE" })
);
assert.ok([200, 403, 500].includes(res.status), `Expected 200/403/500, got ${res.status}`);
if (res.status === 200) {
const body = await res.json();
assert.equal(body.success, true);
const configPath = path.join(tmpHome, ".grok", "config.toml");
const content = fs.readFileSync(configPath, "utf-8");
assert.ok(!content.includes("[model.omniroute]"), "Our section should be removed");
assert.ok(
content.includes("[model.custom-thing]") && content.includes("https://example.test/v1"),
"Unrelated section must survive"
);
assert.ok(content.includes('default = "grok-build"'), "Previous default should be restored");
}
} finally {
process.env.HOME = origHome;
fs.rmSync(tmpHome, { recursive: true, force: true });
}
});
test("grok-build-settings DELETE: no-op success when no config file exists", async () => {
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "grok-build-home-noconfig-"));
const origHome = process.env.HOME;
process.env.HOME = tmpHome;
try {
const res = await DELETE(
new Request("http://localhost/api/cli-tools/grok-build-settings", { method: "DELETE" })
);
assert.equal(res.status, 200);
const body = await res.json();
assert.equal(body.success, true);
} finally {
process.env.HOME = origHome;
fs.rmSync(tmpHome, { recursive: true, force: true });
}
});
// ── Test 6: Error sanitization (Hard Rule #12) ───────────────────────────────
test("grok-build-settings: error responses do not leak stack traces", async () => {
const badReq = new Request("http://localhost/api/cli-tools/grok-build-settings", {
method: "POST",
headers: { "content-type": "application/json" },
body: "{ this is not json }",
});
const res = await POST(badReq);
const bodyStr = JSON.stringify(await res.json());
assert.ok(
!bodyStr.match(/\s+at\s+\/[^\s]/),
"Error response must not contain absolute-path stack traces"
);
});
// ── Test 7: Hard Rule #13 (no exec/spawn) ────────────────────────────────────
test("grok-build-settings route.ts: does not call exec() or spawn() directly", () => {
const routePath = path.resolve(
import.meta.dirname,
"../../src/app/api/cli-tools/grok-build-settings/route.ts"
);
const content = fs.readFileSync(routePath, "utf-8");
assert.ok(!content.match(/\bexec\s*\(/), "Handler must not use exec()");
assert.ok(!content.match(/\bspawn\s*\(/), "Handler must not use spawn()");
});
test.after(async () => {
await resetStorage();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
delete process.env.DATA_DIR;
delete process.env.API_KEY_SECRET;
delete process.env.JWT_SECRET;
});

View File

@@ -0,0 +1,305 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import fsp from "node:fs/promises";
import http from "node:http";
import os from "node:os";
import path from "node:path";
import { once } from "node:events";
const CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
const ENCRYPTED_CONTENT_SENTINEL = "encrypted-codex-state:" + "A".repeat(910);
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-codex-chat-http-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.DISABLE_SQLITE_AUTO_BACKUP = "true";
process.env.API_KEY_SECRET = "codex-chat-http-e2e-secret-123456";
process.env.REQUIRE_API_KEY = "false";
process.env.OMNIROUTE_LOG_REQUEST_SHAPE = "0";
const core = await import("../../src/lib/db/core.ts");
const providersDb = await import("../../src/lib/db/providers.ts");
const chatRoute = await import("../../src/app/api/v1/chat/completions/route.ts");
const originalFetch = globalThis.fetch;
type RecordedRequest = {
url: string;
method: string;
body: Record<string, unknown>;
};
function responsesEvents() {
const response = {
id: "resp_reasoning_http",
object: "response",
status: "in_progress",
model: "gpt-5.6-sol",
output: [],
};
return [
{ type: "response.created", response },
{
type: "response.output_item.added",
output_index: 0,
item: {
id: "rs_reasoning_http",
type: "reasoning",
encrypted_content: ENCRYPTED_CONTENT_SENTINEL,
summary: [],
},
},
{
type: "response.output_item.done",
output_index: 0,
item: {
id: "rs_reasoning_http",
type: "reasoning",
encrypted_content: ENCRYPTED_CONTENT_SENTINEL,
summary: [],
},
},
{
type: "response.output_item.added",
output_index: 1,
item: { id: "msg_reasoning_http", type: "message", role: "assistant", content: [] },
},
{
type: "response.content_part.added",
item_id: "msg_reasoning_http",
output_index: 1,
content_index: 0,
part: { type: "output_text", text: "", annotations: [] },
},
{
type: "response.output_text.delta",
item_id: "msg_reasoning_http",
output_index: 1,
content_index: 0,
delta: "The answer is 42.",
},
{
type: "response.output_text.done",
item_id: "msg_reasoning_http",
output_index: 1,
content_index: 0,
text: "The answer is 42.",
},
{
type: "response.output_item.done",
output_index: 1,
item: {
id: "msg_reasoning_http",
type: "message",
role: "assistant",
content: [{ type: "output_text", text: "The answer is 42.", annotations: [] }],
},
},
{
type: "response.completed",
response: {
...response,
status: "completed",
output: [
{
id: "rs_reasoning_http",
type: "reasoning",
summary: [{ type: "summary_text", text: "I checked the contract. " }],
},
{
id: "msg_reasoning_http",
type: "message",
role: "assistant",
content: [{ type: "output_text", text: "The answer is 42.", annotations: [] }],
},
],
usage: { input_tokens: 8, output_tokens: 9, total_tokens: 17 },
},
},
];
}
function mockResponsesSse() {
const nativeFraming = process.env.CODEX_NATIVE_EVENT_FRAMING === "1";
return responsesEvents()
.map((event) => {
const eventLine = nativeFraming ? `event: ${event.type}\n` : "";
return `${eventLine}data: ${JSON.stringify(event)}\n\n`;
})
.join("");
}
async function readIncomingBody(request: http.IncomingMessage) {
const chunks: Buffer[] = [];
for await (const chunk of request)
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
return Buffer.concat(chunks);
}
async function bridgeRouteResponse(response: Response, outgoing: http.ServerResponse) {
outgoing.writeHead(response.status, Object.fromEntries(response.headers.entries()));
if (!response.body) {
outgoing.end();
return;
}
const reader = response.body.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
if (!outgoing.write(value)) await once(outgoing, "drain");
}
outgoing.end();
} finally {
reader.releaseLock();
}
}
async function startRouteServer() {
const server = http.createServer(async (incoming, outgoing) => {
try {
if (incoming.method !== "POST" || incoming.url !== "/v1/chat/completions") {
outgoing.writeHead(404).end();
return;
}
const body = await readIncomingBody(incoming);
const address = server.address();
assert(address && typeof address !== "string");
const headers = new Headers();
for (const [name, value] of Object.entries(incoming.headers)) {
if (Array.isArray(value)) value.forEach((item) => headers.append(name, item));
else if (value !== undefined) headers.set(name, value);
}
const request = new Request(`http://127.0.0.1:${address.port}${incoming.url}`, {
method: incoming.method,
headers,
body,
});
await bridgeRouteResponse(await chatRoute.POST(request), outgoing);
} catch {
// Mock route bridge: static body only — CodeQL flags ANY error-derived value here,
// including error.message / String(error) (js/stack-trace-exposure #736/#737). The test
// only asserts status===200, so the 500 body is never inspected.
outgoing.writeHead(500, { "content-type": "text/plain" });
outgoing.end("internal test route error");
}
});
await new Promise<void>((resolve, reject) => {
server.once("error", reject);
server.listen(0, "127.0.0.1", resolve);
});
const address = server.address();
assert(address && typeof address !== "string");
return { server, url: `http://127.0.0.1:${address.port}/v1/chat/completions` };
}
function parseSse(raw: string) {
return raw
.split(/\n\n+/)
.map((block) =>
block
.split("\n")
.find((line) => line.startsWith("data: "))
?.slice(6)
)
.filter((data): data is string => Boolean(data));
}
async function closeServer(server: http.Server) {
if (!server.listening) return;
await new Promise<void>((resolve, reject) =>
server.close((error) => (error ? reject(error) : resolve()))
);
}
test("chat completions streams Codex Responses reasoning through real route HTTP", async () => {
const recorded: RecordedRequest[] = [];
let routeServer: http.Server | undefined;
try {
await providersDb.createProviderConnection({
provider: "codex",
authType: "oauth",
name: "codex-http-reasoning",
email: "codex-http@example.test",
accessToken: "mock-codex-access-token",
refreshToken: "mock-codex-refresh-token",
tokenType: "Bearer",
expiresAt: new Date(Date.now() + 60 * 60 * 1000).toISOString(),
isActive: true,
testStatus: "active",
providerSpecificData: {},
});
const routeHarness = await startRouteServer();
routeServer = routeHarness.server;
globalThis.fetch = async (input, init) => {
const request = input instanceof Request ? input : new Request(input, init);
if (request.url !== CODEX_RESPONSES_URL) {
throw new Error(`Unexpected external fetch in Codex HTTP test: ${request.url}`);
}
recorded.push({
url: request.url,
method: request.method,
body: JSON.parse(await request.text()) as Record<string, unknown>,
});
return new Response(mockResponsesSse(), {
status: 200,
headers: { "content-type": "text/event-stream; charset=utf-8" },
});
};
const response = await originalFetch(routeHarness.url, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
model: "codex/gpt-5.6-sol",
stream: true,
reasoning_effort: "high",
messages: [{ role: "user", content: "What is the answer?" }],
}),
});
const raw = await response.text();
assert.equal(response.status, 200, raw);
assert.match(response.headers.get("content-type") ?? "", /^text\/event-stream/);
assert.equal(recorded.length, 1);
assert.equal(recorded[0].url, CODEX_RESPONSES_URL);
assert.equal(recorded[0].method, "POST");
assert.deepEqual(recorded[0].body.reasoning, { effort: "high", summary: "auto" });
assert.deepEqual(recorded[0].body.input, [
{
type: "message",
role: "user",
content: [{ type: "input_text", text: "What is the answer?" }],
},
]);
const chunks = parseSse(raw);
assert.equal(chunks.at(-1), "[DONE]");
const payloads = chunks.slice(0, -1).map((chunk) => JSON.parse(chunk));
const reasoningContentDeltas = payloads
.map((payload) => payload.choices?.[0]?.delta?.reasoning_content)
.filter((content): content is string => Boolean(content));
assert.equal(reasoningContentDeltas.length, 1);
const reasoningContent = reasoningContentDeltas.join("");
assert.match(reasoningContent, /encrypted (?:state|private reasoning)/i);
assert(!raw.includes(ENCRYPTED_CONTENT_SENTINEL), raw);
assert(!reasoningContent.includes(ENCRYPTED_CONTENT_SENTINEL), reasoningContent);
assert(
payloads.some((payload) => payload.choices?.[0]?.delta?.content === "The answer is 42.")
);
assert(!raw.includes("response.reasoning_summary_text.delta"), raw);
assert(!raw.includes('"type":"error"'), raw);
assert(!raw.includes('"error"'), raw);
} finally {
globalThis.fetch = originalFetch;
if (routeServer) await closeServer(routeServer);
core.closeDbInstance({ checkpointMode: null });
await fsp.rm(TEST_DATA_DIR, { recursive: true, force: true });
}
});

View File

@@ -658,6 +658,29 @@
"stream": "https://chatgpt.com/backend-api/conversation"
}
},
"chenzk": {
"format": "openai",
"headers": {
"apiKey": {
"Accept": "text/event-stream",
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json"
},
"nonStream": {
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json"
},
"oauth": {
"Accept": "text/event-stream",
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json"
}
},
"url": {
"nonStream": "https://chenzk.top/v1/chat/completions",
"stream": "https://chenzk.top/v1/chat/completions"
}
},
"chipotle": {
"format": "openai",
"headers": {

View File

@@ -0,0 +1,24 @@
import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import path from "node:path";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const clientPath = path.resolve(
__dirname,
"../../src/app/(dashboard)/dashboard/tools/agent-bridge/AgentBridgePageClient.tsx"
);
const source = readFileSync(clientPath, "utf8");
test("#7157: dns toggle fetch call uses method POST (route.ts only exports POST)", () => {
const dnsCallMatch = source.match(
/\/api\/tools\/agent-bridge\/agents\/\$\{agentId\}\/dns`,\s*\{\s*method:\s*"([A-Z]+)"/
);
assert.ok(dnsCallMatch, "expected to find the dns fetch call in AgentBridgePageClient.tsx");
assert.equal(
dnsCallMatch?.[1],
"POST",
"dns fetch call must use method: 'POST' to match the route.ts export (issue #7157)"
);
});

View File

@@ -58,11 +58,11 @@ test("each agentSkillTool has name, description, inputSchema, and handler", () =
// ─── omniroute_agent_skills_list ────────────────────────────────────────────
test("omniroute_agent_skills_list with no filters returns all 44 skills", async () => {
test("omniroute_agent_skills_list with no filters returns all 45 skills", async () => {
const result = await agentSkillTools.omniroute_agent_skills_list.handler({});
assert.equal(result.count, 44, `Expected 44 but got ${result.count}`);
assert.equal(result.count, 45, `Expected 45 but got ${result.count}`);
assert.ok(Array.isArray(result.skills));
assert.equal(result.skills.length, 44);
assert.equal(result.skills.length, 45);
});
test("omniroute_agent_skills_list({category:'api'}) returns exactly 23 entries", async () => {
@@ -71,9 +71,9 @@ test("omniroute_agent_skills_list({category:'api'}) returns exactly 23 entries",
assert.ok(result.skills.every((s: { category: string }) => s.category === "api"));
});
test("omniroute_agent_skills_list({category:'cli'}) returns exactly 20 entries", async () => {
test("omniroute_agent_skills_list({category:'cli'}) returns exactly 21 entries", async () => {
const result = await agentSkillTools.omniroute_agent_skills_list.handler({ category: "cli" });
assert.equal(result.count, 20, `Expected 20 cli skills but got ${result.count}`);
assert.equal(result.count, 21, `Expected 21 cli skills but got ${result.count}`);
assert.ok(result.skills.every((s: { category: string }) => s.category === "cli"));
});
@@ -83,7 +83,7 @@ test("omniroute_agent_skills_list result includes coverage shape", async () => {
assert.ok(typeof result.coverage.api === "object");
assert.ok(typeof result.coverage.cli === "object");
assert.equal(result.coverage.api.total, 23);
assert.equal(result.coverage.cli.total, 20);
assert.equal(result.coverage.cli.total, 21);
assert.ok(typeof result.coverage.totalSkills === "number");
assert.ok(typeof result.coverage.generatedAt === "string");
});
@@ -168,11 +168,11 @@ test("omniroute_agent_skills_coverage({}) returns coverage shape", async () => {
assert.ok(typeof result.api === "object");
assert.ok(typeof result.cli === "object");
assert.equal(result.api.total, 23);
assert.equal(result.cli.total, 20);
assert.equal(result.cli.total, 21);
assert.ok(typeof result.api.have === "number");
assert.ok(typeof result.cli.have === "number");
assert.ok(result.api.have >= 0 && result.api.have <= 23);
assert.ok(result.cli.have >= 0 && result.cli.have <= 20);
assert.ok(result.cli.have >= 0 && result.cli.have <= 21);
assert.ok(typeof result.totalSkills === "number");
assert.equal(result.totalSkills, result.api.have + result.cli.have + (result.config?.have ?? 0));
assert.ok(typeof result.generatedAt === "string");

View File

@@ -15,10 +15,10 @@ const agentSkillsConstants = await import("../../src/shared/constants/agentSkill
// ─── Counts ───────────────────────────────────────────────────────────────────
test("getCatalog() returns exactly 44 entries", () => {
test("getCatalog() returns exactly 45 entries", () => {
refreshCatalog();
const catalog = getCatalog();
assert.equal(catalog.length, 44, `Expected 44 but got ${catalog.length}`);
assert.equal(catalog.length, 45, `Expected 45 but got ${catalog.length}`);
});
test("API_SKILL_IDS has exactly 23 entries", () => {
@@ -26,7 +26,7 @@ test("API_SKILL_IDS has exactly 23 entries", () => {
});
test("CLI_SKILL_IDS has exactly 20 entries", () => {
assert.equal(CLI_SKILL_IDS.length, 20);
assert.equal(CLI_SKILL_IDS.length, 21);
});
test("getCatalog() contains exactly 22 api skills", () => {
@@ -34,9 +34,9 @@ test("getCatalog() contains exactly 22 api skills", () => {
assert.equal(apiSkills.length, 23);
});
test("getCatalog() contains exactly 20 cli skills", () => {
test("getCatalog() contains exactly 21 cli skills", () => {
const cliSkills = getCatalog().filter((s) => s.category === "cli");
assert.equal(cliSkills.length, 20);
assert.equal(cliSkills.length, 21);
});
// ─── ID format ────────────────────────────────────────────────────────────────
@@ -160,9 +160,9 @@ test("filterCatalog({ category: 'api' }) returns 23 api skills", () => {
}
});
test("filterCatalog({ category: 'cli' }) returns 20 cli skills", () => {
test("filterCatalog({ category: 'cli' }) returns 21 cli skills", () => {
const skills = filterCatalog({ category: "cli" });
assert.equal(skills.length, 20);
assert.equal(skills.length, 21);
for (const s of skills) {
assert.equal(s.category, "cli");
}
@@ -185,9 +185,9 @@ test("filterCatalog({ area: 'nonexistent' }) returns empty array", () => {
assert.equal(skills.length, 0);
});
test("filterCatalog({}) returns full catalog (44 entries)", () => {
test("filterCatalog({}) returns full catalog (45 entries)", () => {
const skills = filterCatalog({});
assert.equal(skills.length, 44);
assert.equal(skills.length, 45);
});
// ─── refreshCatalog ───────────────────────────────────────────────────────────
@@ -214,9 +214,9 @@ test("computeCoverage() returns valid SkillCoverage shape", () => {
assert.ok(cov.api.have >= 0 && cov.api.have <= 23);
assert.ok(typeof cov.cli === "object");
assert.equal(cov.cli.total, 20);
assert.equal(cov.cli.total, 21);
assert.ok(typeof cov.cli.have === "number");
assert.ok(cov.cli.have >= 0 && cov.cli.have <= 20);
assert.ok(cov.cli.have >= 0 && cov.cli.have <= 21);
assert.equal(cov.totalSkills, cov.api.have + cov.cli.have + (cov.config?.have ?? 0));
@@ -255,6 +255,6 @@ test("CLI_SKILL_IDS first entry is cli-serve", () => {
assert.equal(CLI_SKILL_IDS[0], "cli-serve");
});
test("CLI_SKILL_IDS last entry is cli-setup", () => {
assert.equal(CLI_SKILL_IDS[CLI_SKILL_IDS.length - 1], "cli-setup");
test("CLI_SKILL_IDS last entry is cli-skill-collector", () => {
assert.equal(CLI_SKILL_IDS[CLI_SKILL_IDS.length - 1], "cli-skill-collector");
});

View File

@@ -61,11 +61,11 @@ test("dry-run (default) returns report without writing any files", async () => {
outputDir: tmpDir,
});
// All 44 skills should appear as generated (would-write) since dir is empty
// All 45 skills should appear as generated (would-write) since dir is empty
assert.equal(
report.generated.length + report.unchanged.length,
44,
`Expected 44 total (generated+unchanged), got generated=${report.generated.length} unchanged=${report.unchanged.length}`,
45,
`Expected 45 total (generated+unchanged), got generated=${report.generated.length} unchanged=${report.unchanged.length}`,
);
assert.equal(report.errors.length, 0, `Unexpected errors: ${JSON.stringify(report.errors)}`);
@@ -81,7 +81,7 @@ test("dry-run (default) returns report without writing any files", async () => {
}
});
test("dry-run generates report with 44 total (generated+unchanged)", async () => {
test("dry-run generates report with 45 total (generated+unchanged)", async () => {
const tmpDir = mkTmpDir();
try {
refreshCatalog();
@@ -91,7 +91,7 @@ test("dry-run generates report with 44 total (generated+unchanged)", async () =>
outputDir: tmpDir,
});
const total = report.generated.length + report.unchanged.length;
assert.equal(total, 44);
assert.equal(total, 45);
} finally {
rmTmpDir(tmpDir);
}
@@ -134,7 +134,7 @@ test("apply mode writes SKILL.md with valid frontmatter for omni-providers", asy
}
});
test("apply mode writes all 44 SKILL.md files when no onlyIds filter", async () => {
test("apply mode writes all 45 SKILL.md files when no onlyIds filter", async () => {
const tmpDir = mkTmpDir();
try {
refreshCatalog();
@@ -145,7 +145,7 @@ test("apply mode writes all 44 SKILL.md files when no onlyIds filter", async ()
});
assert.equal(report.errors.length, 0, `Errors: ${JSON.stringify(report.errors)}`);
assert.equal(report.generated.length, 44);
assert.equal(report.generated.length, 45);
// Verify all dirs exist
const catalog = getCatalog();

View File

@@ -101,15 +101,15 @@ test.after(() => {
// GET /api/agent-skills
// ═════════════════════════════════════════════════════════════════════════════
test("GET /api/agent-skills — returns 44 skills with count and coverage", async () => {
test("GET /api/agent-skills — returns 45 skills with count and coverage", async () => {
const req = makeRequest("GET", "http://localhost/api/agent-skills");
const res = await listRoute.GET(req);
assert.equal(res.status, 200);
const body = (await res.json()) as { skills: unknown[]; count: number; coverage: unknown };
assert.equal(body.count, 44, `Expected 44 skills but got ${body.count}`);
assert.equal(body.count, 45, `Expected 45 skills but got ${body.count}`);
assert.equal(Array.isArray(body.skills), true);
assert.equal(body.skills.length, 44);
assert.equal(body.skills.length, 45);
assert.ok(body.coverage !== undefined, "coverage should be present");
});
@@ -123,13 +123,13 @@ test("GET /api/agent-skills?category=api — returns 23 api skills", async () =>
assert.ok(body.skills.every((s) => s.category === "api"), "All skills should be api category");
});
test("GET /api/agent-skills?category=cli — returns 20 cli skills", async () => {
test("GET /api/agent-skills?category=cli — returns 21 cli skills", async () => {
const req = makeRequest("GET", "http://localhost/api/agent-skills?category=cli");
const res = await listRoute.GET(req);
assert.equal(res.status, 200);
const body = (await res.json()) as { skills: Array<{ category: string }>; count: number };
assert.equal(body.count, 20);
assert.equal(body.count, 21);
assert.ok(body.skills.every((s) => s.category === "cli"), "All skills should be cli category");
});
@@ -268,7 +268,7 @@ test("GET /api/agent-skills/coverage — returns valid SkillCoverage shape", asy
};
assert.equal(body.api.total, 23, "api.total must be 23");
assert.equal(body.cli.total, 20, "cli.total must be 20");
assert.equal(body.cli.total, 21, "cli.total must be 21");
assert.ok(typeof body.totalSkills === "number", "totalSkills must be a number");
assert.ok(typeof body.generatedAt === "string", "generatedAt must be a string");
// generatedAt must be a valid ISO datetime

View File

@@ -0,0 +1,63 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { AntigravityExecutor } from "../../open-sse/executors/antigravity.ts";
import {
clearAntigravityVersionCache,
seedAntigravityVersionCache,
} from "../../open-sse/services/antigravityVersion.ts";
// Ports decolua/9router#2461: a non-ok (e.g. 403) Antigravity upstream response in the
// STREAMING path was piped straight through to the client via a raw pass-through
// TransformStream, with no `response.ok` check at all — unlike the non-streaming path,
// which already builds a sanitized error via buildAntigravityUpstreamError. When the
// upstream 403 body is gzip-compressed (or otherwise binary/non-UTF8), those raw bytes
// end up surfaced verbatim in the client-visible error message, corrupting it (reporters
// saw literal control-byte garbage after "[ERROR] [403]:").
test.afterEach(() => {
clearAntigravityVersionCache();
});
test("AntigravityExecutor.execute (stream=true) sanitizes a non-ok upstream body instead of piping raw bytes", async () => {
const executor = new AntigravityExecutor();
const originalFetch = globalThis.fetch;
seedAntigravityVersionCache("2026.04.17-test");
// Simulate a gzip-compressed 403 body (magic bytes 0x1f 0x8b), the exact shape
// reported upstream — reading it as text without decoding produces garbage.
const binaryBody = new Uint8Array([0x1f, 0x8b, 0x08, 0x00, 0x02, 0xff, 0x52, 0x41, 0x4e]);
globalThis.fetch = async () =>
new Response(binaryBody, {
status: 403,
headers: { "Content-Type": "application/json" },
});
try {
const result = await executor.execute({
model: "antigravity/gemini-2.5-flash",
body: { request: { contents: [] } },
stream: true,
credentials: { accessToken: "token", projectId: "project-1" },
log: { debug() {}, warn() {} },
});
assert.equal(result.response.status, 403);
const bodyText = await result.response.text();
// The raw gzip magic bytes must never reach the client-visible error text.
assert.ok(
!bodyText.includes("\x1f\x8b"),
`expected sanitized error body, got raw bytes leaking through: ${JSON.stringify(bodyText)}`
);
// Must be routed through buildErrorBody()/buildAntigravityUpstreamError() — a clean,
// parseable JSON error shape (hard rule #12), not an arbitrary pass-through stream.
const parsed = JSON.parse(bodyText) as { error?: { message?: string } };
assert.ok(parsed.error?.message, "expected a structured error.message");
assert.match(parsed.error.message, /Antigravity upstream error \(403\)/);
} finally {
globalThis.fetch = originalFetch;
}
});

View File

@@ -79,9 +79,8 @@ test("bifrost route: returns 503 + fallback header when BIFROST_BASE_URL is unse
delete process.env.BIFROST_STREAMING_ENABLED;
// Dynamic import after env is set so the module reads the empty value.
const { POST } = await import(
"../../../../src/app/api/v1/relay/chat/completions/bifrost/route.ts"
);
const { POST } =
await import("../../../../src/app/api/v1/relay/chat/completions/bifrost/route.ts");
const req = new Request("http://localhost/api/v1/relay/chat/completions/bifrost", {
method: "POST",
@@ -191,12 +190,14 @@ test("bifrost route: records relay usage after SSE stream completion", async ()
delete process.env.BIFROST_STREAMING_ENABLED;
const relayToken = seedRelayToken(`relay_bifrost_sse_${Date.now()}`);
let forwardedRequestId: string | null = null;
globalThis.fetch = async () =>
new Response(
globalThis.fetch = async (_input, init) => {
forwardedRequestId = new Headers(init?.headers).get("x-request-id");
return new Response(
new ReadableStream<Uint8Array>({
start(controller) {
controller.enqueue(new TextEncoder().encode("data: {\"delta\":\"hi\"}\n\n"));
controller.enqueue(new TextEncoder().encode('data: {"delta":"hi"}\n\n'));
controller.close();
},
}),
@@ -205,6 +206,7 @@ test("bifrost route: records relay usage after SSE stream completion", async ()
headers: { "content-type": "text/event-stream" },
}
);
};
const { POST } = await import(
`../../../../src/app/api/v1/relay/chat/completions/bifrost/route.ts?case=${Date.now()}-${Math.random()}`
@@ -227,6 +229,7 @@ test("bifrost route: records relay usage after SSE stream completion", async ()
const res = await POST(req);
assert.equal(res.status, 200);
assert.equal(res.headers.get("X-Routed-By"), "bifrost");
assert.equal(forwardedRequestId, "bifrost-sse-lifecycle-test");
assert.equal(getRelayLogs(relayToken.id, 10).length, 0);
assert.match(await res.text(), /delta/);

View File

@@ -1,8 +1,10 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import {
getBifrostRoutingConfig,
getRoutingFallbackHeader,
getRoutingFallbackReasonHeader,
resolveRelayRoutingBackend,
shouldTryBifrost,
shouldTryBifrostForRequest,
@@ -152,3 +154,52 @@ test("relay routing backend strict bifrost bypasses manifest eligibility", () =>
{ tryBifrost: true }
);
});
test("automatic relay keeps the Bifrost timeout active until an SSE stream finalizes", () => {
const routeSource = readFileSync(
new URL("../../../../src/app/api/v1/relay/chat/completions/route.ts", import.meta.url),
"utf8"
);
const forwardToBifrost = routeSource.slice(
routeSource.indexOf("async function forwardToBifrost"),
routeSource.indexOf("export async function OPTIONS")
);
const streamBranch = forwardToBifrost.slice(
forwardToBifrost.indexOf("if (wantsStream && upstream.body)"),
forwardToBifrost.indexOf("clearTimeout(tid);\n recordUsage(")
);
assert.match(
streamBranch,
/finalizeReadableStream\(upstream\.body, \(error\) => \{\s*clearTimeout\(tid\)/
);
assert.match(streamBranch, /const statusCode = timedOut \? 504 : upstream\.status/);
assert.match(streamBranch, /error && backend === "auto"/);
assert.match(streamBranch, /recordBifrostFailure\(/);
});
test("relay routing fallback reason header strips dynamic cooldown detail to the stable code", () => {
assert.equal(
getRoutingFallbackReasonHeader("bifrost-cooldown; remaining=1500"),
"bifrost-cooldown"
);
});
test("relay routing fallback reason header passes already-stable reasons through unchanged", () => {
assert.equal(getRoutingFallbackReasonHeader("bifrost-error"), "bifrost-error");
assert.equal(getRoutingFallbackReasonHeader("bifrost-ineligible"), "bifrost-ineligible");
assert.equal(
getRoutingFallbackReasonHeader("bifrost-provider-unknown"),
"bifrost-provider-unknown"
);
});
test("relay routing fallback reason header stays unset for the bare static legacy value", () => {
assert.equal(getRoutingFallbackReasonHeader("bifrost"), undefined);
});
test("relay routing fallback reason header stays unset for null/undefined/unrecognized input", () => {
assert.equal(getRoutingFallbackReasonHeader(null), undefined);
assert.equal(getRoutingFallbackReasonHeader(undefined), undefined);
assert.equal(getRoutingFallbackReasonHeader("something-unrecognized"), undefined);
});

View File

@@ -90,6 +90,48 @@ test("extractApiKey accepts Anthropic-Version (TitleCase) header", () => {
assert.equal(extractApiKey(req), "sk-titlecase-version");
});
test("extractApiKey returns the key from x-goog-api-key when Authorization and x-api-key are absent (#7034)", () => {
const req = makeRequest({ "x-goog-api-key": "sk-goog-native" });
assert.equal(extractApiKey(req), "sk-goog-native");
});
test("extractApiKey accepts uppercase X-Goog-Api-Key header casing (#7034)", () => {
const req = makeRequest({ "X-Goog-Api-Key": "sk-goog-uppercase" });
assert.equal(extractApiKey(req), "sk-goog-uppercase");
});
test("extractApiKey trims surrounding whitespace from x-goog-api-key value (#7034)", () => {
const req = makeRequest({ "x-goog-api-key": " sk-goog-padded " });
assert.equal(extractApiKey(req), "sk-goog-padded");
});
test("extractApiKey returns null when x-goog-api-key contains only whitespace (#7034)", () => {
const req = makeRequest({ "x-goog-api-key": " " });
assert.equal(extractApiKey(req), null);
});
test("extractApiKey prefers Bearer over x-goog-api-key when both are present (#7034)", () => {
const req = makeRequest({
Authorization: "Bearer sk-bearer-wins",
"x-goog-api-key": "sk-goog-loser",
});
assert.equal(extractApiKey(req), "sk-bearer-wins");
});
test("extractApiKey prefers x-api-key (with anthropic-version) over x-goog-api-key when both are present (#7034)", () => {
const req = makeRequest({
"x-api-key": "sk-anthropic-wins",
"x-goog-api-key": "sk-goog-loser",
...ANTHROPIC,
});
assert.equal(extractApiKey(req), "sk-anthropic-wins");
});
test("extractApiKey does not require anthropic-version for the x-goog-api-key fallback (#7034)", () => {
const req = makeRequest({ "x-goog-api-key": "sk-goog-no-version-needed" });
assert.equal(extractApiKey(req), "sk-goog-no-version-needed");
});
test("extractApiKey extracts a path-scoped token from /api/v1/vscode/<token>/...", () => {
const req = new Request("https://omniroute.test/api/v1/vscode/sk-test-path-token/models");
assert.equal(extractApiKey(req), "sk-test-path-token");

View File

@@ -237,6 +237,65 @@ test("clientApiPolicy: x-api-key header is accepted as client_api_key subject",
}
});
test("clientApiPolicy: x-goog-api-key header is accepted as client_api_key subject (#7034)", async () => {
const created = await apiKeysDb.createApiKey("policy-test-googkey", "machine-googkey-1234");
assert.ok(created?.key, "createApiKey must return a key");
const policy = await loadPolicy();
const headers = new Headers({ "x-goog-api-key": created.key });
const out = await policy.evaluate(ctx(headers));
assert.equal(out.allow, true);
if (out.allow) {
assert.equal(out.subject.kind, "client_api_key");
assert.match(out.subject.id, /^key_/);
}
});
test("clientApiPolicy: Authorization Bearer wins over x-goog-api-key when both present (#7034)", async () => {
const created = await apiKeysDb.createApiKey("policy-test-goog-precedence", "machine-goog-2345");
assert.ok(created?.key, "createApiKey must return a key");
const policy = await loadPolicy();
const headers = new Headers({
authorization: `Bearer ${created.key}`,
"x-goog-api-key": "sk-goog-should-lose",
});
const out = await policy.evaluate(ctx(headers));
assert.equal(out.allow, true);
if (out.allow) {
assert.equal(out.subject.kind, "client_api_key");
assert.match(out.subject.id, /^key_/);
}
});
test("clientApiPolicy: existing x-api-key still wins over x-goog-api-key when both present (#7034)", async () => {
const created = await apiKeysDb.createApiKey("policy-test-xkey-precedence", "machine-xkey-2345");
assert.ok(created?.key, "createApiKey must return a key");
const policy = await loadPolicy();
const headers = new Headers({
"x-api-key": created.key,
"x-goog-api-key": "sk-goog-should-lose",
});
const out = await policy.evaluate(ctx(headers));
assert.equal(out.allow, true);
if (out.allow) {
assert.equal(out.subject.kind, "client_api_key");
assert.match(out.subject.id, /^key_/);
}
});
test("clientApiPolicy: invalid x-goog-api-key is rejected with 401 AUTH_002 (#7034)", async () => {
const policy = await loadPolicy();
const headers = new Headers({ "x-goog-api-key": "sk-invalid-goog-key" });
const out = await policy.evaluate(ctx(headers));
assert.equal(out.allow, false);
if (!out.allow) {
assert.equal(out.status, 401);
assert.equal(out.code, "AUTH_002");
}
});
test("clientApiPolicy: ROUTER_API_KEY remains accepted for client API routes", async () => {
process.env.ROUTER_API_KEY = "sk-router-policy-test";

View File

@@ -0,0 +1,75 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { lptAssign, weightItems } from "../../scripts/quality/balance-e2e-shards.mjs";
// WS4.1 (v3.8.49 quality plan) — the E2E matrix skew was 14× (24m47s vs 1m47s)
// because Playwright --shard distributes by count, not duration. These tests pin
// the LPT packing invariants; the hard one is COMPLETENESS (a lost spec would
// silently hollow the suite — the CLI self-checks it and falls back to --shard).
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
test("lptAssign puts the heaviest item alone before doubling up lighter shards", () => {
const shards = lptAssign(
[
{ file: "huge.spec.ts", weight: 100 },
{ file: "a.spec.ts", weight: 30 },
{ file: "b.spec.ts", weight: 30 },
{ file: "c.spec.ts", weight: 30 },
],
2
);
assert.deepEqual(shards[0].files, ["huge.spec.ts"]);
assert.deepEqual(shards[1].files, ["a.spec.ts", "b.spec.ts", "c.spec.ts"]);
assert.equal(shards[0].total, 100);
assert.equal(shards[1].total, 90);
});
test("lptAssign is deterministic on equal weights (filename tiebreak)", () => {
const items = [
{ file: "b.spec.ts", weight: 10 },
{ file: "a.spec.ts", weight: 10 },
];
const s1 = lptAssign(items, 2);
const s2 = lptAssign([...items].reverse(), 2);
assert.deepEqual(s1, s2);
});
test("completeness: every file lands in exactly one shard", () => {
const items = Array.from({ length: 37 }, (_, i) => ({
file: `f${String(i).padStart(2, "0")}.spec.ts`,
weight: (i * 7) % 40,
}));
const shards = lptAssign(items, 9);
const union = shards.flatMap((s) => s.files).sort();
assert.deepEqual(union, items.map((i) => i.file).sort());
});
test("weightItems gives unknown/new specs the median weight, not an extreme", () => {
const items = weightItems(["new.spec.ts", "big.spec.ts", "small.spec.ts"], {
_meta: "x",
"big.spec.ts": 600,
"small.spec.ts": 20,
"other.spec.ts": 100,
});
const byFile = Object.fromEntries(items.map((i) => [i.file, i.weight]));
assert.equal(byFile["big.spec.ts"], 600);
assert.equal(byFile["small.spec.ts"], 20);
assert.equal(byFile["new.spec.ts"], 100); // median of [20,100,600]
});
test("the committed timings seed covers every current e2e spec (no drift)", () => {
const timings = JSON.parse(
fs.readFileSync(path.join(ROOT, "config", "quality", "e2e-timings.json"), "utf8")
);
const specs = fs
.readdirSync(path.join(ROOT, "tests", "e2e"))
.filter((f) => f.endsWith(".spec.ts"));
const missing = specs.filter((f) => !(f in timings));
// Missing entries are tolerated at runtime (median fallback) — this assert keeps
// the seed honest so balance quality does not silently rot as specs are added.
assert.deepEqual(missing, [], `add to config/quality/e2e-timings.json: ${missing.join(", ")}`);
});

View File

@@ -0,0 +1,68 @@
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 crypto from "node:crypto";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-7297-"));
process.env.DATA_DIR = TEST_DATA_DIR;
const core = await import("../../src/lib/db/core.ts");
const { protectPayloadForLog } = await import("../../src/lib/logPayloads.ts");
const bedrockExecutor = await import("../../open-sse/executors/bedrock.ts");
test.after(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
function bedrockConverseBodyWithImages(nImages: number, imageBytes: number) {
const content: unknown[] = [];
for (let i = 0; i < nImages; i++) {
const raw = crypto.randomBytes(imageBytes);
content.push({
type: "image_url",
image_url: { url: `data:image/png;base64,${raw.toString("base64")}` },
});
}
content.push({ type: "text", text: "describe these images" });
const chatBody = {
model: "us.anthropic.claude-opus-4-8",
messages: [{ role: "user", content }],
};
// Same call BedrockExecutor.execute() makes right before
// prl.captureCurrentProviderRequest(url, headers, transformedBody, ...).
return bedrockExecutor.openAIToBedrockConverse("us.anthropic.claude-opus-4-8", chatBody);
}
test("#7297 protectPayloadForLog stays fast on a 3-image Bedrock Converse body", () => {
const transformedBody = bedrockConverseBodyWithImages(3, 1_000_000);
const firstImageBlock = (
transformedBody as { messages: Array<{ content: Array<Record<string, unknown>> }> }
).messages[0].content[0] as { image?: { source?: { bytes?: unknown } } };
assert.ok(firstImageBlock.image?.source?.bytes instanceof Uint8Array);
const start = Date.now();
const result = protectPayloadForLog(transformedBody);
const elapsedMs = Date.now() - start;
assert.ok(
elapsedMs < 500,
`protectPayloadForLog took ${elapsedMs}ms for a 3-image request — it is walking every ` +
`decoded image byte as an object key instead of treating image.source.bytes as an ` +
`opaque buffer (see #7297)`
);
const redactedBytes = (
result as { messages: Array<{ content: Array<Record<string, unknown>> }> }
).messages[0].content[0] as { image?: { source?: { bytes?: unknown } } };
assert.ok(
!(redactedBytes.image?.source?.bytes instanceof Uint8Array) &&
!Array.isArray(redactedBytes.image?.source?.bytes),
"binary bytes must be replaced with an opaque placeholder, not expanded into per-byte keys"
);
});

View File

@@ -0,0 +1,93 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs/promises";
import fsSync from "node:fs";
import os from "node:os";
import path from "node:path";
const {
ensureWindowsBuildProfileDirs,
getWindowsBuildProfileDir,
resolveNextBuildEnv,
} = await import("../../scripts/build/build-next-isolated.mjs");
// Port of decolua/9router#2402 ("fix(build): isolate Windows HOME/AppData during
// next build"). Upstream wraps `npm run build` in a new `scripts/build-app.js`
// entrypoint; OmniRoute's build already routes through
// `scripts/build/build-next-isolated.mjs` → resolveNextBuildEnv(), so the fix is
// folded into that existing seam instead of adding a second build entrypoint.
// `.github/workflows/electron-release.yml` already sanitizes USERPROFILE for one
// CI job; this generalizes the isolation to every caller (local Windows builds,
// other CI paths) and adds APPDATA/LOCALAPPDATA, which the CI-only patch does not
// touch.
test("resolveNextBuildEnv leaves HOME/USERPROFILE/APPDATA untouched on non-Windows", () => {
const env = resolveNextBuildEnv({ NODE_ENV: "test", HOME: "/home/dev" }, "linux");
assert.equal(env.HOME, "/home/dev");
assert.equal(env.USERPROFILE, undefined);
assert.equal(env.APPDATA, undefined);
assert.equal(env.LOCALAPPDATA, undefined);
});
test("resolveNextBuildEnv isolates HOME/USERPROFILE/APPDATA/LOCALAPPDATA on win32", () => {
const env = resolveNextBuildEnv(
{ NODE_ENV: "test", USERPROFILE: "C:\\Users\\ci-runner" },
"win32"
);
assert.ok(env.HOME, "HOME must be set to an isolated profile dir on win32");
assert.equal(env.HOME, env.USERPROFILE, "HOME and USERPROFILE must point at the same sandbox");
assert.notEqual(
env.USERPROFILE,
"C:\\Users\\ci-runner",
"the real USERPROFILE (with its junctions) must be replaced, not preserved"
);
assert.match(path.basename(env.APPDATA), /^Roaming$/);
assert.match(path.basename(env.LOCALAPPDATA), /^Local$/);
assert.equal(path.dirname(path.dirname(env.APPDATA)), env.HOME);
assert.equal(path.dirname(path.dirname(env.LOCALAPPDATA)), env.HOME);
});
test("resolveNextBuildEnv skips Windows isolation when a caller already sandboxed the build (NEXT_DIST_DIR)", () => {
const env = resolveNextBuildEnv(
{ NODE_ENV: "test", USERPROFILE: "C:\\Users\\ci-runner", NEXT_DIST_DIR: ".build/cli-next" },
"win32"
);
assert.equal(
env.USERPROFILE,
"C:\\Users\\ci-runner",
"must not override a caller-provided sandbox (e.g. CLI packaging)"
);
assert.equal(env.APPDATA, undefined);
assert.equal(env.LOCALAPPDATA, undefined);
});
test("getWindowsBuildProfileDir is stable per-process (repeated calls return the same path)", () => {
assert.equal(getWindowsBuildProfileDir(), getWindowsBuildProfileDir());
});
test("ensureWindowsBuildProfileDirs is a no-op when the env has no APPDATA/LOCALAPPDATA", () => {
let called = false;
ensureWindowsBuildProfileDirs({ NODE_ENV: "test" }, () => {
called = true;
});
assert.equal(called, false);
});
test("ensureWindowsBuildProfileDirs creates the isolated AppData directories", async () => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "omniroute-win-home-test-"));
try {
const env = {
APPDATA: path.join(tempDir, "AppData", "Roaming"),
LOCALAPPDATA: path.join(tempDir, "AppData", "Local"),
};
ensureWindowsBuildProfileDirs(env);
assert.equal(fsSync.existsSync(env.APPDATA), true);
assert.equal(fsSync.existsSync(env.LOCALAPPDATA), true);
} finally {
await fs.rm(tempDir, { recursive: true, force: true });
}
});

View File

@@ -0,0 +1,111 @@
// tests/unit/build/check-dashboard-typecheck.test.ts
// Unit tests for the pure parsing/diff helpers in check-dashboard-typecheck.mjs.
// No child process is spawned — synthetic tsc-style output only, so the suite is
// fast and hermetic. Proves the gate actually DETECTS the #6625/#6909 bug class
// (an orphaned identifier — used but not declared — in a dashboard TSX file),
// not just that the script runs.
import test from "node:test";
import assert from "node:assert/strict";
import {
parseTscOutput,
diffAgainstBaseline,
} from "../../../scripts/check/check-dashboard-typecheck.mjs";
test("parseTscOutput: parses a TS2304 orphaned-identifier error (the #6625/#6909 bug class)", () => {
const raw =
`src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx(564,7): error TS2304: Cannot find name 'setPoolLoaded'.\n` +
`src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx(1204,12): error TS2304: Cannot find name 'poolLoaded'.\n`;
const counts = parseTscOutput(raw);
assert.deepEqual(counts, {
"src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx": {
TS2304: 2,
},
});
});
test("parseTscOutput: ignores non-error lines (summary/info output)", () => {
const raw =
`Some info line that is not an error\n` +
`src/app/(dashboard)/dashboard/foo.tsx(1,1): error TS2339: Property 'bar' does not exist.\n` +
`Found 1 error in 1 file.\n`;
const counts = parseTscOutput(raw);
assert.deepEqual(counts, {
"src/app/(dashboard)/dashboard/foo.tsx": { TS2339: 1 },
});
});
test("parseTscOutput: returns empty map for clean output", () => {
assert.deepEqual(parseTscOutput(""), {});
assert.deepEqual(parseTscOutput("Found 0 errors.\n"), {});
});
test("diffAgainstBaseline: flags a brand-new orphaned-identifier error as a regression", () => {
const baseline = {};
const live = {
"src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx": {
TS2304: 5,
},
};
const { regressions, improvements } = diffAgainstBaseline(live, baseline);
assert.equal(regressions.length, 1);
assert.equal(
regressions[0].file,
"src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx"
);
assert.equal(regressions[0].code, "TS2304");
assert.equal(regressions[0].liveCount, 5);
assert.equal(regressions[0].baselineCount, 0);
assert.equal(improvements.length, 0);
});
test("diffAgainstBaseline: does NOT flag a frozen pre-existing error within its baselined count", () => {
const baseline = { "src/app/(dashboard)/dashboard/foo.tsx": { TS2339: 3 } };
const live = { "src/app/(dashboard)/dashboard/foo.tsx": { TS2339: 3 } };
const { regressions, improvements } = diffAgainstBaseline(live, baseline);
assert.equal(regressions.length, 0);
assert.equal(improvements.length, 0);
});
test("diffAgainstBaseline: flags a count INCREASE beyond the frozen baseline as a regression", () => {
const baseline = { "src/app/(dashboard)/dashboard/foo.tsx": { TS2339: 2 } };
const live = { "src/app/(dashboard)/dashboard/foo.tsx": { TS2339: 3 } };
const { regressions } = diffAgainstBaseline(live, baseline);
assert.equal(regressions.length, 1);
assert.equal(regressions[0].baselineCount, 2);
assert.equal(regressions[0].liveCount, 3);
});
test("diffAgainstBaseline: reports (does not fail on) a count DECREASE as an improvement", () => {
const baseline = { "src/app/(dashboard)/dashboard/foo.tsx": { TS2339: 3 } };
const live = { "src/app/(dashboard)/dashboard/foo.tsx": { TS2339: 1 } };
const { regressions, improvements } = diffAgainstBaseline(live, baseline);
assert.equal(regressions.length, 0);
assert.equal(improvements.length, 1);
assert.equal(improvements[0].baselineCount, 3);
assert.equal(improvements[0].liveCount, 1);
});
test("diffAgainstBaseline: a baselined error that fully disappears is reported as an improvement, not a failure", () => {
const baseline = { "src/app/(dashboard)/dashboard/foo.tsx": { TS2339: 2 } };
const live = {};
const { regressions, improvements } = diffAgainstBaseline(live, baseline);
assert.equal(regressions.length, 0);
assert.equal(improvements.length, 1);
assert.equal(improvements[0].liveCount, 0);
assert.equal(improvements[0].baselineCount, 2);
});

View File

@@ -0,0 +1,174 @@
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";
type ConnectionRow = Record<string, unknown> & { id: string; name?: string | null };
// #2587 — bulk-add API keys must APPEND connections, never silently overwrite
// an existing one. `createProviderConnection` upserts apikey connections BY
// NAME (src/lib/db/providers.ts): same provider + auth_type "apikey" + same
// `name` updates the existing row (replacing its apiKey/priority/testStatus)
// instead of inserting a new one. Bulk-add auto-names unnamed lines
// "Key 1", "Key 2", ... starting fresh on every request, blind to names
// already saved for the provider — so re-running a bulk paste against a
// provider that already has "Key 1" silently replaced it instead of adding a
// new connection alongside it.
//
// The fix (POST /api/providers/bulk, src/app/api/providers/bulk/route.ts)
// fetches existing connection names for the provider and runs
// `resolveBulkNameCollisions` (src/shared/utils/bulkApiKeyParser.ts) before
// calling createProviderConnection, gap-filling a free "<name> <n>" suffix so
// every entry reaches createProviderConnection as a genuine insert. This test
// exercises that exact production sequence — getProviderConnections ->
// resolveBulkNameCollisions -> createProviderConnection — against a real
// SQLite-backed db module.
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-bulk-add-2587-"));
process.env.DATA_DIR = TEST_DATA_DIR;
const core = await import("../../src/lib/db/core.ts");
const providersDb = await import("../../src/lib/db/providers.ts");
const { resolveBulkNameCollisions } = await import("../../src/shared/utils/bulkApiKeyParser.ts");
async function resetStorage() {
core.resetDbInstance();
for (let attempt = 0; attempt < 10; attempt++) {
try {
if (fs.existsSync(TEST_DATA_DIR)) {
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
}
break;
} catch (error) {
const code = (error as { code?: string } | undefined)?.code;
if ((code === "EBUSY" || code === "EPERM") && attempt < 9) {
await new Promise((resolve) => setTimeout(resolve, 50 * (attempt + 1)));
} else {
throw error;
}
}
}
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}
test.beforeEach(async () => {
await resetStorage();
});
test.after(async () => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
test("bulk-add appends N+M connections and preserves the existing connection's state (the #2587 fix)", async () => {
// Existing connection carries live resilience state: an active rate-limit
// cooldown, a recorded error, and a non-default backoff level/priority.
const future = new Date(Date.now() + 60_000).toISOString();
const created = await providersDb.createProviderConnection({
provider: "openai",
authType: "apikey",
name: "Key 1",
apiKey: "sk-existing",
priority: 3,
testStatus: "unavailable",
lastError: "429 rate limited",
lastErrorType: "rate_limit",
rateLimitedUntil: future,
});
assert.ok(created, "existing connection must be created");
// `backoffLevel` is set by the resilience/cooldown path (not at creation
// time) — apply it the same way a real cooldown escalation would.
const existing = await providersDb.updateProviderConnection(created!.id as string, {
backoffLevel: 2,
});
assert.ok(existing, "existing connection must be updatable");
const before = await providersDb.getProviderConnections({ provider: "openai" });
assert.equal(before.length, 1);
// Simulates a fresh bulk-add paste (parseBulkApiKeys restarts auto-naming at
// "Key 1" every request) — this is exactly what collides with the existing
// connection above.
const rawEntries = [
{ name: "Key 1", apiKey: "sk-new-1" },
{ name: "Key 2", apiKey: "sk-new-2" },
];
// Reproduces the route's fix: fetch existing apikey names for the provider,
// then resolve collisions before ever calling createProviderConnection.
const existingApikeyConnections = (await providersDb.getProviderConnections({
provider: "openai",
authType: "apikey",
})) as ConnectionRow[];
const existingNames = existingApikeyConnections
.map((c) => (typeof c.name === "string" ? c.name : null))
.filter((n): n is string => !!n);
const resolvedEntries = resolveBulkNameCollisions(rawEntries, existingNames);
// Neither new entry may reuse the existing connection's name.
assert.ok(!resolvedEntries.some((e) => e.name === "Key 1"));
for (const entry of resolvedEntries) {
const created = await providersDb.createProviderConnection({
provider: "openai",
authType: "apikey",
name: entry.name,
apiKey: entry.apiKey,
priority: 1,
testStatus: "unknown",
});
assert.ok(created);
}
const after = (await providersDb.getProviderConnections({
provider: "openai",
})) as ConnectionRow[];
// N (1 existing) + M (2 new) = 3 connections — never 2 (an insert masquerading
// as an overwrite) or 1 (both new entries collapsing into the existing row).
assert.equal(after.length, before.length + rawEntries.length);
const survivor = after.find((c) => c.id === (existing as ConnectionRow).id);
assert.ok(survivor, "the pre-existing connection must still exist, unreplaced");
assert.equal(survivor!.name, "Key 1");
assert.equal(survivor!.apiKey, "sk-existing", "existing apiKey must not be overwritten");
assert.equal(survivor!.priority, 3, "existing priority must survive the bulk-add");
assert.equal(survivor!.testStatus, "unavailable", "existing testStatus must survive");
assert.equal(survivor!.lastError, "429 rate limited", "existing lastError must survive");
assert.equal(survivor!.rateLimitedUntil, future, "existing cooldown must survive");
assert.equal(survivor!.backoffLevel, 2, "existing backoffLevel must survive");
const newNames = after
.filter((c) => c.id !== (existing as ConnectionRow).id)
.map((c) => c.name);
assert.equal(new Set(newNames).size, newNames.length, "no duplicate names among new entries");
assert.ok(!newNames.includes("Key 1"));
});
test("without collision resolution, a colliding bulk entry silently replaces the existing connection (documents the pre-fix bug)", async () => {
// This is the exact bug reported upstream: createProviderConnection's
// name-based upsert is intentionally unchanged (other single-add/import
// flows depend on it) — the guard lives one layer up, in the bulk route.
// Skipping that guard reproduces the original data-loss behavior.
const existing = await providersDb.createProviderConnection({
provider: "anthropic",
authType: "apikey",
name: "Key 1",
apiKey: "sk-existing",
});
const collided = await providersDb.createProviderConnection({
provider: "anthropic",
authType: "apikey",
name: "Key 1", // same name, no collision resolution applied
apiKey: "sk-overwritten",
});
assert.equal(collided!.id, existing!.id, "same name upserts into the same row");
const rows = (await providersDb.getProviderConnections({
provider: "anthropic",
})) as ConnectionRow[];
assert.equal(rows.length, 1, "no new connection was inserted — this is the bug");
assert.equal(rows[0].apiKey, "sk-overwritten", "the original key was overwritten");
});

View File

@@ -2,6 +2,7 @@ import test from "node:test";
import assert from "node:assert/strict";
import {
parseBulkApiKeys,
resolveBulkNameCollisions,
BULK_API_KEY_MAX_LINES,
} from "../../src/shared/utils/bulkApiKeyParser.ts";
@@ -87,3 +88,66 @@ test("input exceeding cap is truncated with warning", () => {
assert.equal(warnings.length, 1);
assert.match(warnings[0], /only the first/);
});
// #2587 — resolveBulkNameCollisions: guards against createProviderConnection's
// name-based upsert silently replacing an existing connection when bulk-add
// auto-naming ("Key 1", "Key 2", ...) restarts from 1 and collides with a
// name already saved for the provider.
test("resolveBulkNameCollisions: no-op when nothing collides", () => {
const entries = [{ name: "Key 1" }, { name: "Key 2" }];
const out = resolveBulkNameCollisions(entries, ["Prod"]);
assert.deepEqual(
out.map((e) => e.name),
["Key 1", "Key 2"]
);
});
test("resolveBulkNameCollisions: renames an auto-named entry colliding with an existing connection", () => {
// Provider already has "Key 1" saved; a fresh bulk parse restarts auto-naming
// at "Key 1" too. Without renaming, createProviderConnection would upsert
// into the existing row instead of inserting.
const entries = [{ name: "Key 1" }, { name: "Key 2" }];
const out = resolveBulkNameCollisions(entries, ["Key 1"]);
assert.deepEqual(
out.map((e) => e.name),
["Key 2", "Key 3"]
);
// Never reuses an existing name.
assert.ok(!out.some((e) => e.name === "Key 1"));
});
test("resolveBulkNameCollisions: renames a custom name colliding with an existing connection", () => {
const entries = [{ name: "Prod" }];
const out = resolveBulkNameCollisions(entries, ["Prod"]);
assert.deepEqual(out.map((e) => e.name), ["Prod 1"]);
});
test("resolveBulkNameCollisions: dedupes two identical custom names within the same batch", () => {
// Two "Prod|apiKey" lines pasted in the same batch previously collided with
// EACH OTHER (custom names get no auto-index), so the second entry would
// upsert into the first instead of inserting a second connection.
const entries = [{ name: "Prod" }, { name: "Prod" }];
const out = resolveBulkNameCollisions(entries, []);
const names = out.map((e) => e.name);
assert.equal(new Set(names).size, names.length);
assert.deepEqual(names, ["Prod", "Prod 1"]);
});
test("resolveBulkNameCollisions: preserves non-name fields on renamed entries", () => {
const entries = [{ name: "Key 1", apiKey: "sk-new" }];
const out = resolveBulkNameCollisions(entries, ["Key 1"]);
assert.equal(out[0].apiKey, "sk-new");
assert.equal(out[0].name, "Key 2");
});
test("resolveBulkNameCollisions: name comparison is case-insensitive", () => {
const entries = [{ name: "key 1" }];
const out = resolveBulkNameCollisions(entries, ["KEY 1"]);
assert.equal(out[0].name, "key 2");
});
test("resolveBulkNameCollisions: tolerates non-array existingNames", () => {
const entries = [{ name: "Key 1" }];
const out = resolveBulkNameCollisions(entries, null);
assert.equal(out[0].name, "Key 1");
});

View File

@@ -0,0 +1,74 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { mergeChunksToResponse } from "../../open-sse/utils/bypassResponse.ts";
/**
* Regression guard for the Claude-format non-streaming bypass response bug:
* mergeChunksToResponse() used to return `messageStart.message` as-is, which
* the openai-to-claude translator always initializes with `content: []` —
* the actual text only exists in the separate content_block_start/delta
* events. A synthetic (non-streaming) Claude-format bypass response
* therefore always came back with an empty `content` array, silently
* dropping the bypass text ("CLI Command Execution: Clear Terminal", etc.)
* from every Claude-format client (e.g. the Claude Code CLI).
*/
describe("mergeChunksToResponse (Claude format content reconstruction)", () => {
const chunks = [
{
type: "message_start",
message: {
id: "msg_1",
type: "message",
role: "assistant",
model: "demo",
content: [],
stop_reason: null,
stop_sequence: null,
usage: { input_tokens: 1, cache_read_input_tokens: 2 },
},
},
{ type: "content_block_start", index: 0, content_block: { type: "text", text: "" } },
{ type: "content_block_delta", index: 0, delta: { type: "text_delta", text: "hello world" } },
{ type: "content_block_stop", index: 0 },
{
type: "message_delta",
delta: { stop_reason: "end_turn", stop_sequence: null },
usage: { output_tokens: 3 },
},
{ type: "message_stop" },
];
it("reconstructs the message content from content_block_start/delta chunks", () => {
const result = mergeChunksToResponse(chunks, "claude") as Record<string, unknown>;
assert.equal(result.type, "message");
assert.equal(result.role, "assistant");
assert.deepEqual(result.content, [{ type: "text", text: "hello world" }]);
});
it("merges start + delta usage and carries the final stop_reason", () => {
const result = mergeChunksToResponse(chunks, "claude") as Record<string, unknown>;
assert.equal(result.stop_reason, "end_turn");
assert.deepEqual(result.usage, {
input_tokens: 1,
cache_read_input_tokens: 2,
output_tokens: 3,
});
});
it("falls back to the last chunk untouched for non-Claude formats", () => {
const openaiChunks = [{ type: "chat.completion.chunk", choices: [] }];
assert.equal(mergeChunksToResponse(openaiChunks, "openai"), openaiChunks[0]);
});
it("falls back to a canned unknown response for an empty chunk list", () => {
const result = mergeChunksToResponse([], "claude") as {
model: string;
choices: Array<{ message: { role: string } }>;
};
assert.equal(result.model, "unknown");
assert.equal(result.choices[0].message.role, "assistant");
});
});

View File

@@ -274,7 +274,10 @@ test("chat completions route emits early keepalive while waiting for stream read
assert.match(response.headers.get("content-type") || "", /text\/event-stream/);
const body = await readAll(response);
assert.match(body, /: omniroute-keepalive/);
assert.match(
body,
/data: \{"id":"omniroute-keepalive","object":"chat\.completion\.chunk"/
);
assert.match(body, /OK/);
assert.match(body, /\[DONE\]/);
});

View File

@@ -0,0 +1,227 @@
import test from "node:test";
import assert from "node:assert/strict";
import { EventEmitter } from "node:events";
const { ChatGptWebExecutor, __resetChatGptWebCachesForTesting } = await import(
"../../open-sse/executors/chatgpt-web.ts"
);
const { __setTlsFetchOverrideForTesting } = await import(
"../../open-sse/services/chatgptTlsClient.ts"
);
function makeHeaders(map: Record<string, string> = {}) {
const h = new Headers();
for (const [k, v] of Object.entries(map)) h.set(k, String(v));
return h;
}
const CONVERSATION_ID = "conv-async-7357";
const FINAL_POINTER = "file-service://file-final-7357";
// SSE stream: assistant starts, tool kicks off image_gen (the "Processing
// image..." card via metadata.image_gen_task_id), stream ends WITHOUT any
// resolved image_asset_pointer — the real async case where the image only
// shows up later, over the celsius WebSocket.
function asyncImageGenSseText(): string {
const events = [
{
conversation_id: CONVERSATION_ID,
message: {
id: "msg-1",
author: { role: "assistant" },
content: { content_type: "text", parts: ["Generating your image..."] },
status: "in_progress",
},
},
{
conversation_id: CONVERSATION_ID,
message: {
id: "tool-1",
author: { role: "tool", name: "t2uay3k.sj1i4kz" },
metadata: { image_gen_task_id: "task-7357" },
content: { content_type: "text", parts: [] },
},
},
];
const chunks = events.map((e) => `data: ${JSON.stringify(e)}\r\n\r\n`);
chunks.push("data: [DONE]\r\n\r\n");
return chunks.join("");
}
// Fake global WebSocket: opens, then emits ONE frame shaped like chatgpt.com's
// celsius wire format for the PLURAL case — payload.update_content.messages[]
// — carrying the completed tool-role image_asset_pointer message. This is the
// shape issue #7357 reports chatgpt.com sends and the current parser does not
// recognize (it only reads update_content.message, singular).
class FakeWebSocket extends EventEmitter {
url: string;
onopen: (() => void) | null = null;
onmessage: ((ev: { data: string }) => void) | null = null;
onerror: ((ev: unknown) => void) | null = null;
onclose: (() => void) | null = null;
static instances: FakeWebSocket[] = [];
constructor(url: string) {
super();
this.url = url;
FakeWebSocket.instances.push(this);
setTimeout(() => {
this.onopen?.();
setTimeout(() => {
const frame = {
type: "conversation-update",
payload: {
conversation_id: CONVERSATION_ID,
update_content: {
messages: [
{
message: {
id: "img-msg-final",
author: { role: "tool", name: "t2uay3k.sj1i4kz" },
content: {
content_type: "multimodal_text",
parts: [
{
content_type: "image_asset_pointer",
asset_pointer: FINAL_POINTER,
width: 1024,
height: 1024,
},
],
},
status: "finished_successfully",
},
},
],
},
},
};
this.onmessage?.({ data: JSON.stringify(frame) });
}, 5);
}, 5);
}
close() {}
}
test("#7357: async image_gen pointer delivered via update_content.messages[] should resolve to markdown (currently lost → 502)", async () => {
__resetChatGptWebCachesForTesting();
const previousWebSocket = (globalThis as Record<string, unknown>).WebSocket;
const previousTimeout = process.env.OMNIROUTE_CGPT_WEB_IMAGE_TIMEOUT_MS;
process.env.OMNIROUTE_CGPT_WEB_IMAGE_TIMEOUT_MS = "300"; // keep the probe fast
(globalThis as Record<string, unknown>).WebSocket = FakeWebSocket;
__setTlsFetchOverrideForTesting(async (url, opts = {}) => {
const u = String(url);
const method = opts.method || "GET";
if ((u === "https://chatgpt.com/" || u === "https://chatgpt.com") && method === "GET") {
return {
status: 200,
headers: makeHeaders({ "Content-Type": "text/html" }),
text: '<html data-build="prod-test"><script src="https://cdn.oaistatic.com/main.js"></script></html>',
body: null,
};
}
if (u.includes("/api/auth/session")) {
return {
status: 200,
headers: makeHeaders({ "Content-Type": "application/json" }),
text: JSON.stringify({
accessToken: "jwt-7357",
expires: new Date(Date.now() + 3600_000).toISOString(),
user: { id: "u-7357" },
}),
body: null,
};
}
if (u.includes("/backend-api/sentinel/chat-requirements")) {
return {
status: 200,
headers: makeHeaders({ "Content-Type": "application/json" }),
text: JSON.stringify({ token: "t", proofofwork: { required: false } }),
body: null,
};
}
if (u.endsWith("/backend-api/f/conversation") || u.endsWith("/backend-api/conversation")) {
return {
status: 200,
headers: makeHeaders({ "Content-Type": "text/event-stream" }),
text: asyncImageGenSseText(),
body: null,
};
}
if (u.includes("/backend-api/celsius/ws/user")) {
return {
status: 200,
headers: makeHeaders({ "Content-Type": "application/json" }),
text: JSON.stringify({ websocket_url: "wss://chatgpt.com/fake-celsius-socket" }),
body: null,
};
}
// Resolution path for FINAL_POINTER, exercised ONLY if the WS listener
// actually extracts the pointer from the update_content.messages[] frame.
if (u.match(/\/backend-api\/files\/[^/]+\/download/)) {
return {
status: 200,
headers: makeHeaders({ "Content-Type": "application/json" }),
text: JSON.stringify({
download_url: "https://chatgpt.com/backend-api/estuary/content?id=file-final-7357",
}),
body: null,
};
}
if (u.startsWith("https://chatgpt.com/backend-api/estuary/content")) {
const pngBytes = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
return {
status: 200,
headers: makeHeaders({ "Content-Type": "image/png" }),
text: `data:image/png;base64,${pngBytes.toString("base64")}`,
body: null,
};
}
return { status: 404, headers: makeHeaders(), text: "not mocked", body: null };
});
try {
const executor = new ChatGptWebExecutor();
const result = await executor.execute({
model: "gpt-5.5",
body: { messages: [{ role: "user", content: "generate an image of a kitten" }] },
stream: false,
credentials: { apiKey: "test-session-cookie" },
signal: AbortSignal.timeout(20_000),
log: null,
});
assert.equal(result.response.status, 200, "executor itself does not error");
const json = await result.response.json();
const content = String(json?.choices?.[0]?.message?.content || "");
assert.ok(FakeWebSocket.instances.length >= 1, "a WebSocket connection was opened");
// Expected/correct behavior: the celsius WebSocket delivered a complete,
// well-formed tool-role image_asset_pointer message via chatgpt.com's
// update_content.messages[] (plural) shape. OmniRoute should extract it,
// resolve it, and append image markdown — just like the already-covered
// update_content.message (singular) case in tests/unit/chatgpt-web.test.ts.
assert.match(
content,
/!\[image\]\([^)]*\/v1\/chatgpt-web\/image\/[a-f0-9]+\)/,
"BUG #7357: image pointer delivered via update_content.messages[] (plural) was not " +
"resolved into markdown — waitForImageViaWebSocket() only recognizes the singular " +
"update_content.message / payload.message / data.message shapes and silently drops " +
"this frame, losing an already-completed upstream image."
);
assert.equal(
json.x_image_resolution_failed,
undefined,
"resolution succeeded — no unresolved-pointer flag expected"
);
} finally {
__setTlsFetchOverrideForTesting(null);
if (previousWebSocket === undefined) delete (globalThis as Record<string, unknown>).WebSocket;
else (globalThis as Record<string, unknown>).WebSocket = previousWebSocket;
if (previousTimeout === undefined) delete process.env.OMNIROUTE_CGPT_WEB_IMAGE_TIMEOUT_MS;
else process.env.OMNIROUTE_CGPT_WEB_IMAGE_TIMEOUT_MS = previousTimeout;
}
});

View File

@@ -0,0 +1,57 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { pickTarball, evaluateBoot, pickPort } from "../../scripts/check/check-pack-boot.mjs";
// WS1.2 (T1, v3.8.49 quality plan) — pure-function guards for the tarball boot-smoke
// gate that kills the #7065 class (published artifact crashes on every boot because a
// packaging list drifted; 3rd recurrence). The end-to-end path runs in CI's
// package-artifact job; these tests pin the decision logic.
const SCRIPT_PATH = path.join(
path.dirname(fileURLToPath(import.meta.url)),
"../../scripts/check/check-pack-boot.mjs"
);
test("pickTarball extracts the filename from npm pack --json output", () => {
assert.equal(pickTarball('[{"filename":"omniroute-3.8.49.tgz","size":1}]'), "omniroute-3.8.49.tgz");
});
test("pickTarball normalizes scoped slashes to the on-disk dash form", () => {
assert.equal(pickTarball('[{"filename":"@scope/pkg-1.0.0.tgz"}]'), "@scope-pkg-1.0.0.tgz");
});
test("pickTarball throws on empty/odd npm output instead of booting garbage", () => {
assert.throws(() => pickTarball("[]"));
assert.throws(() => pickTarball("{}"));
});
test("evaluateBoot passes on HTTP 200 + matching version, whatever the health status", () => {
const r = evaluateBoot(200, { version: "3.8.49", status: "warning" }, "3.8.49");
assert.equal(r.ok, true);
assert.deepEqual(r.failures, []);
});
test("evaluateBoot fails on non-200, non-JSON body, and version mismatch", () => {
assert.equal(evaluateBoot(503, { version: "3.8.49" }, "3.8.49").ok, false);
assert.equal(evaluateBoot(200, null, "3.8.49").ok, false);
const wrong = evaluateBoot(200, { version: "3.8.48" }, "3.8.49");
assert.equal(wrong.ok, false);
assert.match(wrong.failures[0], /3\.8\.48/);
});
test("pickPort stays inside the reserved smoke range for any pid", () => {
for (const seed of [0, 1, 4000, 65535, 123456]) {
const p = pickPort(seed);
assert.ok(p >= 23000 && p < 27000, `port ${p} out of range for seed ${seed}`);
}
});
test("source guard: the gate polls the real health endpoint of the INSTALLED binary", () => {
const src = readFileSync(SCRIPT_PATH, "utf8");
assert.ok(src.includes('"install", "-g", "--prefix"'), "must install the packed tarball into a clean prefix");
assert.ok(src.includes("/api/monitoring/health"), "must poll the health endpoint");
assert.ok(src.indexOf("npm") < src.indexOf("spawn"), "pack+install must precede the boot spawn");
});

View File

@@ -14,12 +14,14 @@
* (`if (file.endsWith("check-test-masking.test.ts")) continue;` in
* scripts/check/check-test-masking.mjs) for precisely this reason — this test
* asserts evaluateMasking() now applies the same exclusion for its diff-based
* tautology counters, using the real base(origin/main)/head(HEAD) diff of
* tautology counters, against the REAL current source of
* tests/unit/check-test-masking.test.ts.
*/
import test from "node:test";
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import {
countTautologies,
@@ -28,16 +30,17 @@ import {
} from "../../scripts/check/check-test-masking.mjs";
const FILE = "tests/unit/check-test-masking.test.ts";
function git(args: string[]): string {
return execFileSync("git", args, { encoding: "utf8" });
}
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
test("#6634: check-test-masking.test.ts's own tautology fixtures must not self-flag as weakening", () => {
// origin/main predates the #6404 fixtures (countBareTautologies/scanBareTautologies
// tests) that legitimately embed tautology-pattern literals as string fixtures.
const baseSrc = git(["show", "origin/main:" + FILE]);
const headSrc = git(["show", "HEAD:" + FILE]);
// Read the REAL current source from disk rather than a git ref: the Unit Tests
// job checks out a shallow/single-ref tree with no origin/main, so `git show
// origin/main:<file>` failed the shard before it ever exercised the masking
// behavior under test. An empty base models the file's pre-#6404 state (no
// fixtures), which maximizes headTaut - baseTaut — the strictest input for the
// exclusion this test asserts.
const baseSrc = "";
const headSrc = fs.readFileSync(path.join(REPO_ROOT, FILE), "utf8");
const perFile = [
{

View File

@@ -0,0 +1,49 @@
import test from "node:test";
import assert from "node:assert/strict";
const { APIKEY_PROVIDERS } = await import("../../src/shared/constants/providers.ts");
const { PROVIDER_ENDPOINTS } = await import("../../src/shared/constants/config.ts");
const { REGISTRY: providerRegistry } = await import("../../open-sse/config/providerRegistry.ts");
const CHENZK_CHAT_URL = "https://chenzk.top/v1/chat/completions";
const CHENZK_MODELS_URL = "https://chenzk.top/v1/models";
// Port of decolua/9router#2437 ("feat: add Chenzk API provider"), adapted to
// OmniRoute's directory-per-provider registry (`open-sse/config/providers/registry/`)
// and the `src/shared/constants/providers/apikey/*` metadata catalog, instead of
// upstream's flat `open-sse/providers/registry/*.js` + hardcoded model array. Chenzk
// exposes a "New API"-style OpenAI-compatible gateway with a live /v1/models catalog,
// so — matching the sibling kenari/x5lab/sumopod gateways already in this catalog —
// models are resolved via passthrough rather than a speculative hardcoded list.
test("Chenzk is registered as an OpenAI-compatible API-key gateway", () => {
const entry = APIKEY_PROVIDERS.chenzk;
assert.ok(entry, "APIKEY_PROVIDERS.chenzk must be defined");
assert.equal(entry.id, "chenzk");
assert.equal(entry.alias, "chenzk");
assert.equal(entry.name, "Chenzk API");
assert.equal(entry.website, "https://chenzk.top");
assert.equal(entry.passthroughModels, true);
});
test("Chenzk exposes the OpenAI-compatible chat completions endpoint", () => {
assert.equal(PROVIDER_ENDPOINTS.chenzk, CHENZK_CHAT_URL);
});
test("Chenzk registry entry uses OpenAI format with bearer API-key auth and passthrough models", () => {
const entry = providerRegistry.chenzk;
assert.ok(entry, "providerRegistry.chenzk must be defined");
assert.equal(entry.id, "chenzk");
assert.equal(entry.alias, "chenzk");
assert.equal(entry.format, "openai");
assert.equal(entry.executor, "default");
assert.equal(entry.authType, "apikey");
assert.equal(entry.authHeader, "bearer");
assert.equal(entry.baseUrl, CHENZK_CHAT_URL);
assert.equal(entry.modelsUrl, CHENZK_MODELS_URL);
assert.equal(entry.passthroughModels, true);
assert.deepEqual(
entry.models,
[],
"Chenzk ships no speculative seeded models — live catalog via passthrough only"
);
});

View File

@@ -15,7 +15,7 @@ import { classifyPaths } from "../../scripts/quality/classify-pr-changes.mjs";
test("pure docs PR → docs only (no code unit/lint bag)", () => {
const c = classifyPaths(["docs/architecture/QUALITY_GATES.md", "README.md"]);
assert.deepEqual(c, { code: false, docs: true, i18n: false, workflow: false });
assert.deepEqual(c, { code: false, docs: true, i18n: false, workflow: false, testsOnly: false });
});
test("openapi under docs/ → docs (contract gates live in docs-sync, not unit)", () => {
@@ -26,7 +26,7 @@ test("openapi under docs/ → docs (contract gates live in docs-sync, not unit)"
test("pure message catalog → i18n only (not full unit suite)", () => {
const c = classifyPaths(["src/i18n/messages/en.json", "src/i18n/messages/ko.json"]);
assert.deepEqual(c, { code: false, docs: false, i18n: true, workflow: false });
assert.deepEqual(c, { code: false, docs: false, i18n: true, workflow: false, testsOnly: false });
});
test("i18n tooling/scripts → i18n + code (tooling can break runtime paths)", () => {
@@ -49,7 +49,7 @@ test("workflow change → workflow + code (gates protect the gates)", () => {
test("production source → code", () => {
const c = classifyPaths(["open-sse/handlers/chatCore.ts", "src/lib/db/core.ts"]);
assert.deepEqual(c, { code: true, docs: false, i18n: false, workflow: false });
assert.deepEqual(c, { code: true, docs: false, i18n: false, workflow: false, testsOnly: false });
});
test("mixed docs + code → both flags (jobs union their filters)", () => {
@@ -65,5 +65,31 @@ test("unknown path → code fail-safe (never skip heavy gates by accident)", ()
test("empty change list → all false (nothing to validate)", () => {
const c = classifyPaths([]);
assert.deepEqual(c, { code: false, docs: false, i18n: false, workflow: false });
assert.deepEqual(c, { code: false, docs: false, i18n: false, workflow: false, testsOnly: false });
});
// WS3.1 (v3.8.49 quality plan) — testsOnly powers the hotfix/test-only fast lane:
// a diff touching ONLY tests/ (and no tests/e2e/ spec) does not change the served
// app, so the 9-shard E2E matrix adds wall-time without coverage. e2e specs are
// excluded from the shortcut — changing an e2e spec REQUIRES running e2e.
test("testsOnly: pure unit-test diff → true (still code)", () => {
const c = classifyPaths(["tests/unit/foo.test.ts", "tests/integration/bar.test.ts"]);
assert.equal(c.testsOnly, true);
assert.equal(c.code, true);
});
test("testsOnly: any non-test file flips it false", () => {
const c = classifyPaths(["tests/unit/foo.test.ts", "src/lib/db/core.ts"]);
assert.equal(c.testsOnly, false);
});
test("testsOnly: touching an e2e spec is NOT tests-only (e2e must run)", () => {
const c = classifyPaths(["tests/e2e/login.spec.ts"]);
assert.equal(c.testsOnly, false);
});
test("testsOnly: empty change list → false (fail-safe)", () => {
const c = classifyPaths([]);
assert.equal(c.testsOnly, false);
});

View File

@@ -0,0 +1,103 @@
/**
* Tests for #6954 — mid-conversation system turns misattributed as assistant.
*
* `convertClaudeMessage` mapped any role that wasn't "user" or "tool" to
* "assistant", so a Claude message with `role: "system"` (e.g. an injected
* system reminder mid-conversation) was forwarded to OpenAI-format upstreams
* as an assistant turn — polluting the conversation history.
*/
import test from "node:test";
import assert from "node:assert/strict";
const { claudeToOpenAIRequest } =
await import("../../open-sse/translator/request/claude-to-openai.ts");
// ---------------------------------------------------------------------------
// 1. system message mid-conversation keeps role: "system"
// ---------------------------------------------------------------------------
test("mid-conversation system message preserves role:system (not assistant)", () => {
const result = claudeToOpenAIRequest(
"gpt-4o",
{
messages: [
{ role: "user", content: "hello" },
{ role: "assistant", content: "hi" },
{ role: "system", content: "Reminder: be concise." },
{ role: "user", content: "ok" },
],
},
false
);
const roles = result.messages.map((m: { role: string }) => m.role);
assert.deepEqual(roles, ["user", "assistant", "system", "user"]);
});
// ---------------------------------------------------------------------------
// 2. system message with array content keeps role: "system"
// ---------------------------------------------------------------------------
test("system message with array content preserves role:system", () => {
const result = claudeToOpenAIRequest(
"gpt-4o",
{
messages: [
{ role: "user", content: "hello" },
{
role: "system",
content: [{ type: "text", text: "System reminder text" }],
},
],
},
false
);
const sysMsg = result.messages.find((m: { role: string }) => m.role === "system");
assert.ok(sysMsg, "expected a system message in output");
// Array content with text blocks is flattened to a string for system role
assert.equal(
typeof sysMsg.content === "string" ? sysMsg.content : JSON.stringify(sysMsg.content),
"System reminder text"
);
});
// ---------------------------------------------------------------------------
// 3. top-level body.system still produces role: "system" (regression check)
// ---------------------------------------------------------------------------
test("body.system still produces role:system at index 0", () => {
const result = claudeToOpenAIRequest(
"gpt-4o",
{
system: "You are helpful.",
messages: [{ role: "user", content: "hi" }],
},
false
);
assert.equal(result.messages[0].role, "system");
assert.equal(result.messages[1].role, "user");
});
// ---------------------------------------------------------------------------
// 4. assistant with tool_use still maps to assistant (regression check)
// ---------------------------------------------------------------------------
test("assistant role still maps to assistant (no regression)", () => {
const result = claudeToOpenAIRequest(
"gpt-4o",
{
messages: [
{ role: "user", content: "use the tool" },
{
role: "assistant",
content: [
{ type: "text", text: "calling tool" },
{ type: "tool_use", id: "t1", name: "foo", input: {} },
],
},
],
},
false
);
const roles = result.messages.map((m: { role: string }) => m.role);
assert.ok(roles.includes("assistant"), "assistant role must be preserved");
});

View File

@@ -30,7 +30,7 @@ test(`CLI_TOOLS has exactly ${EXPECTED_AGENT_COUNT} agent entries`, () => {
);
});
test("CLI_TOOLS total code entries (including none) equals 24 (20 visible + 4 none)", () => {
test("CLI_TOOLS total code entries (including none) equals 25 (21 visible + 4 none)", () => {
// code-none entries: antigravity, kiro, cursor (app), hermes (simple guide)
const codeNone = codeAll.filter((t) => t.baseUrlSupport === "none");
assert.equal(
@@ -38,11 +38,11 @@ test("CLI_TOOLS total code entries (including none) equals 24 (20 visible + 4 no
4,
`Expected 4 code entries with baseUrlSupport='none', got ${codeNone.length}: ${codeNone.map((t) => t.id).join(", ")}`
);
assert.equal(codeAll.length, 24, `Expected 24 total code entries, got ${codeAll.length}`);
assert.equal(codeAll.length, 25, `Expected 25 total code entries, got ${codeAll.length}`);
});
test("CLI_TOOLS total (code + agent) = 32", () => {
assert.equal(all.length, 32, `Expected 32 total entries, got ${all.length}`);
test("CLI_TOOLS total (code + agent) = 33", () => {
assert.equal(all.length, 33, `Expected 33 total entries, got ${all.length}`);
});
test("All code-none entries have configType mitm OR are legacy excluded entries", () => {
@@ -66,7 +66,7 @@ test("All agent entries have baseUrlSupport 'full' or 'partial' (no agent is 'no
}
});
test("The 20 visible code entries match D15 list exactly (+ crush + codewhale)", () => {
test("The 21 visible code entries match D15 list exactly (+ crush + codewhale + grok-build)", () => {
const d15List = new Set([
"claude",
"codex",
@@ -88,6 +88,7 @@ test("The 20 visible code entries match D15 list exactly (+ crush + codewhale)",
"pi",
"custom",
"crush",
"grok-build",
]);
const visibleIds = new Set(codeVisible.map((t) => t.id));
for (const id of d15List) {

View File

@@ -0,0 +1,75 @@
import test from "node:test";
import assert from "node:assert/strict";
/**
* Replicate the parsePort + port resolution logic from bin/cli/commands/dashboard.mjs
* to verify that PORT env var is respected when --port is not passed (mirrors
* tests/unit/cli-serve-port.test.ts's convention for serve.mjs).
*/
function parsePort(value: string | undefined, fallback: number): number {
const parsed = parseInt(String(value), 10);
return Number.isFinite(parsed) && parsed > 0 && parsed <= 65535 ? parsed : fallback;
}
function resolvePort(optsPort: string | undefined, envPort: string | undefined): number {
return parsePort(optsPort ?? envPort ?? "20128", 20128);
}
test("dashboard port: uses --port flag when explicitly provided, overriding env", () => {
const port = resolvePort("3000", "9999");
assert.equal(port, 3000);
});
test("dashboard port: falls back to PORT env var when --port is not provided", () => {
const port = resolvePort(undefined, "20129");
assert.equal(port, 20129);
});
test("dashboard port: falls back to 20128 when neither --port nor PORT env var is set", () => {
const port = resolvePort(undefined, undefined);
assert.equal(port, 20128);
});
test("dashboard port: invalid --port (non-numeric) falls back to 20128", () => {
const port = resolvePort("abc", undefined);
assert.equal(port, 20128);
});
test("dashboard port: --port 0 (out of range) falls back to 20128", () => {
const port = resolvePort("0", undefined);
assert.equal(port, 20128);
});
test("dashboard port: --port 70000 (out of range) falls back to 20128", () => {
const port = resolvePort("70000", undefined);
assert.equal(port, 20128);
});
test("dashboard URL generation: http://localhost:<port> built correctly for a custom port", () => {
const port = resolvePort(undefined, "31337");
assert.equal(`http://localhost:${port}`, "http://localhost:31337");
});
test("dashboard command: --port option has no Commander default", async () => {
const fs = await import("node:fs");
const path = await import("node:path");
const dashboardSource = fs.readFileSync(
path.resolve(import.meta.dirname, "../../bin/cli/commands/dashboard.mjs"),
"utf-8",
);
// Ensure the option does NOT carry a baked-in Commander default (third arg).
assert.match(
dashboardSource,
/\.option\("--port <port>",\s*"Port the server is running on"\)/,
);
});
test("dashboard command: source references process.env.PORT (env-fallback regression guard)", async () => {
const fs = await import("node:fs");
const path = await import("node:path");
const dashboardSource = fs.readFileSync(
path.resolve(import.meta.dirname, "../../bin/cli/commands/dashboard.mjs"),
"utf-8",
);
assert.match(dashboardSource, /process\.env\.PORT/);
});

View File

@@ -0,0 +1,80 @@
import { describe, it, before, after } from "node:test";
import assert from "node:assert";
import * as toolDetector from "../../../src/lib/cli-helper/tool-detector.ts";
// #7279 (re-drift of #968) — detectBinary() in tool-detector.ts never checked
// process.platform and never passed shell:true, so on native Windows an
// installed CLI (npm installs claude/codex/opencode as .cmd shims) was reported
// as NOT installed:
// 1. execFileImpl(binary, ["--version"]) fails without shell:true for .cmd shims
// (Node's CVE-2024-27980 hardening).
// 2. the `which` fallback doesn't exist on native Windows (no WSL/git-bash).
// Both throw, both are swallowed by empty catches, detectBinary returns
// { installed: false }. cliRuntime.ts::locateCommand already solved this for
// the runtime-spawn path (#968); this fix reuses it here.
//
// Methodological note (see plan-file): the `which` fallback previously called
// the RAW execFileAsync, not the injected __setExecFileImpl hook, so it wasn't
// mockable and could silently "pass" using the real system `which`. Uses
// `hermes` (confirmed absent from PATH) to avoid that trap; also uses a
// dedicated __setLocateCommandImpl hook (mirrors __setExecFileImpl) so the
// win32 existence probe is deterministic here instead of depending on a real
// `where.exe`.
describe("tool-detector — win32 (#7279)", () => {
const originalPlatformDescriptor = Object.getOwnPropertyDescriptor(process, "platform");
function setPlatform(value: string) {
Object.defineProperty(process, "platform", { configurable: true, value });
}
before(() => {
setPlatform("win32");
toolDetector.__setLocateCommandImpl(async (command: string) => {
if (command === "hermes") {
return {
installed: true,
commandPath: "C:\\Users\\dev\\AppData\\Roaming\\npm\\hermes.cmd",
reason: null,
};
}
return { installed: false, commandPath: null, reason: "not_found" };
});
// @ts-expect-error - internal test hook
toolDetector.__setExecFileImpl(async (_cmd: string, _args: string[], opts?: { shell?: boolean }) => {
// Reproduces the real-world failure: without shell:true, spawning the
// .cmd shim throws (Node's CVE-2024-27980 hardening on Windows).
if (opts?.shell === true) {
return { stdout: "v0.75.3\n" };
}
throw new Error("spawn hermes.cmd ENOENT (shell:true required on win32 for .cmd shims)");
});
});
after(() => {
// This is the only test file exercising these hooks — node:test isolates
// each file's module cache, so no further reset is needed for other suites.
if (originalPlatformDescriptor) {
Object.defineProperty(process, "platform", originalPlatformDescriptor);
}
});
it("reports an installed CLI as installed on native Windows (.cmd shim probed with shell:true)", async () => {
const result = await toolDetector.detectTool("hermes");
assert.ok(result !== null);
assert.strictEqual(
result!.installed,
true,
"expected hermes to be detected as installed via locateCommand + shell:true probe on win32"
);
assert.strictEqual(result!.version, "0.75.3");
});
it("reports a genuinely absent CLI as not installed on native Windows", async () => {
const result = await toolDetector.detectTool("openclaw");
assert.ok(result !== null);
assert.strictEqual(result!.installed, false);
});
});

View File

@@ -71,20 +71,60 @@ test("buildEnvWithRuntime preserva NODE_PATH existente", async () => {
assert.ok(env.NODE_PATH.includes("/existing/path"), "NODE_PATH original deve ser preservado");
});
test("isBetterSqliteBinaryValid detecta ELF magic bytes (Linux)", async () => {
test("isBetterSqliteBinaryValid rejeita binário com magic bytes válidos mas ABI incompatível (regressão #2493)", async () => {
// Regression for upstream 9router#2493: a binary that only "looks" native (correct ELF/Mach-O/PE
// header) but was built for a different Node ABI (NODE_MODULE_VERSION) must NOT be reported as
// valid — loading it crashes the process (segfault) instead of triggering a rebuild.
const { getRuntimeNodeModules, isBetterSqliteBinaryValid } =
await import("../../bin/cli/runtime/nativeDeps.mjs");
const nm = getRuntimeNodeModules();
const buildDir = join(nm, "better-sqlite3", "build", "Release");
mkdirSync(buildDir, { recursive: true });
const binary = join(buildDir, "better_sqlite3.node");
const buf = Buffer.from([0x7f, 0x45, 0x4c, 0x46, 0x00, 0x00, 0x00, 0x00]);
const { platform } = await import("node:os");
const os = platform();
// Correct file-format magic bytes for the current OS, but not a real, loadable native addon —
// this is exactly what the old magic-bytes-only check let through.
const magicByPlatform = {
linux: [0x7f, 0x45, 0x4c, 0x46],
darwin: [0xcf, 0xfa, 0xed, 0xfe],
win32: [0x4d, 0x5a],
};
const magic = magicByPlatform[os] ?? magicByPlatform.linux;
const buf = Buffer.concat([Buffer.from(magic), Buffer.alloc(64, 0)]);
writeFileSync(binary, buf);
const result = isBetterSqliteBinaryValid();
const { platform } = await import("node:os");
if (platform() === "linux") {
assert.equal(result, true, "ELF magic bytes devem ser válidos no Linux");
assert.equal(
result,
false,
"binário com header válido mas ABI/conteúdo incompatível deve ser inválido"
);
rmSync(join(nm, "better-sqlite3"), { recursive: true, force: true });
});
test("isBetterSqliteBinaryValid aceita um binário nativo real e carregável", async () => {
const { getRuntimeNodeModules, isBetterSqliteBinaryValid } =
await import("../../bin/cli/runtime/nativeDeps.mjs");
const { existsSync, copyFileSync } = await import("node:fs");
const realBinary = join(
process.cwd(),
"node_modules",
"better-sqlite3",
"build",
"Release",
"better_sqlite3.node"
);
if (!existsSync(realBinary)) {
// Ambient runtime without a compiled better-sqlite3 binary — nothing to assert here.
return;
}
const nm = getRuntimeNodeModules();
const buildDir = join(nm, "better-sqlite3", "build", "Release");
mkdirSync(buildDir, { recursive: true });
const binary = join(buildDir, "better_sqlite3.node");
copyFileSync(realBinary, binary);
const result = isBetterSqliteBinaryValid();
assert.equal(result, true, "um binário real, compatível com o Node atual, deve ser válido");
rmSync(join(nm, "better-sqlite3"), { recursive: true, force: true });
});

View File

@@ -1,7 +1,7 @@
import test from "node:test";
import assert from "node:assert/strict";
test("CLI_TOOLS registry contains all expected tools (plan 14 — 32 total + crush + codewhale + omp + letta)", async () => {
test("CLI_TOOLS registry contains all expected tools (plan 14 — 33 total + crush + codewhale + omp + letta + grok-build)", async () => {
const { CLI_TOOLS } = await import("../../src/shared/constants/cliTools.ts");
// windsurf and amp removed per plan 14 D17 (MITM backlog plan 11)
// New entries added: roo, jcode, deepseek-tui, smelt, pi, aider, forge,
@@ -10,6 +10,7 @@ test("CLI_TOOLS registry contains all expected tools (plan 14 — 32 total + cru
// codewhale added 2026-07-02 as a dual entry alongside deepseek-tui
// (CodeWhale is the actively-maintained successor to DeepSeek TUI).
// omp + letta added by #6318 (agent-category CLI integrations).
// grok-build added — xAI Grok Build TUI coding agent (ported from upstream decolua/9router#2571).
const expected = [
"claude",
"codex",
@@ -43,6 +44,7 @@ test("CLI_TOOLS registry contains all expected tools (plan 14 — 32 total + cru
"letta",
"agent-deck",
"crush",
"grok-build",
];
for (const id of expected) {
assert.ok(id in CLI_TOOLS, `Missing tool: ${id}`);

View File

@@ -0,0 +1,56 @@
import test from "node:test";
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { isVersionFastPath } from "../../bin/cli/utils/versionFastPath.mjs";
const execFileAsync = promisify(execFile);
// argv shape is [node, script, ...args]
const argv = (...args: string[]) => ["node", "omniroute", ...args];
test("fast-path selector: bare --version/-V select the fast path", () => {
assert.equal(isVersionFastPath(argv("--version")), true);
assert.equal(isVersionFastPath(argv("-V")), true);
});
test("fast-path selector: --help does NOT select the fast path (help text is dynamic)", () => {
assert.equal(isVersionFastPath(argv("--help")), false);
assert.equal(isVersionFastPath(argv("-h")), false);
});
test("fast-path selector: extra args or a subcommand alongside --version fall through", () => {
assert.equal(isVersionFastPath(argv("serve", "--version")), false);
assert.equal(isVersionFastPath(argv("--version", "extra")), false);
assert.equal(isVersionFastPath(argv("--lang", "en", "--version")), false);
});
test("fast-path selector: no args or a real command do not select the fast path", () => {
assert.equal(isVersionFastPath(argv()), false);
assert.equal(isVersionFastPath(argv("serve")), false);
});
test("fast-path selector: defensive on non-array input", () => {
// @ts-expect-error intentional bad input
assert.equal(isVersionFastPath(undefined), false);
});
test("omniroute CLI --version fast-path prints ONLY the version, skipping bootstrap output", async () => {
const pkg = JSON.parse(
readFileSync(join(process.cwd(), "package.json"), "utf8")
) as { version: string };
const { stdout } = await execFileAsync(process.execPath, ["bin/omniroute.mjs", "--version"], {
cwd: process.cwd(),
env: { ...process.env, DATA_DIR: "" },
});
// Before the fast-path, env-file loading (loadEnvFile) runs ahead of Commander and
// prints "Loaded env from ..." lines interleaved with the version — proving the full
// bootstrap (tsx/esm polyfill, env loading, ~70-command Commander registration) ran
// for a plain --version query. The fast-path must short-circuit before any of that,
// so stdout is EXACTLY the version string and nothing else.
assert.equal(stdout.trim(), pkg.version);
});

View File

@@ -0,0 +1,108 @@
/**
* Issue #6980 — Cloudflare Workers AI daily neuron exhaustion 429 must be
* classified as quota_exhausted (not transient rate_limit).
*
* Two layers of defense:
* 1. Provider-specific rule in providerErrorRules.ts → getProviderErrorRuleMatch
* 2. Global QUOTA_PATTERNS in classify429.ts → looksLikeQuotaExhausted
*
* Without these, the 429 body "you have used up your daily free allocation of
* 10,000 neurons" matches no keyword, falls through to rate_limit (~60s cooldown),
* and the combo router keeps cycling through every cloudflare model on retry
* against a budget that only resets at UTC midnight.
*/
import { describe, test } from "node:test";
import assert from "node:assert/strict";
import {
getProviderErrorRuleMatch,
providerRuleRegistry,
} from "../../open-sse/config/providerErrorRules.ts";
import { classify429, looksLikeQuotaExhausted } from "../../src/shared/utils/classify429.ts";
// ─── Fixtures ────────────────────────────────────────────────────────────────
const CF_NEURON_BODY =
"you have used up your daily free allocation of 10,000 neurons, please upgrade to Cloudflare's Workers Paid plan";
const CF_NEURON_BODY_JSON = {
errors: [
{
code: 4006,
message:
"you have used up your daily free allocation of 10,000 neurons, please upgrade to Cloudflare's Workers Paid plan",
},
],
};
// ─── Tests: provider-specific rule (primary path) ───────────────────────────
describe("#6980 provider rule: cloudflare-ai neuron exhaustion", () => {
test("cloudflare-ai is registered in providerRuleRegistry", () => {
assert.ok(providerRuleRegistry.has("cloudflare-ai"));
});
test("429 with plain-string neuron body → quota_exhausted, scope connection", () => {
const result = getProviderErrorRuleMatch("cloudflare-ai", 429, {}, CF_NEURON_BODY);
assert.ok(result, "expected a match");
assert.equal(result!.reason, "quota_exhausted");
assert.equal(result!.scope, "connection");
// No explicit cooldownMs — recordModelLockoutFailure resolves to next UTC midnight.
assert.equal(result!.cooldownMs, undefined);
});
test("429 with JSON-structured neuron body → quota_exhausted", () => {
const result = getProviderErrorRuleMatch("cloudflare-ai", 429, {}, CF_NEURON_BODY_JSON);
assert.ok(result);
assert.equal(result!.reason, "quota_exhausted");
assert.equal(result!.scope, "connection");
});
test("non-429 status does not match even with neuron body", () => {
const result = getProviderErrorRuleMatch("cloudflare-ai", 500, {}, CF_NEURON_BODY);
assert.equal(result, null);
});
test("429 with unrelated body does not match", () => {
const result = getProviderErrorRuleMatch(
"cloudflare-ai",
429,
{},
{
error: "rate limited, try again later",
}
);
assert.equal(result, null);
});
test("provider name matching is case-insensitive", () => {
const result = getProviderErrorRuleMatch("Cloudflare-AI", 429, {}, CF_NEURON_BODY);
assert.ok(result);
assert.equal(result!.reason, "quota_exhausted");
});
});
// ─── Tests: classify429 defense-in-depth (fallback path) ────────────────────
describe("#6980 classify429: daily free allocation pattern", () => {
test("looksLikeQuotaExhausted matches neuron body string", () => {
assert.ok(looksLikeQuotaExhausted(CF_NEURON_BODY));
});
test("looksLikeQuotaExhausted matches neuron body JSON-stringified", () => {
assert.ok(looksLikeQuotaExhausted(CF_NEURON_BODY_JSON));
});
test("classify429 returns quota_exhausted for neuron body", () => {
assert.equal(classify429({ status: 429, body: CF_NEURON_BODY }), "quota_exhausted");
});
test("classify429 returns quota_exhausted for neuron JSON body", () => {
assert.equal(classify429({ status: 429, body: CF_NEURON_BODY_JSON }), "quota_exhausted");
});
test("classify429 returns rate_limit for generic 429 without quota keywords", () => {
assert.equal(classify429({ status: 429, body: "Too many requests" }), "rate_limit");
});
});

View File

@@ -0,0 +1,172 @@
// Regression test for #7522: POST /api/oauth/codex/import must validate the
// imported refresh_token BEFORE persisting a connection. Previously a payload
// carrying an already-invalidated refresh_token (e.g. `refresh_token_invalidated`
// / a dead-on-arrival `auth.json`) was imported as `active` and only failed
// confusingly on first real use.
//
// This test mocks global.fetch so `refreshCodexToken()` (open-sse/services/
// tokenRefresh.ts) talks to a fake OpenAI OAuth token endpoint instead of the
// network — the refresh exchange itself is reused, not reimplemented.
//
// DB handles are released in test.after (CLAUDE.md learning: unreleased
// SQLite handles hang node:test).
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-codex-import-refresh-"));
process.env.DATA_DIR = TEST_DATA_DIR;
const core = await import("../../src/lib/db/core.ts");
const settingsDb = await import("../../src/lib/db/settings.ts");
const providersDb = await import("../../src/lib/db/providers.ts");
const route = await import("../../src/app/api/oauth/codex/import/route.ts");
test.before(async () => {
await settingsDb.updateSettings({ requireLogin: false });
});
test.after(async () => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
function jsonResponse(body: unknown, status = 200) {
return new Response(JSON.stringify(body), {
status,
headers: { "content-type": "application/json" },
});
}
async function withMockedFetch<T>(impl: typeof fetch, fn: () => Promise<T>): Promise<T> {
const original = globalThis.fetch;
globalThis.fetch = impl;
try {
return await fn();
} finally {
globalThis.fetch = original;
}
}
async function postImport(body: unknown) {
const request = new Request("http://localhost:20128/api/oauth/codex/import", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const response = await route.POST(request);
return { status: response.status, body: await response.json() };
}
const BASE_RECORD = {
access_token: "seed-access-token",
refresh_token: "seed-refresh-token-2026-07-10",
email: "operator@example.com",
};
test("import: rejects a record whose refresh_token is already invalidated upstream (#7522)", async () => {
await withMockedFetch(
(async () =>
jsonResponse({ error: { code: "refresh_token_invalidated" } }, 401)) as unknown as typeof fetch,
async () => {
const { status, body } = await postImport({ accounts: BASE_RECORD });
assert.equal(status, 200);
assert.equal(body.success, false);
assert.equal(body.imported, 0);
assert.equal(body.failed, 1);
assert.equal(body.results[0].ok, false);
assert.match(body.results[0].error, /expired|codex login/i);
const rows = await providersDb.getProviderConnections({ provider: "codex" });
const created = rows.find((r) => r.email === BASE_RECORD.email);
assert.equal(created, undefined, "no connection should be persisted for a dead refresh_token");
}
);
});
test("import: rejects a record whose refresh_token was already consumed (refresh_token_reused)", async () => {
await withMockedFetch(
(async () =>
jsonResponse({ error: { code: "refresh_token_reused" } }, 400)) as unknown as typeof fetch,
async () => {
const { status, body } = await postImport({
accounts: { ...BASE_RECORD, email: "reused@example.com" },
});
assert.equal(status, 200);
assert.equal(body.success, false);
assert.equal(body.failed, 1);
const rows = await providersDb.getProviderConnections({ provider: "codex" });
const created = rows.find((r) => r.email === "reused@example.com");
assert.equal(created, undefined);
}
);
});
test("import: creates the connection (with rotated tokens) when the refresh_token is still valid", async () => {
await withMockedFetch(
(async () =>
jsonResponse({
access_token: "rotated-access-token",
refresh_token: "rotated-refresh-token",
expires_in: 3600,
})) as unknown as typeof fetch,
async () => {
const { status, body } = await postImport({
accounts: { ...BASE_RECORD, email: "valid@example.com" },
});
assert.equal(status, 200);
assert.equal(body.success, true);
assert.equal(body.imported, 1);
assert.equal(body.failed, 0);
assert.equal(body.results[0].ok, true);
const rows = await providersDb.getProviderConnections({ provider: "codex" });
const created = rows.find((r) => r.email === "valid@example.com");
assert.ok(created, "connection should be persisted for a valid refresh_token");
assert.equal(created?.accessToken, "rotated-access-token");
assert.equal(created?.refreshToken, "rotated-refresh-token");
}
);
});
test("import: a transient network error validating the refresh_token does not block the import", async () => {
await withMockedFetch(
(async () => {
throw new Error("ECONNRESET");
}) as unknown as typeof fetch,
async () => {
const { status, body } = await postImport({
accounts: { ...BASE_RECORD, email: "transient@example.com" },
});
assert.equal(status, 200);
assert.equal(body.success, true);
assert.equal(body.imported, 1);
const rows = await providersDb.getProviderConnections({ provider: "codex" });
const created = rows.find((r) => r.email === "transient@example.com");
assert.ok(created, "import should proceed with the original tokens on a transient failure");
assert.equal(created?.accessToken, BASE_RECORD.access_token);
}
);
});
test("import: error responses never leak a stack trace", async () => {
await withMockedFetch(
(async () => jsonResponse({ error: { code: "refresh_token_invalidated" } }, 401)) as unknown as typeof fetch,
async () => {
const { body } = await postImport({
accounts: { ...BASE_RECORD, email: "leak-check@example.com" },
});
assert.ok(!JSON.stringify(body).includes("at /"), "must not leak a stack trace");
assert.ok(!JSON.stringify(body).includes(".ts:"), "must not leak a source location");
}
);
});

View File

@@ -0,0 +1,42 @@
// Regression test for #7523: the Codex (and Windsurf/Devin) PKCE OAuth callback
// server binds the SERVER's loopback (localhost:PORT). When OmniRoute runs on a
// remote host (e.g. the VPS) and the operator drives the browser from a different
// machine, the provider redirects to the operator's OWN localhost:PORT — the
// login confirmation screen hangs forever with no explanation.
//
// buildRemoteOAuthHint() detects a non-loopback Host and surfaces the
// reverse-tunnel instruction so the start-callback-server response carries it
// (the UI shows it instead of a silent hang). Loopback access is unaffected.
import test from "node:test";
import assert from "node:assert/strict";
import { buildRemoteOAuthHint } from "../../src/app/api/oauth/[provider]/[action]/remoteOAuthHint.ts";
test("loopback Host → no remote hint (local access is unaffected)", () => {
for (const host of ["localhost", "localhost:20128", "127.0.0.1:20128", "[::1]:20128", "::1"]) {
const hint = buildRemoteOAuthHint(host, 1455);
assert.equal(hint.remoteHost, false, `expected no hint for loopback host ${host}`);
}
});
test("null Host → no remote hint (fail-open: never block a local flow on a missing header)", () => {
const hint = buildRemoteOAuthHint(null, 1455);
assert.equal(hint.remoteHost, false);
});
test("remote Host → returns the reverse-tunnel hint with the exact callback port", () => {
const hint = buildRemoteOAuthHint("192.168.0.15:20128", 1455);
assert.equal(hint.remoteHost, true);
assert.ok(hint.remoteHost === true); // narrow the union
// The tunnel must forward the SAME port the callback server bound, both sides.
assert.equal(hint.tunnelCommand, "ssh -L 1455:127.0.0.1:1455 <user>@<omniroute-host>");
assert.match(hint.message, /remote host \(192\.168\.0\.15:20128\)/);
assert.match(hint.message, /hang/i);
});
test("remote Host honours a random callback port (Windsurf/Devin OS-assigned port)", () => {
const hint = buildRemoteOAuthHint("omniroute.example.com", 54321);
assert.ok(hint.remoteHost === true);
assert.equal(hint.tunnelCommand, "ssh -L 54321:127.0.0.1:54321 <user>@<omniroute-host>");
});

View File

@@ -0,0 +1,73 @@
// #7536: Codex non-stream chat 502'd with "Response body is already used".
//
// Root cause (confirmed live on the VPS via fs-instrumentation): the Codex HTTP
// transport uses the wreq-js TLS-fingerprint client, whose Response is backed by
// a native body handle. On that response, merely *accessing* `response.body`
// disturbs the handle so a later `.text()` throws
// `TypeError: Response body is already used`. The Codex non-stream upstream
// response arrives with an EMPTY content-type, so `peekCodexSseTransientError`
// early-returns — but its guard evaluated `!response.body` (touching `.body`)
// BEFORE the content-type check. That single `.body` access consumed the body,
// and chatCore's `readNonStreamingResponseBody` → `.text()` then 502'd. Streaming
// was unaffected because the peek genuinely wants the body for SSE responses.
//
// The fix reorders the guard so the content-type is checked before `.body` is
// touched. This test locks that in: peek must NOT access `.body` for a non-SSE
// response, and the body must remain readable downstream.
import test from "node:test";
import assert from "node:assert/strict";
import { peekCodexSseTransientError } from "../../open-sse/executors/codex.ts";
/**
* Mimic a wreq-js native-handle Response: reading `.body` is destructive — once
* accessed, `.text()` throws exactly like the live 502. This is what the real
* bug looked like end-to-end.
*/
function makeDestructiveBodyResponse(contentType: string) {
let bodyAccessCount = 0;
let disturbed = false;
const response = {
ok: true,
status: 200,
statusText: "OK",
headers: new Headers(contentType ? { "content-type": contentType } : {}),
get body() {
bodyAccessCount += 1;
disturbed = true; // native handle is now consumed
return new ReadableStream<Uint8Array>();
},
async text() {
if (disturbed) throw new TypeError("Response body is already used");
return "downstream still works";
},
} as unknown as Response;
return { response, bodyAccessCount: () => bodyAccessCount };
}
test("peekCodexSseTransientError does not touch response.body for an empty-content-type response (#7536)", async () => {
const { response, bodyAccessCount } = makeDestructiveBodyResponse("");
const result = await peekCodexSseTransientError(response);
assert.equal(result.matched, null);
assert.equal(result.replacementBody, null);
assert.equal(
bodyAccessCount(),
0,
"peek must not access .body when content-type is not text/event-stream"
);
// The real regression: the body had to stay readable for the non-stream path.
assert.equal(await response.text(), "downstream still works");
});
test("peekCodexSseTransientError does not touch response.body for a non-SSE (application/json) response (#7536)", async () => {
const { response, bodyAccessCount } = makeDestructiveBodyResponse("application/json");
const result = await peekCodexSseTransientError(response);
assert.equal(result.matched, null);
assert.equal(result.replacementBody, null);
assert.equal(bodyAccessCount(), 0, "non-SSE content-type must short-circuit before .body");
assert.equal(await response.text(), "downstream still works");
});

View File

@@ -0,0 +1,50 @@
// Port of upstream decolua/9router PR #2570 (feat(ui): show Codex plan labels
// in provider and quota views).
//
// Two independent gaps this closes:
//
// 1. providerPageHelpers.getCodexPlanLabel — the provider-detail ConnectionRow
// never surfaced the Codex subscription plan (persisted at OAuth import
// time in providerSpecificData.chatgptPlanType — see
// src/lib/oauth/services/codexImport.ts) anywhere in the row UI.
//
// 2. ProviderLimits/utils.resolvePlanValue — the quota-view plan badge
// machinery already existed (tierByConnection / QuotaCardHeader), but its
// persisted-metadata fallback list did not include chatgptPlanType. When
// the live Codex usage endpoint does not return a plan_type field (usage
// service falls back to the literal string "unknown" — see
// open-sse/services/usage/codex.ts), the badge fell through to "Unknown"
// instead of the plan captured at login.
import { test } from "node:test";
import assert from "node:assert/strict";
import { getCodexPlanLabel } from "@/app/(dashboard)/dashboard/providers/[id]/codexPlanLabel";
import { resolvePlanValue } from "@/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils";
test("getCodexPlanLabel returns the trimmed chatgptPlanType for codex connections", () => {
assert.equal(getCodexPlanLabel(true, { chatgptPlanType: " Pro " }), "Pro");
});
test("getCodexPlanLabel returns empty string when not a codex connection", () => {
assert.equal(getCodexPlanLabel(false, { chatgptPlanType: "Pro" }), "");
});
test("getCodexPlanLabel returns empty string when chatgptPlanType is missing/blank", () => {
assert.equal(getCodexPlanLabel(true, {}), "");
assert.equal(getCodexPlanLabel(true, { chatgptPlanType: " " }), "");
assert.equal(getCodexPlanLabel(true, undefined), "");
});
test("resolvePlanValue falls back to the persisted Codex chatgptPlanType when the live plan is unknown", () => {
// Reproduces the exact shape open-sse/services/usage/codex.ts returns when
// the upstream Codex usage endpoint omits plan_type/planType.
assert.equal(resolvePlanValue("unknown", { chatgptPlanType: "Pro" }), "Pro");
});
test("resolvePlanValue still prefers a real live plan over the persisted Codex fallback", () => {
assert.equal(resolvePlanValue("Team", { chatgptPlanType: "Pro" }), "Team");
});
test("resolvePlanValue returns null when neither live nor persisted Codex plan is available", () => {
assert.equal(resolvePlanValue("unknown", {}), null);
assert.equal(resolvePlanValue(null, null), null);
});

View File

@@ -0,0 +1,136 @@
// Live-VPS bug (2026-07-16, release/v3.8.49): a Codex (ChatGPT account) NON-STREAM
// chat request fails 100% of the time with `[502]: Response body is already used
// (reset after 1m)`, returned almost instantly (not a real network/timeout error).
// The streaming (playground) path is unaffected.
//
// Root cause: `peekCodexSseTransientError` (open-sse/executors/codex.ts) peeks the
// first bytes of the upstream SSE response by calling `response.body.getReader()`,
// then — when no transient error is found — calls `reader.releaseLock()` followed
// by a SECOND `response.body.getReader()` on the very same underlying body to
// "continue draining" it into a replacement stream. Re-acquiring a reader on a
// response body that has already been disturbed is exactly the pattern undici's
// fetch/Response implementation guards against ("Body is unusable: Body has
// already been read" / surfaced upstream as "Response body is already used").
// Any runtime/build where the second `getReader()` call on the SAME response.body
// throws turns every single non-streaming Codex request into an uncaught
// TypeError, which chatCore's generic upstream-error handling then classifies as
// a transient failure and stamps with a default 60s cooldown ("reset after 1m") —
// masking a pure code defect as a rate limit.
//
// This test proves the defect directly against `peekCodexSseTransientError`: it
// installs a `getReader` spy on the *original* response body that throws on any
// call after the first (reproducing the "double-acquire" hazard precisely), then
// asserts the function must complete without ever needing a second reader on the
// original body — i.e. it must not throw, and the replacement body it hands back
// must be byte-identical to the original upstream SSE payload.
import test from "node:test";
import assert from "node:assert/strict";
import { peekCodexSseTransientError } from "../../open-sse/executors/codex.ts";
function sseStreamFromChunks(chunks: string[]): ReadableStream<Uint8Array> {
const encoder = new TextEncoder();
let i = 0;
return new ReadableStream<Uint8Array>({
pull(controller) {
if (i >= chunks.length) {
controller.close();
return;
}
controller.enqueue(encoder.encode(chunks[i]));
i++;
},
});
}
/**
* Wrap a ReadableStream so that `getReader()` throws on every call after the
* first — reproducing, at the unit level, a runtime that refuses to re-acquire
* a reader on a body it already considers disturbed (the exact "Response body
* is already used" failure mode observed live).
*/
function withSingleUseGetReader(stream: ReadableStream<Uint8Array>): {
stream: ReadableStream<Uint8Array>;
getReaderCallCount: () => number;
} {
let calls = 0;
const originalGetReader = stream.getReader.bind(stream);
Object.defineProperty(stream, "getReader", {
value: (...args: unknown[]) => {
calls++;
if (calls > 1) {
throw new TypeError("Body is unusable: Body has already been read");
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (originalGetReader as any)(...args);
},
writable: true,
configurable: true,
});
return { stream, getReaderCallCount: () => calls };
}
async function drainStream(stream: ReadableStream<Uint8Array>): Promise<string> {
const reader = stream.getReader();
const decoder = new TextDecoder();
let out = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
out += decoder.decode(value, { stream: true });
}
out += decoder.decode();
return out;
}
test("peekCodexSseTransientError does not re-acquire a reader on the original body for a normal 200-OK SSE response", async () => {
const normalSse =
'event: response.output_text.delta\ndata: {"type":"response.output_text.delta","delta":"Hello"}\n\n' +
'event: response.completed\ndata: {"type":"response.completed","response":{"status":"completed"}}\n\n';
const { stream, getReaderCallCount } = withSingleUseGetReader(sseStreamFromChunks([normalSse]));
const response = new Response(stream, {
status: 200,
headers: { "Content-Type": "text/event-stream" },
});
const peek = await peekCodexSseTransientError(response);
assert.equal(peek.matched, null, "must not classify a normal reply as a transient error");
assert.ok(peek.replacementBody, "must hand back a replacement body to continue draining");
const drained = await drainStream(peek.replacementBody!);
assert.equal(drained, normalSse, "replacement body must be byte-identical to the upstream SSE payload");
// The regression: the OLD implementation calls response.body.getReader() a
// SECOND time (after releaseLock()) to "continue" reading the same body. A
// runtime that refuses that second acquisition throws — which is exactly
// what withSingleUseGetReader reproduces. The fix must never need more than
// one reader on the ORIGINAL body.
assert.ok(
getReaderCallCount() <= 1,
`expected at most 1 getReader() call on the original response body, got ${getReaderCallCount()}`
);
});
test("peekCodexSseTransientError still detects a 200-OK transient-error SSE payload without touching the original body twice", async () => {
const { stream, getReaderCallCount } = withSingleUseGetReader(
sseStreamFromChunks([
'event: error\ndata: {"error":{"message":"Selected model is at capacity. Please try a different model."}}\n\n',
])
);
const response = new Response(stream, {
status: 200,
headers: { "Content-Type": "text/event-stream" },
});
const peek = await peekCodexSseTransientError(response);
assert.equal(peek.matched, "selected model is at capacity");
assert.match(peek.message ?? "", /at capacity/i);
assert.equal(peek.replacementBody, null);
assert.ok(
getReaderCallCount() <= 1,
`expected at most 1 getReader() call on the original response body, got ${getReaderCallCount()}`
);
});

View File

@@ -0,0 +1,74 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { normalizeCodexTools } from "../../open-sse/executors/codex/tools.ts";
// Port of 9router#1556: OpenAI/Codex Responses API rejects JSON Schema `pattern`
// fields containing regex lookaround (lookahead/lookbehind) with:
// "Invalid JSON schema: regex lookaround is not supported. Found at $.properties.email.pattern."
// Clients (e.g. IDE agent harnesses) commonly emit lookahead patterns such as
// `^(?=.*@).+$` for "must contain an @". These must be stripped before the
// tool schema reaches the Codex/OpenAI Responses API.
test("normalizeCodexTools strips regex lookaround from function tool parameter patterns", () => {
const body: Record<string, unknown> = {
tools: [
{
type: "function",
function: {
name: "send_email",
description: "Send an email",
parameters: {
type: "object",
properties: {
email: {
type: "string",
pattern: "^(?=.*@).+$",
},
},
},
},
},
],
};
normalizeCodexTools(body);
const tools = body.tools as Array<Record<string, unknown>>;
const parameters = tools[0].parameters as Record<string, unknown>;
const properties = parameters.properties as Record<string, unknown>;
const emailSchema = properties.email as Record<string, unknown>;
assert.equal(
emailSchema.pattern,
undefined,
"lookaround pattern must be stripped, not forwarded upstream"
);
});
test("normalizeCodexTools preserves plain (non-lookaround) regex patterns", () => {
const body: Record<string, unknown> = {
tools: [
{
type: "function",
function: {
name: "send_email",
parameters: {
type: "object",
properties: {
zip: { type: "string", pattern: "^[0-9]{5}$" },
},
},
},
},
],
};
normalizeCodexTools(body);
const tools = body.tools as Array<Record<string, unknown>>;
const parameters = tools[0].parameters as Record<string, unknown>;
const properties = parameters.properties as Record<string, unknown>;
const zipSchema = properties.zip as Record<string, unknown>;
assert.equal(zipSchema.pattern, "^[0-9]{5}$");
});

View File

@@ -79,3 +79,41 @@ test("combo diagnostics: secret containment — non-whitelisted fields never sur
assert.ok(!serialized.includes("accessToken"), "no accessToken KEY survives");
assert.ok(!serialized.includes("token"), "no token KEY survives");
});
test("combo diagnostics: terminalReason with a non-Latin1 char (em dash) must not crash Response construction (#6612)", () => {
const terminalReason = "reasoning consumed 5/5 tokens — no content output";
assert.doesNotThrow(() => {
const res = errorResponseWithComboDiagnostics(
502,
`Upstream response failed quality validation: ${terminalReason}`,
{
poolSize: 4,
attempted: 1,
excluded: [{ provider: "deepseek", model: "deepseek-v4-flash-free", reason: "quality — bad" }],
attemptOrder: [{ provider: "deepseek", model: "deepseek-v4-flash-free" }],
terminalReason,
}
);
assert.equal(res.status, 502);
});
});
test("combo diagnostics: JSON body keeps the original non-Latin1 text even though headers are ASCII-sanitized (#6612)", async () => {
const terminalReason = "reasoning consumed 5/5 tokens — no content output";
const res = errorResponseWithComboDiagnostics(
502,
`Upstream response failed quality validation: ${terminalReason}`,
{
poolSize: 1,
attempted: 1,
excluded: [],
attemptOrder: [{ provider: "deepseek", model: "deepseek-v4-flash-free" }],
terminalReason,
}
);
// Header value must be a valid Latin1 ByteString — em dash (U+2014) replaced.
assert.equal(res.headers.get("x-omniroute-combo-terminal-reason"), terminalReason.replace("—", "?"));
const body = await res.json();
// JSON body keeps the original, readable (unsanitized) em dash.
assert.equal(body.diagnostics.terminalReason, terminalReason);
});

View File

@@ -0,0 +1,190 @@
/**
* #6764 — fusion strategy silently dropped `combo-ref` panel members.
*
* Every other combo strategy resolves a `{kind:"combo-ref", comboName}` panel
* member through the shared execute-mode machinery (see
* `open-sse/services/combo/runtimeUnits.ts::executeComboRefUnit`); the fusion
* branch in `open-sse/services/combo.ts` only recognized plain `string` or
* `{model: string}` entries, so a combo-ref member had neither field and was
* filtered out (`.filter(Boolean)`) — no error, no warning. This suite proves
* the fix: a combo-ref fusion panel member is dispatched as ONE black-box
* panel voice (a recursive `handleComboChat` call into the referenced combo),
* not dropped, not fanned out into the referenced combo's own targets.
*/
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-combo-fusion-ref-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "combo-fusion-ref-test-secret";
const { handleComboChat } = await import("../../open-sse/services/combo.ts");
const noop = () => {};
const log = { info: noop, warn: noop, debug: noop, error: noop };
type Body = Record<string, unknown>;
function okResponse(content: string): Response {
const body = JSON.stringify({ choices: [{ message: { role: "assistant", content } }] });
return new Response(body, { status: 200, headers: { "Content-Type": "application/json" } });
}
function fusionCombo(models: unknown[], extra: Record<string, unknown> = {}) {
return {
name: "test-fusion-combo-ref",
strategy: "fusion",
models,
config: extra,
};
}
test("fusion: a combo-ref panel member is dispatched, not silently dropped", async () => {
const seen: string[] = [];
const handleSingleModel = async (_b: Body, m: string) => {
seen.push(m);
if (m === "p/judge") return okResponse("FINAL");
return okResponse(`ans-${m}`);
};
const nestedPriority = {
name: "nested-priority",
strategy: "priority",
models: ["openai/nested-a"],
config: { maxRetries: 0, retryDelayMs: 0 },
};
const combo = fusionCombo(
[{ kind: "combo-ref", comboName: "nested-priority" }, { model: "p/plain" }],
{ judgeModel: "p/judge" }
);
const res = await handleComboChat({
body: { messages: [{ role: "user", content: "Q" }] },
combo,
handleSingleModel,
log,
settings: {},
allCombos: [combo, nestedPriority],
});
assert.equal(res.status, 200);
// Proves the combo-ref member was actually dispatched (its nested target
// model reached handleSingleModel) instead of being silently filtered out.
assert.ok(
seen.includes("openai/nested-a"),
`expected nested combo's target model to be dispatched, saw: ${seen.join(", ")}`
);
assert.ok(seen.includes("p/plain"), "plain panel member must still dispatch alongside combo-ref");
});
test("fusion: combo-ref-only panel resolves normally (not a 400 empty-panel error)", async () => {
const seen: string[] = [];
const handleSingleModel = async (_b: Body, m: string) => {
seen.push(m);
return okResponse(`ans-${m}`);
};
const nestedPriority = {
name: "solo-nested",
strategy: "priority",
models: ["openai/solo-target"],
config: { maxRetries: 0, retryDelayMs: 0 },
};
const combo = fusionCombo([{ kind: "combo-ref", comboName: "solo-nested" }]);
const res = await handleComboChat({
body: { messages: [{ role: "user", content: "Q" }] },
combo,
handleSingleModel,
log,
settings: {},
allCombos: [combo, nestedPriority],
});
assert.notEqual(res.status, 400);
assert.ok(seen.includes("openai/solo-target"));
});
test("fusion: self-referencing combo-ref fails that panel member gracefully, not an infinite loop", async () => {
const seen: string[] = [];
const handleSingleModel = async (_b: Body, m: string) => {
seen.push(m);
return okResponse(`ans-${m}`);
};
const combo = fusionCombo([
{ kind: "combo-ref", comboName: "test-fusion-combo-ref" },
{ model: "p/other" },
]);
const res = await handleComboChat({
body: { messages: [{ role: "user", content: "Q" }] },
combo,
handleSingleModel,
log,
settings: {},
allCombos: [combo],
});
// Overall request still degrades gracefully because "p/other" survives.
assert.equal(res.status, 200);
assert.ok(seen.includes("p/other"));
assert.ok(!seen.includes("test-fusion-combo-ref"));
});
test("fusion: combo-ref pointing at a nonexistent combo fails only that panel member", async () => {
const seen: string[] = [];
const handleSingleModel = async (_b: Body, m: string) => {
seen.push(m);
return okResponse(`ans-${m}`);
};
const combo = fusionCombo([
{ kind: "combo-ref", comboName: "does-not-exist" },
{ model: "p/other" },
]);
const res = await handleComboChat({
body: { messages: [{ role: "user", content: "Q" }] },
combo,
handleSingleModel,
log,
settings: {},
allCombos: [combo],
});
assert.equal(res.status, 200);
assert.ok(seen.includes("p/other"));
});
test("fusion: mixed plain string / auto-style / combo-ref panel members all dispatch together", async () => {
const seen: string[] = [];
const handleSingleModel = async (_b: Body, m: string) => {
seen.push(m);
return okResponse(`ans-${m}`);
};
const nestedPriority = {
name: "mixed-nested",
strategy: "priority",
models: ["openai/mixed-target"],
config: { maxRetries: 0, retryDelayMs: 0 },
};
const combo = fusionCombo([
"auto/best-coding",
{ model: "p/direct" },
{ kind: "combo-ref", comboName: "mixed-nested" },
]);
const res = await handleComboChat({
body: { messages: [{ role: "user", content: "Q" }] },
combo,
handleSingleModel,
log,
settings: {},
allCombos: [combo, nestedPriority],
});
assert.equal(res.status, 200);
assert.ok(seen.includes("auto/best-coding"));
assert.ok(seen.includes("p/direct"));
assert.ok(seen.includes("openai/mixed-target"));
});

View File

@@ -79,7 +79,6 @@ test("fusion: fans out to the panel then routes a synthesis turn to the judge",
body: {
messages: [{ role: "user", content: "Q" }],
stream: true,
tools: [{ name: "x" }],
},
combo: fusionCombo(["p/a", "p/b", "p/c"], { judgeModel: "p/judge" }),
handleSingleModel,

View File

@@ -0,0 +1,32 @@
import test from "node:test";
import assert from "node:assert/strict";
import { parseComboProxyAssignmentIds } from "../../src/app/(dashboard)/dashboard/combos/useComboProxyAssignments.ts";
test("#7149: parseComboProxyAssignmentIds extracts scopeIds from valid combo assignments", () => {
const data = {
items: [
{ scopeId: "combo-1", proxyId: "proxy-1", scope: "combo" },
{ scopeId: "combo-2", proxyId: "proxy-2", scope: "combo" },
],
};
assert.deepEqual(parseComboProxyAssignmentIds(data), ["combo-1", "combo-2"]);
});
test("#7149: parseComboProxyAssignmentIds drops entries missing scopeId or proxyId", () => {
const data = {
items: [
{ scopeId: "combo-1", proxyId: "proxy-1" },
{ scopeId: "combo-2", proxyId: null },
{ scopeId: null, proxyId: "proxy-3" },
{},
],
};
assert.deepEqual(parseComboProxyAssignmentIds(data), ["combo-1"]);
});
test("#7149: parseComboProxyAssignmentIds returns [] for missing/malformed items", () => {
assert.deepEqual(parseComboProxyAssignmentIds(null), []);
assert.deepEqual(parseComboProxyAssignmentIds(undefined), []);
assert.deepEqual(parseComboProxyAssignmentIds({}), []);
assert.deepEqual(parseComboProxyAssignmentIds({ items: "not-an-array" }), []);
});

View File

@@ -0,0 +1,87 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-combo-proxy-7149-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.API_KEY_SECRET = "test-secret";
const core = await import("../../src/lib/db/core.ts");
const proxiesDb = await import("../../src/lib/db/proxies.ts");
const providersDb = await import("../../src/lib/db/providers.ts");
const combosDb = await import("../../src/lib/db/combos.ts");
const settingsDb = await import("../../src/lib/db/settings.ts");
type ProxyResolutionLike = {
proxy?: { host?: string } | null;
level?: string;
levelId?: string | null;
} | null;
async function resetStorage() {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}
test.after(async () => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
test("#7149: a proxy assigned to a Combo via the dashboard (registry scope='combo') is honored when resolving the proxy for a request routed through that combo", async () => {
await resetStorage();
const comboProxy = await proxiesDb.createProxy({
name: "Combo-Assigned Proxy",
type: "http",
host: "10.20.30.40",
port: 8888,
});
assert.ok(comboProxy?.id);
const combo = await combosDb.createCombo({
name: "diy_deepseek-v4-flash",
strategy: "round-robin",
models: ["openai/gpt-4"],
});
const comboRecord = combo as Record<string, unknown>;
assert.ok(comboRecord?.id);
const comboId = comboRecord.id as string;
const assignment = await proxiesDb.assignProxyToScope("combo", comboId, comboProxy!.id);
assert.ok(assignment, "assignProxyToScope('combo', ...) should persist the assignment");
const directRegistryLookup = (await proxiesDb.resolveProxyForScopeFromRegistry(
"combo",
comboId
)) as ProxyResolutionLike;
assert.ok(
directRegistryLookup?.proxy,
"the registry must be able to answer a direct combo-scope lookup"
);
assert.equal(directRegistryLookup?.proxy?.host, "10.20.30.40");
const connection = await providersDb.createProviderConnection({
provider: "openai",
authType: "apikey",
apiKey: "sk-test-1234",
name: "openai-account-1",
});
const connectionRecord = connection as Record<string, unknown> | null;
const connectionId = connectionRecord?.id as string;
assert.ok(connectionId, "test setup requires a real connection id");
const resolved = (await settingsDb.resolveProxyForConnection(
connectionId
)) as ProxyResolutionLike;
assert.equal(
resolved?.level,
"combo",
`expected the combo-assigned proxy to be resolved (level="combo"), got level="${resolved?.level}" — the registry-based combo proxy assignment is never consulted by resolveProxyForConnection()`
);
assert.equal(resolved?.proxy?.host, "10.20.30.40");
});

View File

@@ -0,0 +1,65 @@
import test from "node:test";
import assert from "node:assert/strict";
const { validateResponseQuality } = await import("../../open-sse/services/combo.ts");
const encoder = new TextEncoder();
const silentLog = { warn: () => {} };
function sseStream(body: string): ReadableStream<Uint8Array> {
return new ReadableStream<Uint8Array>({
start(controller) {
controller.enqueue(encoder.encode(body));
controller.close();
},
});
}
// OpenAI-shape stream: single role-only delta chunk, then the connection
// closes. No finish_reason anywhere, no `data: [DONE]` sentinel.
function makeTruncatedOpenAiStream(): Response {
const body =
`data: ${JSON.stringify({
id: "chatcmpl-test-truncated",
object: "chat.completion.chunk",
choices: [{ index: 0, delta: { role: "assistant" }, finish_reason: null }],
})}\n\n`;
return new Response(sseStream(body), {
status: 200,
headers: { "content-type": "text/event-stream" },
});
}
// Healthy OpenAI-shape stream: content delta + a chunk carrying
// finish_reason: "stop" — must keep passing through (#3399/#3685 contract).
function makeHealthyOpenAiStream(): Response {
const chunks = [
JSON.stringify({
id: "chatcmpl-test-healthy",
object: "chat.completion.chunk",
choices: [{ index: 0, delta: { role: "assistant", content: "Hello" }, finish_reason: null }],
}),
JSON.stringify({
id: "chatcmpl-test-healthy",
object: "chat.completion.chunk",
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
}),
];
const body = chunks.map((c) => `data: ${c}\n\n`).join("") + "data: [DONE]\n\n";
return new Response(sseStream(body), {
status: 200,
headers: { "content-type": "text/event-stream" },
});
}
test("#7285 RED: OpenAI-shape stream with role-only delta and NO finish_reason should fail over but currently passes as valid", async () => {
const res = makeTruncatedOpenAiStream();
const out = await validateResponseQuality(res, true, silentLog);
assert.equal(out.valid, false, "expected failover (valid:false)");
});
test("#7285 control: a healthy OpenAI stream ending with finish_reason still passes through (#3399/#3685 no-regression)", async () => {
const res = makeHealthyOpenAiStream();
const out = await validateResponseQuality(res, true, silentLog);
assert.equal(out.valid, true, "expected valid:true for a properly terminated stream");
});

View File

@@ -0,0 +1,211 @@
// Unit tests for #6928: expose an editable base-URL field on the ComfyUI
// provider connection + wire the per-connection override through the shared
// resolveComfyUiBaseUrl helper (used by image/video/music generation handlers).
import { test } from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
process.env.DATA_DIR = mkdtempSync(join(tmpdir(), "omniroute-comfyui-baseurl-"));
import { resolveComfyUiBaseUrl } from "../../open-sse/utils/comfyuiClient.ts";
const { handleImageGeneration } = await import("../../open-sse/handlers/imageGeneration.ts");
const { handleVideoGeneration } = await import("../../open-sse/handlers/videoGeneration.ts");
const { handleMusicGeneration } = await import("../../open-sse/handlers/musicGeneration.ts");
const FALLBACK = "http://localhost:8188";
const OVERRIDE = "http://comfyui:8188";
function immediateTimeout(callback, _ms, ...args) {
if (typeof callback === "function") callback(...args);
return 0;
}
function mockComfyFetch(promptId: string, seenUrls: string[]) {
return async (url: string, options: { body?: unknown } = {}) => {
const stringUrl = String(url);
seenUrls.push(stringUrl);
if (stringUrl.endsWith("/prompt")) {
return new Response(JSON.stringify({ prompt_id: promptId }), {
status: 200,
headers: { "content-type": "application/json" },
});
}
if (stringUrl.includes(`/history/${promptId}`)) {
return new Response(
JSON.stringify({
[promptId]: {
outputs: {
1: { images: [{ filename: "out.png", subfolder: "", type: "output" }] },
},
},
}),
{ status: 200, headers: { "content-type": "application/json" } }
);
}
if (stringUrl.includes("/view?")) {
return new Response(new Uint8Array([1, 2, 3]), { status: 200 });
}
throw new Error(`Unexpected URL: ${stringUrl}`);
};
}
test("handleImageGeneration uses the connection's providerSpecificData.baseUrl override for ComfyUI", async () => {
const originalFetch = globalThis.fetch;
const originalSetTimeout = globalThis.setTimeout;
const seenUrls: string[] = [];
globalThis.setTimeout = immediateTimeout;
globalThis.fetch = mockComfyFetch("img-override", seenUrls);
try {
const result = await handleImageGeneration({
body: { model: "comfyui/flux-dev", prompt: "override test" },
credentials: { providerSpecificData: { baseUrl: OVERRIDE } },
log: null,
});
assert.equal(result.success, true);
assert.ok(
seenUrls.every((u) => u.startsWith(OVERRIDE)),
`expected all requests to use ${OVERRIDE}, got ${seenUrls.join(", ")}`
);
} finally {
globalThis.fetch = originalFetch;
globalThis.setTimeout = originalSetTimeout;
}
});
test("handleImageGeneration falls back to localhost:8188 for ComfyUI when credentials is null (no regression)", async () => {
const originalFetch = globalThis.fetch;
const originalSetTimeout = globalThis.setTimeout;
const seenUrls: string[] = [];
globalThis.setTimeout = immediateTimeout;
globalThis.fetch = mockComfyFetch("img-default", seenUrls);
try {
const result = await handleImageGeneration({
body: { model: "comfyui/flux-dev", prompt: "default test" },
credentials: null,
log: null,
});
assert.equal(result.success, true);
assert.ok(seenUrls.every((u) => u.startsWith(FALLBACK)));
} finally {
globalThis.fetch = originalFetch;
globalThis.setTimeout = originalSetTimeout;
}
});
test("handleVideoGeneration uses the connection's providerSpecificData.baseUrl override for ComfyUI", async () => {
const originalFetch = globalThis.fetch;
const originalSetTimeout = globalThis.setTimeout;
const seenUrls: string[] = [];
globalThis.setTimeout = immediateTimeout;
globalThis.fetch = mockComfyFetch("vid-override", seenUrls);
try {
const result = await handleVideoGeneration({
body: { model: "comfyui/animatediff", prompt: "override video" },
credentials: { providerSpecificData: { baseUrl: OVERRIDE } },
log: null,
});
assert.equal(result.success, true);
assert.ok(seenUrls.every((u) => u.startsWith(OVERRIDE)));
} finally {
globalThis.fetch = originalFetch;
globalThis.setTimeout = originalSetTimeout;
}
});
test("handleMusicGeneration uses the connection's providerSpecificData.baseUrl override for ComfyUI", async () => {
const originalFetch = globalThis.fetch;
const originalSetTimeout = globalThis.setTimeout;
const seenUrls: string[] = [];
globalThis.setTimeout = immediateTimeout;
globalThis.fetch = mockComfyFetch("music-override", seenUrls);
try {
const result = await handleMusicGeneration({
body: { model: "comfyui/musicgen-medium", prompt: "override music" },
credentials: { providerSpecificData: { baseUrl: OVERRIDE } },
log: null,
});
assert.equal(result.success, true);
assert.ok(seenUrls.every((u) => u.startsWith(OVERRIDE)));
} finally {
globalThis.fetch = originalFetch;
globalThis.setTimeout = originalSetTimeout;
}
});
test("resolveComfyUiBaseUrl returns the fallback for null credentials", () => {
assert.equal(resolveComfyUiBaseUrl(null, FALLBACK), FALLBACK);
});
test("resolveComfyUiBaseUrl returns the fallback for undefined credentials", () => {
assert.equal(resolveComfyUiBaseUrl(undefined, FALLBACK), FALLBACK);
});
test("resolveComfyUiBaseUrl returns the fallback when providerSpecificData is absent", () => {
assert.equal(resolveComfyUiBaseUrl({}, FALLBACK), FALLBACK);
});
test("resolveComfyUiBaseUrl returns the fallback when providerSpecificData is null", () => {
assert.equal(
resolveComfyUiBaseUrl({ providerSpecificData: null }, FALLBACK),
FALLBACK
);
});
test("resolveComfyUiBaseUrl returns the fallback when baseUrl is absent", () => {
assert.equal(
resolveComfyUiBaseUrl({ providerSpecificData: {} }, FALLBACK),
FALLBACK
);
});
test("resolveComfyUiBaseUrl returns the fallback when baseUrl is not a string", () => {
assert.equal(
resolveComfyUiBaseUrl(
{ providerSpecificData: { baseUrl: 12345 as unknown as string } },
FALLBACK
),
FALLBACK
);
});
test("resolveComfyUiBaseUrl returns the fallback when baseUrl is whitespace-only", () => {
assert.equal(
resolveComfyUiBaseUrl({ providerSpecificData: { baseUrl: " " } }, FALLBACK),
FALLBACK
);
});
test("resolveComfyUiBaseUrl returns the trimmed override when set", () => {
assert.equal(
resolveComfyUiBaseUrl(
{ providerSpecificData: { baseUrl: " http://comfyui:8188 " } },
FALLBACK
),
"http://comfyui:8188"
);
});
test("resolveComfyUiBaseUrl accepts a bare Docker-network hostname override", () => {
assert.equal(
resolveComfyUiBaseUrl({ providerSpecificData: { baseUrl: "http://comfyui:8188" } }, FALLBACK),
"http://comfyui:8188"
);
});
test("resolveComfyUiBaseUrl ignores a top-level credentials.baseUrl (not providerSpecificData)", () => {
const credentials = {
baseUrl: "http://should-be-ignored:8188",
} as { baseUrl: string; providerSpecificData?: { baseUrl?: unknown } | null };
assert.equal(resolveComfyUiBaseUrl(credentials, FALLBACK), FALLBACK);
});

View File

@@ -214,3 +214,27 @@ test("feedStreamingChunk: noop after done state", () => {
assert.equal(out.safeDelta, "");
assert.equal(out.ready, false);
});
// ─── Regression: space-separated arg name/value (9router#1811) ───────────────
// Cursor's real Composer/Auto output has been observed using a single space
// (instead of a newline) between the arg name and its value inside a
// <tool▁sep> segment, e.g. "<tool▁sep>path /Users/.../test". The parser
// must still extract {path: "/Users/.../test"} rather than treating the whole
// segment as the (empty-valued) arg name.
test("parseComposerToolCalls: parses args separated by a space instead of a newline (Cursor Composer live capture)", () => {
const text =
"<tool▁calls▁begin><tool▁call▁begin> Write " +
"<tool▁sep>path /Users/kabawagang/Desktop/Code/iOS_Review/test " +
"<tool▁sep>contents 22\n\n<tool▁call▁end><tool▁calls▁end>";
const result = parseComposerToolCalls(text);
assert.equal(result.toolCalls.length, 1);
const tc = result.toolCalls[0];
assert.equal(tc.function.name, "Write");
const args = JSON.parse(tc.function.arguments);
assert.deepEqual(args, {
path: "/Users/kabawagang/Desktop/Code/iOS_Review/test",
contents: 22,
});
});

View File

@@ -0,0 +1,84 @@
// Regression test for #7005 — adaptive context-budget dial not configurable.
//
// The compute engine for the adaptive context-budget ("dial") shipped in PR #4716
// (Phase 4C), but it was never wired to persistence or the API: the PUT schema
// rejected any `contextBudget` payload (strict schema, no such key) and the DB-backed
// GET path never surfaced a `contextBudget` field. This test proves both halves of
// the wiring: the Zod schema accepts a `contextBudget` write, and the DB read/write
// path round-trips it.
import { describe, it, beforeEach, afterEach, after } from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const TEST_DATA_DIR = fs.mkdtempSync(
path.join(os.tmpdir(), "omniroute-adaptive-context-budget-db-")
);
const ORIGINAL_DATA_DIR = process.env.DATA_DIR;
process.env.DATA_DIR = TEST_DATA_DIR;
const core = await import("../../../src/lib/db/core.ts");
const { getCompressionSettings, updateCompressionSettings } = await import(
"../../../src/lib/db/compression.ts"
);
const { compressionSettingsUpdateSchema } = await import(
"../../../src/shared/validation/compressionConfigSchemas.ts"
);
const { DEFAULT_CONTEXT_BUDGET } = await import(
"../../../open-sse/services/compression/adaptiveCompression/types.ts"
);
beforeEach(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
});
afterEach(() => {
core.resetDbInstance();
});
after(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
if (ORIGINAL_DATA_DIR === undefined) {
delete process.env.DATA_DIR;
} else {
process.env.DATA_DIR = ORIGINAL_DATA_DIR;
}
});
describe("bug #7005: adaptive context-budget dial is configurable", () => {
it("compressionSettingsUpdateSchema accepts a contextBudget write", () => {
const result = compressionSettingsUpdateSchema.safeParse({
contextBudget: {
mode: "floor",
policy: "percentage",
outputReserve: 2048,
safetyMargin: 512,
pct: 0.75,
absoluteBudget: 0,
},
});
assert.equal(result.success, true, JSON.stringify("error" in result ? result.error : null));
});
it("getCompressionSettings() defaults contextBudget to DEFAULT_CONTEXT_BUDGET when absent", async () => {
const settings = await getCompressionSettings();
assert.deepEqual(settings.contextBudget, DEFAULT_CONTEXT_BUDGET);
});
it("updateCompressionSettings() persists a partial contextBudget merge", async () => {
await updateCompressionSettings({
contextBudget: { ...DEFAULT_CONTEXT_BUDGET, mode: "floor", policy: "absolute", absoluteBudget: 8000 },
});
const settings = await getCompressionSettings();
assert.equal(settings.contextBudget?.mode, "floor");
assert.equal(settings.contextBudget?.policy, "absolute");
assert.equal(settings.contextBudget?.absoluteBudget, 8000);
// Untouched fields keep their defaults (this is a JSON-column replace like ultra/aggressive,
// not a deep merge — the caller sends the full object, mirroring the existing pattern).
assert.equal(settings.contextBudget?.outputReserve, DEFAULT_CONTEXT_BUDGET.outputReserve);
});
});

View File

@@ -0,0 +1,82 @@
/**
* Regression for #7096 — the RTK code stripper imported the `typescript`
* package eagerly at module top level (`import ts from "typescript"`), but
* `typescript` lives in devDependencies. After a production-lean deploy
* (`npm run build && npm prune --omit=dev`, recommended in Discussion #6956)
* the package is gone, so merely importing `codeStripper.ts` — which every
* Compression Context page (Lite/Aggressive/Ultra/CCR) pulls in — threw a
* module-not-found error and broke the whole feature.
*
* The fix resolves `typescript` lazily and only when AST-based comment
* stripping is actually requested (opt-in, default off), degrading to a no-op
* when the package is unavailable instead of crashing at import time.
*
* Run: node --import tsx/esm --test tests/unit/compression/codestripper-lazy-ts-7096.test.ts
*/
import { describe, it, afterEach } from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
// Namespace import so a missing named export does not crash the whole test
// module at load — it simply shows up as `undefined` (clean granular red).
import * as codeStripper from "../../../open-sse/services/compression/engines/rtk/codeStripper.ts";
const CODE_STRIPPER_SOURCE = fileURLToPath(
new URL("../../../open-sse/services/compression/engines/rtk/codeStripper.ts", import.meta.url)
);
describe("RTK codeStripper — lazy TypeScript loading (#7096)", () => {
afterEach(() => {
// Always restore the default loader if the seam exists.
codeStripper.__setTypeScriptModuleLoaderForTests?.(null);
});
it("does not import the `typescript` package eagerly at module top level", () => {
const source = fs.readFileSync(CODE_STRIPPER_SOURCE, "utf8");
// A top-level *value* import of typescript (`import ts from "typescript"`)
// is what broke every compression page after `npm prune --omit=dev`.
// Type-only imports (`import type ... from "typescript"`) are erased at
// build time and are fine.
const eagerValueImport =
/^\s*import\s+(?!type\b)[^;\n]*\bfrom\s+["']typescript["']/m.test(source);
assert.equal(
eagerValueImport,
false,
"codeStripper.ts must not import `typescript` eagerly at module top level (use a lazy require instead)"
);
});
it("still strips comments when `typescript` is available (opt-in)", () => {
const code = [
"const x = 1; // inline note",
"// full line comment",
"const y = 2;",
].join("\n");
const out = codeStripper.stripCode(code, "typescript", { removeComments: true });
assert.ok(!out.text.includes("inline note"), "line comment should be removed");
assert.ok(!out.text.includes("full line comment"), "full-line comment should be removed");
assert.ok(out.text.includes("const x = 1"), "code should survive");
assert.ok(out.text.includes("const y = 2"), "code should survive");
});
it("degrades to a no-op (no throw) when `typescript` cannot be resolved", () => {
assert.equal(
typeof codeStripper.__setTypeScriptModuleLoaderForTests,
"function",
"codeStripper must expose a lazy TypeScript loader seam so it can degrade gracefully"
);
// Simulate `npm prune --omit=dev`: typescript is not resolvable.
codeStripper.__setTypeScriptModuleLoaderForTests(() => null);
const code = ["const x = 1; // keep me", "const y = 2;"].join("\n");
let out: ReturnType<typeof codeStripper.stripCode>;
assert.doesNotThrow(() => {
out = codeStripper.stripCode(code, "typescript", { removeComments: true });
}, "stripCode must not throw when typescript is unavailable");
// Comment stripping is skipped, but the code passes through intact.
assert.ok(out!.text.includes("const x = 1"), "code passes through");
assert.ok(out!.text.includes("keep me"), "comment left intact under graceful degradation");
});
});

View File

@@ -0,0 +1,91 @@
/**
* Regression test for upstream 9router#2132 (ported): "Token saver Headroom ruins plan mode
* in Codex CLI".
*
* Root cause: SmartCrusher's system-message guard only checked `role === "system"`. Codex CLI
* (open-sse/executors/codex.ts) sends its instructions/tool-schema turn with role "developer"
* (the Responses-API equivalent of "system" used by newer models). Every other guard in this
* codebase that excludes "system" also excludes "developer" (see roleNormalizer.ts,
* contextManager.ts, claudeUpstreamMessages.ts, etc.) — SmartCrusher was the exception, so it
* happily tabular-compacted JSON arrays (e.g. the update_plan tool schema/examples) embedded in
* the developer-role turn, corrupting the instructions the model needs to call the plan tool.
*/
import { describe, it, before } from "node:test";
import assert from "node:assert/strict";
let crushMessages: typeof import("../../../open-sse/services/compression/engines/headroom/smartcrusher.ts").crushMessages;
let collectCompactableArrays: typeof import("../../../open-sse/services/compression/engines/headroom/smartcrusher.ts").collectCompactableArrays;
let headroomEngine: import("../../../open-sse/services/compression/engines/headroom/index.ts").headroomEngine;
before(async () => {
const mod = await import("../../../open-sse/services/compression/engines/headroom/smartcrusher.ts");
crushMessages = mod.crushMessages;
collectCompactableArrays = mod.collectCompactableArrays;
const engineMod = await import("../../../open-sse/services/compression/engines/headroom/index.ts");
headroomEngine = engineMod.headroomEngine;
});
/** A homogeneous array big enough (>= default minRows=8) to trigger compaction. */
function makePlanSchemaExample(): Record<string, unknown>[] {
return Array.from({ length: 10 }, (_, i) => ({
step: `step-${i + 1}`,
status: i === 0 ? "in_progress" : "pending",
}));
}
describe("headroom SmartCrusher — developer-role guard (9router#2132)", () => {
it("does NOT compact JSON arrays embedded in a developer-role message (crushMessages)", () => {
const json = JSON.stringify(makePlanSchemaExample());
const messages = [
{
role: "developer",
content: `Use the update_plan tool. Example plan:\n\`\`\`json\n${json}\n\`\`\``,
},
{ role: "user", content: "Refactor the auth module." },
];
const { messages: result, changed } = crushMessages(messages, 8);
assert.equal(changed, false, "developer-role content must not be touched");
assert.equal(result[0].content, messages[0].content);
});
it("still compacts the same payload when placed under role: system (control case)", () => {
// Sanity check: this proves the array itself WOULD be compactable — the guard, not the
// shape of the payload, is what must change.
const json = JSON.stringify(makePlanSchemaExample());
const messages = [{ role: "user", content: `\`\`\`json\n${json}\n\`\`\`` }];
const { changed } = crushMessages(messages, 8);
assert.equal(changed, true, "control case: user-role content of the same shape IS compacted");
});
it("collectCompactableArrays does not surface arrays from developer-role messages", () => {
const json = JSON.stringify(makePlanSchemaExample());
const messages = [
{ role: "developer", content: `\`\`\`json\n${json}\n\`\`\`` },
];
const found = collectCompactableArrays(messages, 8);
assert.equal(found.length, 0);
});
it("headroomEngine.apply leaves a Codex-CLI-shaped developer turn untouched end-to-end", () => {
const json = JSON.stringify(makePlanSchemaExample());
const body: Record<string, unknown> = {
model: "gpt-5-codex",
messages: [
{
role: "developer",
content: `Instructions with an embedded schema example:\n\`\`\`json\n${json}\n\`\`\``,
},
{ role: "user", content: "Implement the feature." },
],
};
const result = headroomEngine.apply(body);
assert.equal(result.compressed, false);
assert.deepEqual(result.body, body);
});
});

View File

@@ -0,0 +1,207 @@
import { describe, test } from "node:test";
import assert from "node:assert/strict";
import type { z } from "zod";
import {
providerSupportsCaching,
providerHonorsOpenAIFormatCacheControl,
resolveConnectionCacheOverride,
shouldPreserveCacheControl,
} from "../../open-sse/utils/cacheControlPolicy.ts";
import {
detectCachingContext,
getCacheAwareStrategy,
} from "../../open-sse/services/compression/cachingAware.ts";
import { validateProviderSpecificData } from "../../src/shared/validation/providerSpecificData.ts";
import { normalizeProviderSpecificData } from "../../src/lib/providers/requestDefaults.ts";
// Regression for #6880: a custom/openai-compatible connection (provider id like
// `openai-compatible-chat-<uuid>`) can never match the hardcoded CACHING_PROVIDERS /
// OPENAI_FORMAT_CACHE_CONTROL_PROVIDERS name sets in cacheControlPolicy.ts, so cache
// behaviors (prompt_cache_key injection, the compression cache-aware guard, and
// cache_control passthrough) are permanently disabled for that class of connections with
// no way to opt in. This adds a per-connection `cache` capability override consulted
// first by the policy functions, defaulting to today's hardcoded-set behavior.
function collectIssues(): { ctx: z.RefinementCtx; issues: Array<{ path: (string | number)[]; message: string }> } {
const issues: Array<{ path: (string | number)[]; message: string }> = [];
const ctx = {
addIssue: (issue: { path?: (string | number)[]; message: string }) => {
issues.push({ path: issue.path ?? [], message: issue.message });
},
} as unknown as z.RefinementCtx;
return { ctx, issues };
}
describe("#6880 resolveConnectionCacheOverride", () => {
test("returns null for undefined/non-object/empty cache", () => {
assert.equal(resolveConnectionCacheOverride(undefined), null);
assert.equal(resolveConnectionCacheOverride(null), null);
assert.equal(resolveConnectionCacheOverride("nope"), null);
assert.equal(resolveConnectionCacheOverride({}), null);
assert.equal(resolveConnectionCacheOverride({ cache: null }), null);
assert.equal(resolveConnectionCacheOverride({ cache: [] }), null);
assert.equal(resolveConnectionCacheOverride({ cache: {} }), null);
});
test("extracts valid fields and drops invalid/unknown values", () => {
const result = resolveConnectionCacheOverride({
cache: {
supportsPromptCaching: true,
cacheControlPassthrough: "openai-format",
unknownField: "ignored",
},
});
assert.deepEqual(result, {
supportsPromptCaching: true,
cacheControlPassthrough: "openai-format",
});
const invalid = resolveConnectionCacheOverride({
cache: { supportsPromptCaching: "yes", cacheControlPassthrough: "bogus" },
});
assert.equal(invalid, null);
});
});
describe("#6880 providerSupportsCaching override", () => {
test("unblocks a custom openai-compatible connection when the override opts in", () => {
assert.equal(
providerSupportsCaching("openai-compatible-chat-abc123", undefined, {
supportsPromptCaching: true,
}),
true
);
});
test("no override -> default hardcoded-set behavior is unchanged", () => {
assert.equal(providerSupportsCaching("openai-compatible-chat-abc123"), false);
});
test("explicit opt-out overrides the hardcoded set", () => {
assert.equal(providerSupportsCaching("claude", undefined, { supportsPromptCaching: false }), false);
});
});
describe("#6880 providerHonorsOpenAIFormatCacheControl override", () => {
test("openai-format override enables passthrough for a non-hardcoded provider", () => {
assert.equal(
providerHonorsOpenAIFormatCacheControl("grok-custom", { cacheControlPassthrough: "openai-format" }),
true
);
});
test("strip override disables passthrough", () => {
assert.equal(
providerHonorsOpenAIFormatCacheControl("grok-custom", { cacheControlPassthrough: "strip" }),
false
);
});
test("no override -> default hardcoded-set behavior is unchanged", () => {
assert.equal(providerHonorsOpenAIFormatCacheControl("grok-custom"), false);
assert.equal(providerHonorsOpenAIFormatCacheControl("alibaba"), true);
});
});
describe("#6880 shouldPreserveCacheControl override", () => {
test("preserves cache_control for a non-hardcoded provider when override opts in", () => {
const result = shouldPreserveCacheControl({
userAgent: "claude-code/1.0",
isCombo: false,
targetProvider: "openai-compatible-chat-abc123",
targetFormat: "openai",
connectionCacheOverride: { supportsPromptCaching: true },
});
assert.equal(result, true);
});
test("no override -> non-hardcoded provider still not preserved", () => {
const result = shouldPreserveCacheControl({
userAgent: "claude-code/1.0",
isCombo: false,
targetProvider: "openai-compatible-chat-abc123",
targetFormat: "openai",
});
assert.equal(result, false);
});
});
describe("#6880 compression cache-aware guard", () => {
test("detectCachingContext reports isCachingProvider=true when the override opts in", () => {
const ctx = detectCachingContext(
{ messages: [{ role: "user", content: "hi" }] },
{
provider: "openai-compatible-chat-xyz",
targetFormat: "openai",
connectionCacheOverride: { supportsPromptCaching: true },
}
);
assert.equal(ctx.isCachingProvider, true);
});
test("detectCachingContext without override keeps default (non-caching) behavior", () => {
const ctx = detectCachingContext(
{ messages: [{ role: "user", content: "hi" }] },
{ provider: "openai-compatible-chat-xyz", targetFormat: "openai" }
);
assert.equal(ctx.isCachingProvider, false);
});
test("getCacheAwareStrategy protects the cacheable prefix for an overridden context", () => {
const ctx = detectCachingContext(
{ messages: [{ role: "user", content: "hi" }] },
{
provider: "openai-compatible-chat-xyz",
targetFormat: "openai",
connectionCacheOverride: { supportsPromptCaching: true },
}
);
const strategy = getCacheAwareStrategy("aggressive", ctx);
assert.equal(strategy.skipSystemPrompt, true);
assert.equal(strategy.deterministicOnly, true);
});
});
describe("#6880 validateProviderSpecificData cache block", () => {
test("accepts a well-formed cache block", () => {
const { ctx, issues } = collectIssues();
validateProviderSpecificData(
{ cache: { supportsPromptCaching: true, cacheControlPassthrough: "openai-format" } },
ctx
);
assert.deepEqual(issues, []);
});
test("rejects a non-object cache", () => {
const { ctx, issues } = collectIssues();
validateProviderSpecificData({ cache: "nope" }, ctx);
assert.equal(issues.length, 1);
assert.deepEqual(issues[0]?.path, ["cache"]);
});
test("rejects an invalid cacheControlPassthrough value", () => {
const { ctx, issues } = collectIssues();
validateProviderSpecificData({ cache: { cacheControlPassthrough: "bogus" } }, ctx);
assert.equal(issues.length, 1);
assert.deepEqual(issues[0]?.path, ["cache", "cacheControlPassthrough"]);
});
});
describe("#6880 normalizeProviderSpecificData cache block", () => {
test("strips an invalid cache sub-object down to nothing (key deleted)", () => {
const normalized = normalizeProviderSpecificData("openai-compatible-chat-xyz", {
cache: { supportsPromptCaching: "yes", cacheControlPassthrough: "bogus" },
});
assert.equal(normalized?.cache, undefined);
});
test("preserves a valid cache sub-object", () => {
const normalized = normalizeProviderSpecificData("openai-compatible-chat-xyz", {
cache: { supportsPromptCaching: true, cacheControlPassthrough: "openai-format", junk: 1 },
});
assert.deepEqual(normalized?.cache, {
supportsPromptCaching: true,
cacheControlPassthrough: "openai-format",
});
});
});

View File

@@ -0,0 +1,87 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import {
sortConnectionsByAvailability,
isConnectionAvailable,
getConnectionEffectiveStatus,
} from "../../src/app/(dashboard)/dashboard/providers/[id]/components/connectionRowHelpers";
// Reorder-by-availability — upstream 9router PR #2558 ported to OmniRoute's
// resilience model (rateLimitedUntil cooldown + testStatus), not the
// upstream `modelLock_*` field convention. See CLAUDE.md "Resilience Runtime
// State" → Connection Cooldown.
test("sortConnectionsByAvailability moves available connections to the top", () => {
const connections = [
{ id: "a", testStatus: "error" },
{ id: "b", testStatus: "active" },
{ id: "c", testStatus: "success" },
{ id: "d", testStatus: "expired" },
];
const sorted = sortConnectionsByAvailability(connections);
assert.deepEqual(
sorted.map((c) => c.id),
["b", "c", "a", "d"]
);
});
test("sortConnectionsByAvailability is a stable sort (preserves relative order within each group)", () => {
const connections = [
{ id: "1", testStatus: "error" },
{ id: "2", testStatus: "active" },
{ id: "3", testStatus: "error" },
{ id: "4", testStatus: "success" },
{ id: "5", testStatus: "unknown" },
];
const sorted = sortConnectionsByAvailability(connections);
// Available group (2, 4) keeps its original relative order, then the
// unavailable group (1, 3, 5) keeps its original relative order.
assert.deepEqual(
sorted.map((c) => c.id),
["2", "4", "1", "3", "5"]
);
});
test("sortConnectionsByAvailability does not mutate the input array", () => {
const connections = [{ id: "a", testStatus: "error" }, { id: "b", testStatus: "active" }];
const original = [...connections];
sortConnectionsByAvailability(connections);
assert.deepEqual(connections, original);
});
test("a testStatus: 'unavailable' connection past its cooldown counts as available (lazy recovery)", () => {
const pastCooldown = new Date(Date.now() - 60_000).toISOString();
const connection = { testStatus: "unavailable", rateLimitedUntil: pastCooldown };
assert.equal(getConnectionEffectiveStatus(connection), "active");
assert.equal(isConnectionAvailable(connection), true);
});
test("a testStatus: 'unavailable' connection still within cooldown stays unavailable", () => {
const futureCooldown = new Date(Date.now() + 60_000).toISOString();
const connection = { testStatus: "unavailable", rateLimitedUntil: futureCooldown };
assert.equal(getConnectionEffectiveStatus(connection), "unavailable");
assert.equal(isConnectionAvailable(connection), false);
});
test("sortConnectionsByAvailability treats an active cooldown as unavailable even ahead of a hard error", () => {
const futureCooldown = new Date(Date.now() + 60_000).toISOString();
const connections = [
{ id: "cooling", testStatus: "unavailable", rateLimitedUntil: futureCooldown },
{ id: "recovered", testStatus: "active" },
];
const sorted = sortConnectionsByAvailability(connections);
assert.deepEqual(
sorted.map((c) => c.id),
["recovered", "cooling"]
);
});

View File

@@ -0,0 +1,62 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { filterActiveConnections, filterUsableConnections } from "@/shared/utils/connectionStatus";
// Ported from decolua/9router#2526 — the combos builder listed provider
// connections the user had explicitly disabled, because the page only
// filtered on the connection's last `testStatus` and ignored `isActive`.
// A disabled connection can still carry a stale "active"/"success"
// testStatus from before it was disabled.
test("filterActiveConnections excludes explicitly disabled connections", () => {
const active = { id: "active", isActive: true };
const legacyActive = { id: "legacy" }; // no isActive field -> treated as active
const disabled = { id: "disabled", isActive: false };
assert.deepEqual(filterActiveConnections([active, disabled, legacyActive]), [
active,
legacyActive,
]);
});
test("filterActiveConnections returns an empty list for invalid input", () => {
assert.deepEqual(filterActiveConnections(undefined), []);
assert.deepEqual(filterActiveConnections(null), []);
});
test("filterActiveConnections drops nullish entries instead of passing them through", () => {
// A nullish element must not survive: callers read properties off the
// result (e.g. `connection.testStatus`) and would throw a TypeError.
const active = { id: "active", isActive: true };
assert.deepEqual(filterActiveConnections([null, active, undefined]), [active]);
assert.doesNotThrow(() => filterUsableConnections([null, undefined]));
assert.deepEqual(filterUsableConnections([null, { id: "ok", testStatus: "active" }]), [
{ id: "ok", testStatus: "active" },
]);
});
test("filterUsableConnections applies the isActive gate before the testStatus gate", () => {
// Regression for the exact bug: a disabled connection with a stale
// "active" testStatus must NOT survive the combined filter that
// src/app/(dashboard)/dashboard/combos/page.tsx fetchData() calls.
const connections = [
{ id: "healthy", isActive: true, testStatus: "active" },
{ id: "healthy-success", isActive: true, testStatus: "success" },
{ id: "disabled-but-stale-status", isActive: false, testStatus: "active" },
{ id: "disabled-success-status", isActive: false, testStatus: "success" },
{ id: "enabled-not-tested", isActive: true, testStatus: "untested" },
{ id: "legacy-no-isActive", testStatus: "active" },
];
assert.deepEqual(
filterUsableConnections(connections).map((c) => c.id),
["healthy", "healthy-success", "legacy-no-isActive"]
);
});
test("filterUsableConnections returns an empty list for invalid input", () => {
assert.deepEqual(filterUsableConnections(undefined), []);
assert.deepEqual(filterUsableConnections(null), []);
});

View File

@@ -27,6 +27,7 @@ import type { RegistryModel } from "../../open-sse/config/providerRegistry.ts";
const CHAT_URL = "https://api.githubcopilot.com/chat/completions";
const RESPONSES_URL = "https://api.githubcopilot.com/responses";
const MESSAGES_URL = "https://api.githubcopilot.com/v1/messages";
function getGithubModel(modelId: string): RegistryModel {
const model = PROVIDER_MODELS["gh"]?.find((entry) => entry.id === modelId);
@@ -35,7 +36,7 @@ function getGithubModel(modelId: string): RegistryModel {
}
describe("GithubExecutor — Gemini/Claude must never hit /responses (port 9router#1536)", () => {
it("routes registered Claude/Gemini Copilot models to chat/completions", () => {
it("routes registered Claude Copilot models to the native /v1/messages shim (port decolua/9router#2608)", () => {
const exec = new GithubExecutor();
for (const id of [
"claude-haiku-4.5",
@@ -43,14 +44,18 @@ describe("GithubExecutor — Gemini/Claude must never hit /responses (port 9rout
"claude-sonnet-4.6",
"claude-sonnet-5",
"claude-fable-5",
"claude-opus-4.6",
"claude-opus-4.7",
"claude-opus-4.8",
"claude-opus-4.8-fast",
"claude-opus-4.5",
"gemini-3.1-pro-preview",
"gemini-3.5-flash",
]) {
assert.equal(exec.buildUrl(id, false), MESSAGES_URL, `${id} must route to /v1/messages`);
}
});
it("routes registered Gemini Copilot models to chat/completions", () => {
const exec = new GithubExecutor();
for (const id of ["gemini-3.1-pro-preview", "gemini-3.5-flash"]) {
assert.equal(exec.buildUrl(id, false), CHAT_URL, `${id} must route to chat/completions`);
}
});

View File

@@ -174,6 +174,51 @@ describe("cors/origins.applyCorsHeaders", () => {
assert.match(res.headers.get("Vary") || "", /Origin/);
});
it("CLIENT_API: appends Vary: Accept-Encoding on a 2xx relaxForTokenAuth response (#6737)", () => {
const res = NextResponse.json({ ok: true });
const req = new Request("https://server.example.com/api/v1/models");
applyCorsHeaders(res, req, true);
assert.match(res.headers.get("Vary") || "", /Accept-Encoding/);
});
it("CLIENT_API: combines with Vary: Origin into a single comma-joined header (#6737)", () => {
process.env.CORS_ALLOWED_ORIGINS = "https://app.example.com";
const res = NextResponse.json({ ok: true });
const req = new Request("https://server.example.com/api/v1/models", {
headers: { Origin: "https://app.example.com" },
});
applyCorsHeaders(res, req, true);
const varyValues = res.headers.getSetCookie ? res.headers.get("Vary") : res.headers.get("Vary");
assert.equal(varyValues, "Origin, Accept-Encoding");
assert.equal([...res.headers.entries()].filter(([k]) => k.toLowerCase() === "vary").length, 1);
});
it("MANAGEMENT: does not append Vary: Accept-Encoding (relax off) (#6737)", () => {
const res = NextResponse.json({ ok: true });
const req = new Request("https://server.example.com/api/keys");
applyCorsHeaders(res, req);
assert.doesNotMatch(res.headers.get("Vary") || "", /Accept-Encoding/);
applyCorsHeaders(res, req, false);
assert.doesNotMatch(res.headers.get("Vary") || "", /Accept-Encoding/);
});
it("204 response: does not append Vary: Accept-Encoding even with relaxForTokenAuth (#6737)", () => {
const res = new NextResponse(null, { status: 204 });
const req = new Request("https://server.example.com/api/v1/models", {
method: "OPTIONS",
});
applyCorsHeaders(res, req, true);
assert.doesNotMatch(res.headers.get("Vary") || "", /Accept-Encoding/);
});
it("CLIENT_API: appends Vary: Accept-Encoding even without an Origin header (#6737)", () => {
const res = NextResponse.json({ ok: true });
const req = new Request("https://server.example.com/api/v1/models");
applyCorsHeaders(res, req, true);
assert.equal(res.headers.get("Access-Control-Allow-Origin"), "*");
assert.match(res.headers.get("Vary") || "", /Accept-Encoding/);
});
it("reflects requested headers from Access-Control-Request-Headers preflight", () => {
process.env.CORS_ALLOWED_ORIGINS = "https://app.example.com";
const res = NextResponse.json({ ok: true });

View File

@@ -35,9 +35,12 @@ test("resolveRequestedModel maps cursor-agent's client-side aliases", () => {
modelId: "composer-2",
parameters: [{ id: "fast", value: "true" }],
});
// #7289: pinned Claude ids with an effort suffix split into the base id +
// an "effort" ModelParameter — cursor's server has no route for the
// suffixed id verbatim (see cursor-model-effort-suffix-7289.test.ts).
assert.deepEqual(resolveRequestedModel("claude-4.6-sonnet-medium"), {
modelId: "claude-4.6-sonnet-medium",
parameters: [],
modelId: "claude-4.6-sonnet",
parameters: [{ id: "effort", value: "medium" }],
});
assert.deepEqual(resolveRequestedModel("composer-2"), { modelId: "composer-2", parameters: [] });
});
@@ -148,15 +151,21 @@ test("encodeAgentRunRequest sends ModelDetails for pinned thinking models (#3714
// #3714: pinned Claude/GPT thinking variants returned an empty turn when sent only via
// RequestedModel (field 9, bare model_id). cursor-agent's working wire format also
// carries a ModelDetails envelope with model_id + display_model_id + display_name.
// #7289: the trailing effort suffix ("-xhigh") is now split off into a separate
// ModelParameter — the BASE id is what's shared across RequestedModel + ModelDetails.
const modelId = "claude-opus-4-7-thinking-xhigh";
const baseModelId = "claude-opus-4-7-thinking";
const buf = encodeAgentRunRequest({ modelId, userText: "hi" });
const occurrences = buf.toString("latin1").split(modelId).length - 1;
const text = buf.toString("latin1");
const occurrences = text.split(baseModelId).length - 1;
// RequestedModel.model_id (1) + ModelDetails {model_id, display_model_id, display_name}
// (3) → the id must now appear at least 4 times (it appeared once before the fix).
// (3) → the base id must appear at least 4 times.
assert.ok(
occurrences >= 4,
`pinned model id must be encoded in both RequestedModel and ModelDetails (got ${occurrences})`
`base model id must be encoded in both RequestedModel and ModelDetails (got ${occurrences})`
);
assert.ok(text.includes("effort"), "effort parameter id present (#7289)");
assert.ok(text.includes("xhigh"), "effort parameter value present (#7289)");
});
test("encodeAgentRunRequest keeps RequestedModel + parameters alongside ModelDetails (#3714)", () => {

View File

@@ -0,0 +1,50 @@
import test from "node:test";
import assert from "node:assert/strict";
import { resolveRequestedModel } from "../../open-sse/utils/cursorAgentProtobuf";
// Issue #7289: pinned Claude/GPT models carrying an effort/reasoning suffix
// (e.g. "claude-opus-4-8-high") return an empty turn from cursor's server.
//
// Ground truth captured from the real cursor-agent 2026.07.09 (Node) client
// via an http2/fetch preload hook: the wire request for a pinned model with
// an effort suffix carries the BASE model id (suffix stripped) plus a
// separate ModelParameter — "effort" for Claude models, "reasoning" for GPT
// models — not the full suffixed id crammed into model_id.
test("resolveRequestedModel splits the effort suffix off pinned Claude model ids (#7289)", () => {
assert.deepEqual(resolveRequestedModel("claude-opus-4-8-high"), {
modelId: "claude-opus-4-8",
parameters: [{ id: "effort", value: "high" }],
});
});
test("resolveRequestedModel splits the effort suffix off pinned Claude sonnet model ids (#7289)", () => {
assert.deepEqual(resolveRequestedModel("claude-sonnet-5-high"), {
modelId: "claude-sonnet-5",
parameters: [{ id: "effort", value: "high" }],
});
});
test("resolveRequestedModel splits the reasoning suffix off pinned GPT model ids (#7289)", () => {
assert.deepEqual(resolveRequestedModel("gpt-5.5-high"), {
modelId: "gpt-5.5",
parameters: [{ id: "reasoning", value: "high" }],
});
});
test("resolveRequestedModel does not touch the composer -fast toggle (#7289 regression guard)", () => {
assert.deepEqual(resolveRequestedModel("composer-2-fast"), {
modelId: "composer-2",
parameters: [{ id: "fast", value: "true" }],
});
});
test("resolveRequestedModel does not rewrite ids with no recognized effort suffix (#7289 regression guard)", () => {
assert.deepEqual(resolveRequestedModel("claude-2.5"), {
modelId: "claude-2.5",
parameters: [],
});
assert.deepEqual(resolveRequestedModel("gpt-4o"), {
modelId: "gpt-4o",
parameters: [],
});
});

View File

@@ -0,0 +1,122 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-xp-audit-cleanup-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.DISABLE_SQLITE_AUTO_BACKUP = "true";
const core = await import("../../src/lib/db/core.ts");
const databaseSettings = await import("../../src/lib/db/databaseSettings.ts");
const databaseSettingsRoute = await import("../../src/app/api/settings/database/route.ts");
const cleanup = await import("../../src/lib/db/cleanup.ts");
type CountRow = {
count: number;
};
function resetStorage() {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}
function makeJsonRequest(method: string, body?: unknown): Request {
return new Request("http://localhost/api/settings/database", {
method,
headers: { "Content-Type": "application/json" },
body: body === undefined ? undefined : JSON.stringify(body),
});
}
function insertXpAuditLogRow(createdAt: string) {
const db = core.getDbInstance();
db.prepare(
`INSERT INTO xp_audit_log (api_key_id, action, xp_earned, metadata, created_at)
VALUES (?, ?, ?, ?, ?)`
).run("test-api-key", "test-action", 10, null, createdAt);
}
function countXpAuditLogRows(): number {
const db = core.getDbInstance();
const row = db.prepare("SELECT COUNT(*) AS count FROM xp_audit_log").get() as CountRow;
return row.count;
}
test.beforeEach(() => {
resetStorage();
});
test.after(() => {
resetStorage();
});
test("cleanupXpAuditLog deletes rows older than the retention window and keeps recent rows", async () => {
const oldCreatedAt = new Date(Date.now() - 40 * 24 * 60 * 60 * 1000).toISOString();
const recentCreatedAt = new Date().toISOString();
insertXpAuditLogRow(oldCreatedAt);
insertXpAuditLogRow(recentCreatedAt);
const result = await cleanup.cleanupXpAuditLog();
assert.equal(result.errors, 0);
assert.equal(result.deleted, 1);
assert.equal(countXpAuditLogRows(), 1);
});
test("runAutoCleanup includes an xpAuditLog result with numeric deleted/errors fields", async () => {
const oldCreatedAt = new Date(Date.now() - 40 * 24 * 60 * 60 * 1000).toISOString();
insertXpAuditLogRow(oldCreatedAt);
const result = await cleanup.runAutoCleanup();
assert.ok(result.results.xpAuditLog);
assert.equal(typeof result.results.xpAuditLog.deleted, "number");
assert.equal(typeof result.results.xpAuditLog.errors, "number");
assert.equal(result.results.xpAuditLog.deleted, 1);
assert.equal(countXpAuditLogRows(), 0);
});
test("cleanupXpAuditLog honors a configurable retention.xpAuditLog value", async () => {
const tenDaysAgo = new Date(Date.now() - 10 * 24 * 60 * 60 * 1000).toISOString();
insertXpAuditLogRow(tenDaysAgo);
const current = databaseSettings.getUserDatabaseSettings();
databaseSettings.updateDatabaseSettings({
retention: { ...current.retention, xpAuditLog: 15 },
});
let result = await cleanup.cleanupXpAuditLog();
assert.equal(result.deleted, 0);
assert.equal(countXpAuditLogRows(), 1);
databaseSettings.updateDatabaseSettings({
retention: { ...databaseSettings.getUserDatabaseSettings().retention, xpAuditLog: 5 },
});
result = await cleanup.cleanupXpAuditLog();
assert.equal(result.deleted, 1);
assert.equal(countXpAuditLogRows(), 0);
});
test("PATCH /api/settings/database round-trips retention.xpAuditLog without stripping it", async () => {
const current = databaseSettings.getUserDatabaseSettings();
const response = await databaseSettingsRoute.PATCH(
makeJsonRequest("PATCH", {
retention: { ...current.retention, xpAuditLog: 45 },
}) as never
);
const body = await response.json();
assert.equal(response.status, 200);
assert.equal(body.retention.xpAuditLog, 45);
const getResponse = await databaseSettingsRoute.GET(makeJsonRequest("GET") as never);
const getBody = await getResponse.json();
assert.equal(getResponse.status, 200);
assert.equal(getBody.retention.xpAuditLog, 45);
});

View File

@@ -0,0 +1,200 @@
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 { fileURLToPath } from "node:url";
// Regression guard for #7288 / #7494 — a startup step reaching
// `getDbInstance()` before `preInitSqlJs()` had run threw the misleading
// "sql.js WASM ainda não foi pré-inicializado" error for an EXISTING DB file
// when both synchronous drivers (better-sqlite3, node:sqlite) failed.
//
// NOTE on approach: an earlier version of this fix added a top-level
// `await preInitSqlJs(...)` barrier that used to sit at the bottom of
// `src/lib/db/core.ts` so merely *importing* core.ts guaranteed the
// pre-init. That made core.ts an async ES module — esbuild's CJS bundling
// path (used by `tsx`'s CJS require hook, and hit by several other test
// files that `require("../../src/lib/db/core.ts")` for cleanup, e.g.
// tests/unit/stmt-cache-lru.test.ts) rejects any `require()` of a module
// whose dependency graph contains a top-level await ("This require call is
// not allowed because the transitive dependency ... contains a top-level
// await"), and even where esbuild didn't hard-fail, sharing that pending
// top-level-await Promise across node:test's process broke unrelated tests'
// event-loop bookkeeping ("Promise resolution is still pending but the
// event loop has already resolved" — reproduced with
// tests/unit/api/compression/compression-api.test.ts run in the same
// process as any of the `require(".../core.ts")` cleanup helpers above).
//
// The fix instead closes the ordering gap at the real startup entrypoint:
// `registerNodejs()` (src/instrumentation-node.ts) now calls
// `ensureDbReadyForBoot()` — which pre-initializes sql.js when needed —
// BEFORE any other startup step (ensureSecrets(), clearStaleCrashCooldowns(),
// getSettings(), initAuditLog()) can reach `getDbInstance()`. No top-level
// await anywhere in core.ts.
async function importFreshCore() {
const url = new URL("../../src/lib/db/core.ts", import.meta.url).href;
return import(`${url}?test=${Date.now()}-${Math.random().toString(16).slice(2)}`);
}
let dataDir: string;
let prevDataDir: string | undefined;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let coreModule: any;
test.after(() => {
try {
coreModule?.resetDbInstance?.();
} catch {
/* best-effort cleanup */
}
if (prevDataDir === undefined) delete process.env.DATA_DIR;
else process.env.DATA_DIR = prevDataDir;
if (dataDir) fs.rmSync(dataDir, { recursive: true, force: true });
});
test("src/lib/db/core.ts has no top-level await (breaks esbuild's CJS require() bundling — #7288 hotfix)", () => {
const corePath = fileURLToPath(new URL("../../src/lib/db/core.ts", import.meta.url));
const source = fs.readFileSync(corePath, "utf8");
// A bare `await <expr>;` at column 0 (module top-level scope, not inside
// any function) is the exact pattern that broke esbuild's CJS bundling for
// every transitive `require()` of this module (tsx's CJS require hook,
// used by tests/unit/stmt-cache-lru.test.ts and friends).
assert.doesNotMatch(
source,
/^await\s/m,
"core.ts must not contain a top-level `await` — it makes the module " +
"un-require()-able via esbuild's CJS bundling path and breaks other " +
"tests' event-loop bookkeeping when required in the same process"
);
});
test(
"registerNodejs() calls ensureDbReadyForBoot() before any startup step that " +
"reaches getDbInstance() (ensureSecrets/clearStaleCrashCooldowns/getSettings/" +
"initAuditLog) — closes the #7288/#7494 ordering gap at the real entrypoint",
() => {
const instrumentationPath = fileURLToPath(
new URL("../../src/instrumentation-node.ts", import.meta.url)
);
const source = fs.readFileSync(instrumentationPath, "utf8");
const registerStart = source.indexOf("export async function registerNodejs(");
assert.ok(registerStart >= 0, "registerNodejs() must exist in instrumentation-node.ts");
const dbReadyIndex = source.indexOf("await ensureDbReadyForBoot();", registerStart);
assert.ok(
dbReadyIndex >= 0,
"registerNodejs() must call `await ensureDbReadyForBoot();` — it is the only " +
"caller of preInitSqlJs()"
);
for (const laterDbTouch of [
"await ensureSecrets();",
"clearStaleCrashCooldowns()",
"await getSettings();",
"initAuditLog();",
]) {
const touchIndex = source.indexOf(laterDbTouch, registerStart);
assert.ok(touchIndex >= 0, `expected to find \`${laterDbTouch}\` in registerNodejs()`);
assert.ok(
dbReadyIndex < touchIndex,
`\`await ensureDbReadyForBoot();\` (index ${dbReadyIndex}) must run before ` +
`\`${laterDbTouch}\` (index ${touchIndex}) — otherwise that step can reach ` +
"getDbInstance() before sql.js has had a chance to pre-initialize (#7288 / #7494)"
);
}
}
);
test(
"getDbInstance() called after the REAL ensureDbReadyForBoot() warm-up (the one " +
"registerNodejs() now runs ahead of every other startup step) no longer throws " +
"the ordering-gap 'sql.js WASM ainda não foi pré-inicializado' error when both " +
"sync drivers fail on an EXISTING db file (#7288 / #7494)",
async () => {
dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-7288-"));
const sqliteFile = path.join(dataDir, "storage.sqlite");
// A directory in place of the sqlite file makes BOTH better-sqlite3 and
// node:sqlite fail to open it for real (no mocking needed), while
// fs.existsSync(sqliteFile) stays true — the same shape of failure a
// real ABI mismatch would produce for the two sync drivers.
fs.mkdirSync(sqliteFile);
prevDataDir = process.env.DATA_DIR;
process.env.DATA_DIR = dataDir;
coreModule = await importFreshCore();
// Exercise the REAL production warm-up, not a stand-in: registerNodejs()
// awaits ensureDbReadyForBoot() -> ensureDbInitialized() (which itself
// calls preInitSqlJs() when the sync drivers can't open the file) BEFORE
// any other startup step (ensureSecrets() / clearStaleCrashCooldowns() /
// getSettings() / initAuditLog()) reaches getDbInstance().
const { ensureDbReadyForBoot } = await import("../../src/instrumentation-node");
try {
await ensureDbReadyForBoot(coreModule.ensureDbInitialized);
} catch {
// A literal directory can never become a valid DB for ANY driver, so the
// warm-up itself is expected to fail here. What matters is only WHICH
// error getDbInstance() reports afterwards — see the assertion below.
}
let thrownMessage: string | null = null;
try {
coreModule.getDbInstance();
} catch (err) {
thrownMessage = err instanceof Error ? err.message : String(err);
}
// Acceptance criterion (#7288): "an existing storage.sqlite still boots
// via the sql.js fallback (no 'ainda não foi pré-inicializado')". A
// literal directory can't be opened by ANY driver — including sql.js's
// own fs.readFileSync — so a residual, *different* I/O error here (e.g.
// EISDIR) is expected and is not the ordering-gap bug under test: what
// this test proves is that preInitSqlJs() is actually attempted ahead of
// getDbInstance() (the fix), not that a synthetic directory becomes a
// valid database (impossible for any driver).
assert.ok(
thrownMessage === null || !/ainda não foi pré-inicializado/.test(thrownMessage),
"expected the fix to make preInitSqlJs() run ahead of getDbInstance() instead of " +
"throwing the 'not pre-initialized yet' error when both sync drivers fail on an " +
`existing DB file — got: ${thrownMessage}`
);
}
);
test(
"the warm-up costs nothing on the happy path: sql.js stays un-initialized when a " +
"sync driver can already open the file",
async () => {
const dir2 = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-7288-happy-"));
const file2 = path.join(dir2, "storage.sqlite");
try {
const { tryOpenSync, getSqlJsAdapter } = await import(
"../../src/lib/db/adapters/driverFactory"
);
const { default: Database } = await import("better-sqlite3");
const seed = new Database(file2);
seed.exec("CREATE TABLE t (id INTEGER)");
seed.close();
// The sync-driver probe is what gates the sql.js/WASM fallback: when it
// succeeds, nothing downstream should ever reach preInitSqlJs().
const probe = tryOpenSync(file2, { readonly: true });
assert.ok(probe, "sanity: a sync driver must be able to open a healthy sqlite file here");
probe!.close();
assert.equal(
getSqlJsAdapter(file2),
null,
"sql.js must NOT be pre-initialized when a sync driver can already open the file — " +
"otherwise every boot would pay the WASM-load cost even on the happy path"
);
} finally {
fs.rmSync(dir2, { recursive: true, force: true });
}
}
);

View File

@@ -0,0 +1,99 @@
import { describe, it, before, after } from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-6996-"));
process.env.DATA_DIR = TEST_DATA_DIR;
const { DuckDuckGoWebExecutor, STATUS_URL } = await import(
"../../open-sse/executors/duckduckgo-web.ts"
);
const { resetDbInstance } = await import("../../src/lib/db/core.ts");
const executeInputBase = {
model: "gpt-4o-mini",
body: {
model: "gpt-4o-mini",
messages: [{ role: "user", content: "hi" }],
stream: false,
},
stream: false,
credentials: {},
};
describe("#6996 DuckDuckGo VQD 429 misclassification", () => {
let originalFetch: typeof fetch;
before(() => {
originalFetch = globalThis.fetch;
});
after(() => {
globalThis.fetch = originalFetch;
resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
it("propagates upstream 429 instead of masking it as a generic 503", async () => {
// Set the mock AFTER the module import so it wins over
// open-sse/utils/proxyFetch.ts's own module-load-time
// `globalThis.fetch = patchedFetch` side effect.
globalThis.fetch = (async (input: RequestInfo | URL) => {
const url = typeof input === "string" ? input : (input as URL | Request).toString();
if (url === STATUS_URL) {
return new Response("", {
status: 429,
headers: { "Retry-After": "30" },
});
}
if (url.includes("/duckchat/v1/chat")) {
throw new Error("unexpected chat POST reached without a VQD token");
}
return new Response("<html></html>", { status: 200 });
}) as typeof fetch;
const executor = new DuckDuckGoWebExecutor();
const response = await executor.execute(executeInputBase);
const httpResponse =
response instanceof Response
? response
: (response as { response: Response }).response;
const bodyText = await httpResponse.text();
assert.equal(
httpResponse.status,
429,
`expected the executor to surface DuckDuckGo's real 429 rate-limit status, got ${httpResponse.status} (body: ${bodyText})`
);
});
it("still returns 503 fallback for a genuine 5xx status on the VQD endpoint", async () => {
globalThis.fetch = (async (input: RequestInfo | URL) => {
const url = typeof input === "string" ? input : (input as URL | Request).toString();
if (url === STATUS_URL) {
return new Response("", { status: 500 });
}
if (url.includes("/duckchat/v1/chat")) {
throw new Error("unexpected chat POST reached without a VQD token");
}
return new Response("<html></html>", { status: 200 });
}) as typeof fetch;
const executor = new DuckDuckGoWebExecutor();
const response = await executor.execute(executeInputBase);
const httpResponse =
response instanceof Response
? response
: (response as { response: Response }).response;
const bodyText = await httpResponse.text();
assert.equal(
httpResponse.status,
503,
`expected the executor to keep the 503 fallback for a genuine upstream 5xx, got ${httpResponse.status} (body: ${bodyText})`
);
});
});

View File

@@ -4,6 +4,7 @@ import assert from "node:assert/strict";
import {
withEarlyStreamKeepalive,
ANTHROPIC_PING_FRAME,
OPENAI_KEEPALIVE_FRAME,
} from "../../open-sse/utils/earlyStreamKeepalive.ts";
async function readAll(response: Response): Promise<string> {
@@ -68,9 +69,40 @@ test("ANTHROPIC_PING_FRAME is a real Anthropic ping event (not a comment)", () =
assert.doesNotMatch(decoded, /^:/, "must not be an SSE comment");
});
test("OPENAI_KEEPALIVE_FRAME is a JSON-parseable OpenAI streaming chunk", () => {
const decoded = new TextDecoder().decode(OPENAI_KEEPALIVE_FRAME);
assert.match(decoded, /^data: /);
assert.doesNotMatch(decoded, /^:/, "must not be an SSE comment");
const payload = JSON.parse(decoded.slice("data: ".length).trim());
assert.equal(payload.object, "chat.completion.chunk");
assert.deepEqual(payload.choices, [{ index: 0, delta: {}, finish_reason: null }]);
});
test("slow handler emits the custom OpenAI keepalive chunk before the body", async () => {
const slow = new Promise<Response>((resolve) => {
setTimeout(() => resolve(sseResponse("data: [DONE]\n\n")), 120);
});
const result = await withEarlyStreamKeepalive(slow, {
thresholdMs: 25,
intervalMs: 20,
keepaliveFrame: OPENAI_KEEPALIVE_FRAME,
});
const body = await readAll(result);
assert.doesNotMatch(body, /: omniroute-keepalive/);
const firstFrame = body.split("\n\n")[0];
assert.doesNotThrow(() => JSON.parse(firstFrame.slice("data: ".length)));
assert.match(body, /data: \[DONE\]/);
});
test("slow handler emits the custom keepaliveFrame (Anthropic ping) before the body", async () => {
const slow = new Promise<Response>((resolve) => {
setTimeout(() => resolve(sseResponse("event: message_start\ndata: {}\n\ndata: [DONE]\n\n")), 120);
setTimeout(
() => resolve(sseResponse("event: message_start\ndata: {}\n\ndata: [DONE]\n\n")),
120
);
});
const result = await withEarlyStreamKeepalive(slow, {

View File

@@ -87,15 +87,21 @@ test("dropTrailingAssistantPrefill is null/empty safe", () => {
test("GithubExecutor.transformRequest drops the trailing assistant prefill end-to-end", () => {
const executor = new GithubExecutor();
// Use an unregistered claude-* id so getModelTargetFormat("gh", ...) resolves
// to null and this stays on the /chat/completions path this test targets.
// Registered claude-* ids (e.g. "claude-sonnet-4.6") now carry
// targetFormat:"claude" (native /v1/messages, which supports prefill — port
// of decolua/9router#2608, see github-copilot-claude-native-messages.test.ts)
// and intentionally skip this drop.
const body = {
model: "claude-sonnet-4.6",
model: "claude-sonnet-4",
messages: [
{ role: "user", content: "Hi" },
{ role: "assistant", content: "Here is the answer:" },
],
};
const out = executor.transformRequest("claude-sonnet-4.6", body, false, {});
const out = executor.transformRequest("claude-sonnet-4", body, false, {});
assert.equal(out.messages.length, 1);
assert.equal(out.messages[0].role, "user");

View File

@@ -161,7 +161,13 @@ test("GithubExecutor.transformRequest sanitizes Anthropic-shape content parts (t
],
};
const result = executor.transformRequest("claude-sonnet-4.6", body, true, {});
// Use an unregistered claude-* id (not "claude-sonnet-4.6"/etc.) so
// getModelTargetFormat("gh", ...) resolves to null and this stays on the
// /chat/completions path this test targets. Registered claude-* ids now
// carry targetFormat:"claude" (native /v1/messages — port of
// decolua/9router#2608, see github-copilot-claude-native-messages.test.ts)
// and intentionally skip this sanitization.
const result = executor.transformRequest("claude-sonnet-4", body, true, {});
// user message keeps text + image_url parts untouched
assert.equal(result.messages[0].content[0].type, "text");

View File

@@ -91,13 +91,13 @@ afterEach(() => {
// ── Tests ─────────────────────────────────────────────────────────────────────
describe("FreePoolTab source toggles", () => {
it("renders a toggle group with exactly 3 buttons", async () => {
it("renders a toggle group with exactly 4 buttons", async () => {
const el = renderTab();
await waitForCondition(() => el.querySelector("[role='group']") !== null);
const bar = el.querySelector("[role='group']")!;
expect(bar).toBeTruthy();
const buttons = bar.querySelectorAll("button");
expect(buttons.length).toBe(3);
expect(buttons.length).toBe(4);
});
it("all toggles start enabled (aria-pressed=true)", async () => {
@@ -160,7 +160,7 @@ describe("FreePoolTab source toggles", () => {
expect(stored).toContain("1proxy");
});
it("button labels are 1proxy, Proxifly, IPLocate", async () => {
it("button labels are 1proxy, Proxifly, IPLocate, Webshare", async () => {
const el = renderTab();
await waitForCondition(() => el.querySelector("[role='group']") !== null);
const texts = Array.from(el.querySelector("[role='group']")!.querySelectorAll("button")).map(
@@ -169,6 +169,7 @@ describe("FreePoolTab source toggles", () => {
expect(texts).toContain("1proxy");
expect(texts).toContain("Proxifly");
expect(texts).toContain("IPLocate");
expect(texts).toContain("Webshare");
});
});

View File

@@ -0,0 +1,117 @@
/**
* Unit tests for #6915 — Sort/filter Free Provider Rankings by auth Type.
*
* Targets the PURE helpers `filterRankingsByAuthType` + `sortRankingsAuthTypeFirst`
* (no DB, no I/O) so the new filter/sort logic is exercised in isolation, mirroring
* `tests/unit/freeProviderRankings-filters.test.ts`.
*/
import { test } from "node:test";
import assert from "node:assert/strict";
import {
filterRankingsByAuthType,
sortRankingsAuthTypeFirst,
type FreeProviderRanking,
type ProviderAuthType,
} from "../../src/lib/freeProviderRankings.ts";
function ranking(id: string, category: ProviderAuthType, score: number): FreeProviderRanking {
return {
id,
name: id,
icon: "",
color: "#000",
category,
topModel: null,
averageScore: score,
modelCount: 1,
};
}
// ──────────────── filterRankingsByAuthType ────────────────
test("filterRankingsByAuthType: 'noauth' keeps only NOAUTH rows", () => {
const rows = [
ranking("a", "noauth", 0.9),
ranking("b", "oauth", 0.8),
ranking("c", "apikey", 0.7),
ranking("d", "noauth", 0.6),
];
const result = filterRankingsByAuthType(rows, "noauth");
assert.deepEqual(
result.map((r) => r.id),
["a", "d"]
);
});
test("filterRankingsByAuthType: 'oauth' keeps only OAUTH rows", () => {
const rows = [ranking("a", "noauth", 0.9), ranking("b", "oauth", 0.8)];
const result = filterRankingsByAuthType(rows, "oauth");
assert.deepEqual(
result.map((r) => r.id),
["b"]
);
});
test("filterRankingsByAuthType: 'apikey' keeps only APIKEY rows", () => {
const rows = [ranking("a", "apikey", 0.9), ranking("b", "oauth", 0.8)];
const result = filterRankingsByAuthType(rows, "apikey");
assert.deepEqual(
result.map((r) => r.id),
["a"]
);
});
test("filterRankingsByAuthType: empty-string type returns input unchanged (identity — 'All')", () => {
const rows = [ranking("a", "noauth", 0.9), ranking("b", "oauth", 0.8)];
const result = filterRankingsByAuthType(rows, "");
assert.equal(result, rows);
});
test("filterRankingsByAuthType: undefined type returns input unchanged (identity — 'All')", () => {
const rows = [ranking("a", "noauth", 0.9), ranking("b", "oauth", 0.8)];
const result = filterRankingsByAuthType(rows);
assert.equal(result, rows);
});
// ──────────────── sortRankingsAuthTypeFirst ────────────────
test("sortRankingsAuthTypeFirst: groups NOAUTH < OAUTH < APIKEY", () => {
const rows = [
ranking("apikey-1", "apikey", 0.95),
ranking("oauth-1", "oauth", 0.9),
ranking("noauth-1", "noauth", 0.5),
];
const result = sortRankingsAuthTypeFirst(rows);
assert.deepEqual(
result.map((r) => r.category),
["noauth", "oauth", "apikey"]
);
});
test("sortRankingsAuthTypeFirst: preserves relative (score) order within each group (stable-sort proof)", () => {
// Input already sorted by score across mixed types (simulating computeFreeProviderRankings output).
const rows = [
ranking("apikey-best", "apikey", 0.95),
ranking("oauth-best", "oauth", 0.9),
ranking("noauth-best", "noauth", 0.85),
ranking("apikey-worst", "apikey", 0.8),
ranking("oauth-worst", "oauth", 0.6),
ranking("noauth-worst", "noauth", 0.4),
];
const result = sortRankingsAuthTypeFirst(rows);
assert.deepEqual(
result.map((r) => r.id),
["noauth-best", "noauth-worst", "oauth-best", "oauth-worst", "apikey-best", "apikey-worst"]
);
});
test("sortRankingsAuthTypeFirst: does not mutate the input array", () => {
const rows = [ranking("apikey-1", "apikey", 0.95), ranking("noauth-1", "noauth", 0.5)];
const original = [...rows];
sortRankingsAuthTypeFirst(rows);
assert.deepEqual(rows, original);
});
test("sortRankingsAuthTypeFirst: empty input returns empty output", () => {
assert.deepEqual(sortRankingsAuthTypeFirst([]), []);
});

View File

@@ -0,0 +1,79 @@
/**
* Regression test for upstream issue decolua/9router#1905.
*
* Reported symptom: a fusion combo populated with ~70+ panel models fans every
* member out in parallel (`open-sse/services/fusion.ts::handleFusionChat` →
* `Promise.all`-style fan-out via `collectPanel`), buffering each model's full
* response text in memory at once. With the runtime heap capped at 1024MB
* (Dockerfile `OMNIROUTE_MEMORY_MB`), a large panel with sizable concurrent
* responses can exceed the heap ceiling and crash the whole container with
* "FATAL ERROR: Ineffective mark-compacts near heap limit — JavaScript heap
* out of memory" instead of failing one request gracefully.
*
* Fix: `handleFusionChat` now rejects panels above a configurable hard cap
* (`FUSION_DEFAULTS.maxPanel`, overridable via `fusionTuning.maxPanel`) with a
* clean 400 *before* fan-out, rather than let an unbounded panel size drive
* the process into an OOM crash.
*/
import test from "node:test";
import assert from "node:assert/strict";
import { handleFusionChat, FUSION_DEFAULTS } from "../../open-sse/services/fusion.ts";
const noop = () => {};
const log = { info: noop, warn: noop, debug: noop, error: noop };
type Body = Record<string, unknown>;
test("fusion #1905: an oversized panel (73 models) is rejected before fan-out instead of OOM-crashing", async () => {
let calls = 0;
const handleSingleModel = (_b: Body, _m: string) => {
calls++;
const body = JSON.stringify({
choices: [{ message: { role: "assistant", content: "x".repeat(1000) } }],
});
return Promise.resolve(
new Response(body, { status: 200, headers: { "Content-Type": "application/json" } })
);
};
const panel = Array.from({ length: 73 }, (_, i) => `provider/model-${i}`);
const res = await handleFusionChat({
body: { messages: [{ role: "user", content: "hi" }] },
models: panel,
handleSingleModel,
log,
comboName: "auto",
});
assert.equal(res.status, 400);
// Must reject BEFORE fan-out — no per-model calls should have happened.
assert.equal(calls, 0, "panel fan-out must not start once the size cap is exceeded");
const json = (await res.json()) as { error?: { message?: string } };
assert.match(json.error?.message ?? "", /panel/i);
});
test("fusion #1905: a panel at or under the cap still fans out normally", async () => {
const handleSingleModel = (_b: Body, _m: string) => {
const body = JSON.stringify({
choices: [{ message: { role: "assistant", content: "ok" } }],
});
return Promise.resolve(
new Response(body, { status: 200, headers: { "Content-Type": "application/json" } })
);
};
const panel = Array.from({ length: FUSION_DEFAULTS.maxPanel }, (_, i) => `provider/model-${i}`);
const res = await handleFusionChat({
body: { messages: [{ role: "user", content: "hi" }] },
models: panel,
handleSingleModel,
log,
comboName: "auto",
});
assert.equal(res.status, 200);
});

View File

@@ -0,0 +1,153 @@
/**
* Regression guard for #6771 — fusion combos stripping tools/tool_choice for
* tool-bearing requests via panel-fan-out-then-judge synthesis.
*
* Root cause: panel members answer tool-shaped prompts with no tool access
* (degraded prose), and the judge's injected synthesis directive ("produce
* ONE authoritative final answer" from anonymized panel sources) steers even
* a tools-capable judge away from emitting a real tool call.
*
* Fix: detect a tool-bearing request up front (non-empty `tools`, and
* `tool_choice` not explicitly "none") and bypass the panel fan-out +
* judge-synthesis path entirely — route the full, unmodified body straight
* to a single model (the configured judgeModel, or panel[0]).
*/
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-fusion-6771-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "fusion-6771-test-secret";
const { handleComboChat } = await import("../../open-sse/services/combo.ts");
type Body = Record<string, unknown>;
function jsonResponse(model: string, content: string): Response {
const body = JSON.stringify({ model, choices: [{ message: { role: "assistant", content } }] });
return new Response(body, { status: 200, headers: { "Content-Type": "application/json" } });
}
const noop = () => {};
const log = { info: noop, warn: noop, debug: noop, error: noop };
function fusionCombo(models: string[], extra: Record<string, unknown> = {}) {
return {
name: "fusion-tools",
strategy: "fusion",
models: models.map((m) => ({ model: m })),
config: extra,
};
}
const TOOLS = [
{
type: "function",
function: { name: "get_weather", description: "Get the weather", parameters: {} },
},
];
test("6771: tool-bearing request bypasses panel fan-out — single call, tools intact, targets configured judge", async () => {
const calls: Array<{ model: string; body: Body }> = [];
const handleSingleModel = async (b: Body, m: string) => {
calls.push({ model: m, body: b });
return jsonResponse(m, "tool decision");
};
const requestBody: Body = {
messages: [{ role: "user", content: "what's the weather?" }],
tools: TOOLS,
tool_choice: "auto",
};
const res = await handleComboChat({
body: requestBody,
combo: fusionCombo(["panel/a", "panel/b"], { judgeModel: "judge/model" }),
handleSingleModel,
log,
settings: {},
allCombos: [],
});
// Exactly one call — not once per panel member + once for the judge.
assert.equal(calls.length, 1, `expected exactly 1 call, got: ${calls.map((c) => c.model).join(", ")}`);
assert.equal(calls[0].model, "judge/model");
// The forwarded body still contains the original tools/tool_choice unmodified.
assert.deepEqual(calls[0].body.tools, TOOLS);
assert.equal(calls[0].body.tool_choice, "auto");
assert.equal(res.status, 200);
});
test("6771: tool-bearing request with no explicit judgeModel targets panel[0]", async () => {
const calls: string[] = [];
const handleSingleModel = async (_b: Body, m: string) => {
calls.push(m);
return jsonResponse(m, "tool decision");
};
await handleComboChat({
body: {
messages: [{ role: "user", content: "what's the weather?" }],
tools: TOOLS,
},
combo: fusionCombo(["panel/a", "panel/b"]), // no judgeModel — defaults to panel[0]
handleSingleModel,
log,
settings: {},
allCombos: [],
});
assert.deepEqual(calls, ["panel/a"]);
});
test("6771: tools present but tool_choice:\"none\" still goes through normal fan-out+judge path", async () => {
const calls: string[] = [];
const handleSingleModel = async (_b: Body, m: string) => {
calls.push(m);
return jsonResponse(m, "prose answer");
};
await handleComboChat({
body: {
messages: [{ role: "user", content: "hi" }],
tools: TOOLS,
tool_choice: "none",
},
combo: fusionCombo(["panel/a", "panel/b"], { judgeModel: "judge/model" }),
handleSingleModel,
log,
settings: {},
allCombos: [],
});
// panel.length (2) fan-out calls + 1 judge call = 3.
assert.equal(calls.length, 3, `expected 3 calls (fan-out+judge), got: ${calls.join(", ")}`);
assert.deepEqual(calls.slice(0, 2).sort(), ["panel/a", "panel/b"]);
assert.equal(calls[2], "judge/model");
});
test("6771: no regression — request without tools still goes through normal fan-out+judge path", async () => {
const calls: string[] = [];
const handleSingleModel = async (_b: Body, m: string) => {
calls.push(m);
return jsonResponse(m, "prose answer");
};
await handleComboChat({
body: { messages: [{ role: "user", content: "hi" }] },
combo: fusionCombo(["panel/a", "panel/b"], { judgeModel: "judge/model" }),
handleSingleModel,
log,
settings: {},
allCombos: [],
});
assert.equal(calls.length, 3, `expected 3 calls (fan-out+judge), got: ${calls.join(", ")}`);
assert.deepEqual(calls.slice(0, 2).sort(), ["panel/a", "panel/b"]);
assert.equal(calls[2], "judge/model");
});

View File

@@ -266,15 +266,20 @@ test("#2832: GeminiWebExecutor catch block sanitizes Playwright launch errors (i
// ─── StreamGenerate parsing ─────────────────────────────────────────────────
test("parseStreamResponse concatenates Gemini Web text from multiple wrb.fr chunks", () => {
test("parseStreamResponse keeps only the final cumulative StreamGenerate snapshot (no duplication) — regression for #7163", () => {
const makeChunk = (text: string) => {
const inner = new Array(80).fill(null);
inner[4] = [[null, [text]]];
return `[["wrb.fr", null, ${JSON.stringify(JSON.stringify(inner))}]]`;
};
const raw = `)]}'\n10\n${makeChunk("First ")}\n5\n${makeChunk("chunk")}`;
assert.equal(parseStreamResponse(raw), "First chunk");
// Gemini's StreamGenerate frames are CUMULATIVE snapshots: each later frame
// repeats the full answer generated so far, not just the new characters.
const frame1 = "Hello!";
const frame2 = "Hello! How can I";
const frame3 = "Hello! How can I help you out today?";
const raw = `)]}'\n10\n${makeChunk(frame1)}\n5\n${makeChunk(frame2)}\n5\n${makeChunk(frame3)}`;
assert.equal(parseStreamResponse(raw), frame3);
});
test("parseStreamResponse ignores wrb.fr lines whose first entry is not an array", () => {

View File

@@ -0,0 +1,131 @@
// GitHub Copilot exposes an Anthropic-native `/v1/messages` shim alongside its
// OpenAI-shape `/chat/completions` and `/responses` endpoints. Only the native
// shim surfaces prompt-cache token counts (`cached_tokens`) for Claude models —
// /chat/completions silently drops them, and round-tripping Claude tool_use /
// tool_result / thinking content blocks through the OpenAI shape is lossy.
//
// Port of upstream decolua/9router#2608 (author: yidecode), adapted to
// OmniRoute's architecture: instead of the executor doing its own
// translateRequest/translateResponse + manual SSE TransformStream (9router has
// no generic per-model targetFormat mechanism), OmniRoute already has a
// registry-driven `targetFormat` field (see opencode/zen's Qwen entries,
// opencode/go) that makes chatCore.ts translate the request to Claude shape
// *before* the executor ever sees it, and translate the response back
// generically afterwards. So the actual port is: (1) tag the github registry's
// claude-* models with targetFormat:"claude", (2) teach the github executor's
// buildUrl()/buildHeaders() to route those models at the new messagesUrl with
// an anthropic-version header, and (3) gate the executor's /chat/completions-only
// request transforms (content-part flattening, trailing-assistant-prefill drop,
// response_format-as-system-prompt workaround) off for the native path, since
// they either don't apply to Claude-shape bodies or actively corrupt them.
import test from "node:test";
import assert from "node:assert/strict";
const { GithubExecutor } = await import("../../open-sse/executors/github.ts");
const { getModelTargetFormat } = await import("../../open-sse/config/providerModels.ts");
test("registry: claude-* github models resolve targetFormat 'claude'", () => {
for (const model of ["claude-opus-4.8", "claude-sonnet-4.6", "claude-haiku-4.5"]) {
assert.equal(
getModelTargetFormat("gh", model),
"claude",
`${model} must resolve to the claude target format so chatCore translates natively`
);
}
});
test("registry: non-claude github models keep their existing targetFormat", () => {
assert.equal(getModelTargetFormat("gh", "gpt-5.4"), "openai-responses");
assert.equal(getModelTargetFormat("gh", "gpt-4o-mini"), null);
});
test("buildUrl: claude models route to the native /v1/messages endpoint", () => {
const executor = new GithubExecutor();
const url = executor.buildUrl("claude-opus-4.8", true);
assert.equal(url, "https://api.githubcopilot.com/v1/messages");
});
test("buildUrl: gpt codex/responses models still route to /responses", () => {
const executor = new GithubExecutor();
const url = executor.buildUrl("gpt-5.4", true);
assert.match(url, /\/responses$/);
});
test("buildUrl: plain gpt models still route to /chat/completions", () => {
const executor = new GithubExecutor();
const url = executor.buildUrl("gpt-4o-mini", true);
assert.equal(url, executor.config.baseUrl);
assert.match(url, /\/chat\/completions$/);
});
test("buildHeaders: claude-native requests carry anthropic-version", () => {
const executor = new GithubExecutor();
const headers = executor.buildHeaders({ accessToken: "tok" }, true, null, "claude-opus-4.8");
assert.equal(headers["anthropic-version"], "2023-06-01");
});
test("buildHeaders: non-claude requests do not carry anthropic-version", () => {
const executor = new GithubExecutor();
const headers = executor.buildHeaders({ accessToken: "tok" }, true, null, "gpt-4o-mini");
assert.equal(headers["anthropic-version"], undefined);
});
test("transformRequest: claude-native path preserves native tool_use/tool_result content blocks", () => {
const executor = new GithubExecutor();
const body = {
model: "claude-opus-4.8",
system: "you are a helpful assistant",
messages: [
{
role: "assistant",
content: [{ type: "tool_use", id: "t1", name: "search", input: { q: "hi" } }],
},
{
role: "user",
content: [{ type: "tool_result", tool_use_id: "t1", content: "result text" }],
},
],
};
const result = executor.transformRequest("claude-opus-4.8", body, true, {});
// Pre-port: sanitizeChatCompletionsMessage flattened every non-text/image_url
// part to {type:"text", text: ...}, destroying the tool_use/tool_result blocks
// Anthropic's native /v1/messages endpoint actually needs.
assert.equal((result.messages[0].content[0] as { type: string }).type, "tool_use");
assert.equal((result.messages[1].content[0] as { type: string }).type, "tool_result");
});
test("transformRequest: claude-native path keeps a trailing assistant message (prefill)", () => {
const executor = new GithubExecutor();
const body = {
model: "claude-opus-4.8",
messages: [
{ role: "user", content: "hi" },
{ role: "assistant", content: "Sure, here is" },
],
};
const result = executor.transformRequest("claude-opus-4.8", body, true, {});
// Pre-port: dropTrailingAssistantPrefill removed the trailing assistant turn
// because Copilot's /chat/completions rejects prefill — but the native
// /v1/messages endpoint is real Anthropic-compatible and supports it.
assert.equal(result.messages.length, 2);
assert.equal(result.messages[1].role, "assistant");
});
test("transformRequest: non-claude (chat/completions) path still flattens tool_use content and drops prefill", () => {
const executor = new GithubExecutor();
const body = {
model: "gpt-4o-mini",
messages: [
{
role: "assistant",
content: [{ type: "tool_use", id: "t1", name: "search", input: {} }],
},
{ role: "user", content: "hi" },
{ role: "assistant", content: "trailing prefill" },
],
};
const result = executor.transformRequest("gpt-4o-mini", body, true, {});
assert.equal((result.messages[0].content[0] as { type: string }).type, "text");
assert.equal(result.messages.length, 2, "trailing assistant message must still be dropped");
});

View File

@@ -0,0 +1,44 @@
// tests/unit/glm-executor-max-tokens-clamp-7364.test.ts
// #7364 Defect B: GlmExecutor.execute() drives its own fetch flow (executeTransport /
// transformForTransport) and never runs through DefaultExecutor.execute()'s
// stripUnsupportedParams() call site — so a STRIP_RULES clamp entry for provider "glm"
// was dead code until transformForTransport() called it directly. This proves the wiring,
// not just the STRIP_RULES entry (see zai-glm-max-tokens-clamp-7364.test.ts for that).
import test from "node:test";
import assert from "node:assert/strict";
import { GlmExecutor } from "../../open-sse/executors/glm.ts";
test("GlmExecutor.transformForTransport clamps an oversized client max_tokens for glm-4.6v (openai transport)", () => {
const executor = new GlmExecutor("glm");
const body = { messages: [{ role: "user", content: "describe this image" }], max_tokens: 65536 };
const transformed = executor.transformForTransport(
"glm-4.6v",
body,
false,
{ apiKey: "glm-key" },
"openai"
) as { max_tokens?: number };
assert.equal(
transformed.max_tokens,
32768,
"#7364: glm-4.6v max_tokens above the catalog ceiling must be clamped by the real GlmExecutor transform path"
);
});
test("GlmExecutor.transformForTransport leaves an in-range max_tokens for glm-4.6v untouched", () => {
const executor = new GlmExecutor("glm");
const body = { messages: [{ role: "user", content: "describe this image" }], max_tokens: 2048 };
const transformed = executor.transformForTransport(
"glm-4.6v",
body,
false,
{ apiKey: "glm-key" },
"openai"
) as { max_tokens?: number };
assert.equal(transformed.max_tokens, 2048);
});

View File

@@ -0,0 +1,35 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { globToRegex } from "@/shared/utils/globPattern";
test("globToRegex — * matches any sequence of characters", () => {
const re = globToRegex("claude-sonnet*");
assert.equal(re.test("claude-sonnet-4"), true);
assert.equal(re.test("claude-sonnet"), true);
assert.equal(re.test("claude-opus-4"), false);
});
test("globToRegex — ? matches exactly one character", () => {
const re = globToRegex("gpt-?");
assert.equal(re.test("gpt-4"), true);
assert.equal(re.test("gpt-40"), false);
assert.equal(re.test("gpt-"), false);
});
test("globToRegex — case-insensitive", () => {
const re = globToRegex("Claude-Sonnet*");
assert.equal(re.test("claude-sonnet-4"), true);
assert.equal(re.test("CLAUDE-SONNET-4"), true);
});
test("globToRegex — anchored (no partial match)", () => {
const re = globToRegex("sonnet");
assert.equal(re.test("claude-sonnet-4"), false);
assert.equal(re.test("sonnet"), true);
});
test("globToRegex — escapes regex special characters", () => {
const re = globToRegex("gpt-4.1");
assert.equal(re.test("gpt-4.1"), true);
assert.equal(re.test("gpt-4X1"), false); // literal dot, not "any char"
});

View File

@@ -0,0 +1,172 @@
/**
* GPT-5 tools+reasoning guard — `stripGpt5ReasoningWhenTools`.
*
* On the raw `openai` Chat Completions surface, GPT-5.x reasoning models reject a
* request that carries BOTH function tools and an active `reasoning_effort` with
* HTTP 400: "Function tools with reasoning_effort are not supported for
* <model> in /v1/chat/completions. Please use /v1/responses instead."
* (port of 9router#2540). OmniRoute's `forceResponsesUpstream` guard only fires
* for `openai-compatible-*` connections carrying MCP/tool_search tool shapes —
* the plain `openai` provider has no equivalent guard, so this scenario still
* reaches the upstream 400 today. Strip `reasoning_effort`/`reasoning` when
* function tools are present so the request succeeds on /v1/chat/completions.
*
* The guard is passed the request's already-resolved `targetFormat` (chatCore
* resolves it once via `resolveChatCoreTargetFormat` before this guard runs) so it
* gates on the actual upstream surface for THIS request rather than a model-name
* list. This matters because #7242 (closes #2540 upstream / 9router#2547) tags the
* public GPT-5.6 family with `targetFormat: "openai-responses"` and routes it to
* `/v1/responses` instead — an endpoint that accepts tools + reasoning natively —
* so stripping must NOT fire for GPT-5.6 requests once that routing is in effect.
* Without this composition, #7101's strip and #7242's reroute would combine into
* the worst of both worlds: routed to the endpoint that supports reasoning, but
* with reasoning silently dropped anyway.
*/
import test from "node:test";
import assert from "node:assert/strict";
import { stripGpt5ReasoningWhenTools } from "../../open-sse/services/gpt5SamplingGuard.ts";
// Chat Completions models in this suite use gpt-5.4/gpt-5.5 (targetFormat "openai"),
// which stay on /chat/completions and must keep being stripped. gpt-5.6-sol is reserved
// for the /v1/responses composition cases below, where stripping must NOT happen.
test("strips reasoning_effort for openai gpt-5.x on /chat/completions when function tools are present", () => {
const body = {
model: "gpt-5.4-sol",
reasoning_effort: "high",
tools: [{ type: "function", function: { name: "read_file" } }],
messages: [],
};
const result = stripGpt5ReasoningWhenTools(body, "openai", "gpt-5.4-sol", "openai");
assert.equal(result.reasoning_effort, undefined);
});
test("strips nested reasoning.effort for openai gpt-5.x on /chat/completions when function tools are present", () => {
const body = {
model: "gpt-5.4-sol",
reasoning: { effort: "medium" },
tools: [{ type: "function", function: { name: "read_file" } }],
};
const result = stripGpt5ReasoningWhenTools(body, "openai", "gpt-5.4-sol", "openai");
assert.equal(result.reasoning, undefined);
});
test("keeps reasoning_effort=none untouched (already non-reasoning mode)", () => {
const body = {
model: "gpt-5.4-sol",
reasoning_effort: "none",
tools: [{ type: "function", function: { name: "read_file" } }],
};
const result = stripGpt5ReasoningWhenTools(body, "openai", "gpt-5.4-sol", "openai");
assert.equal(result.reasoning_effort, "none");
});
test("keeps reasoning_effort when there are no tools", () => {
const body = { model: "gpt-5.4-sol", reasoning_effort: "high", messages: [] };
const result = stripGpt5ReasoningWhenTools(body, "openai", "gpt-5.4-sol", "openai");
assert.equal(result.reasoning_effort, "high");
});
test("keeps reasoning_effort when tools array is empty", () => {
const body = { model: "gpt-5.4-sol", reasoning_effort: "high", tools: [] };
const result = stripGpt5ReasoningWhenTools(body, "openai", "gpt-5.4-sol", "openai");
assert.equal(result.reasoning_effort, "high");
});
test("non-openai provider is untouched", () => {
const body = {
model: "gpt-5.4-sol",
reasoning_effort: "high",
tools: [{ type: "function", function: { name: "x" } }],
};
const result = stripGpt5ReasoningWhenTools(body, "codex", "gpt-5.4-sol", "openai");
assert.equal(result.reasoning_effort, "high");
});
test("non-gpt-5 openai model is untouched", () => {
const body = {
model: "gpt-4o",
reasoning_effort: "high",
tools: [{ type: "function", function: { name: "x" } }],
};
const result = stripGpt5ReasoningWhenTools(body, "openai", "gpt-4o", "openai");
assert.equal(result.reasoning_effort, "high");
});
test("returns the same reference when nothing to strip", () => {
const body = { model: "gpt-5.4-sol", tools: [{ type: "function" }], messages: [] };
const result = stripGpt5ReasoningWhenTools(body, "openai", "gpt-5.4-sol", "openai");
assert.equal(result, body);
});
test("logs the stripped fields when a logger is provided", () => {
const calls: Array<[string, string]> = [];
const log = { warn: (tag: string, message: string) => calls.push([tag, message]) };
stripGpt5ReasoningWhenTools(
{
model: "gpt-5.4-sol",
reasoning_effort: "high",
tools: [{ type: "function", function: { name: "x" } }],
},
"openai",
"gpt-5.4-sol",
"openai",
log
);
assert.equal(calls.length, 1);
assert.equal(calls[0][0], "PARAMS");
assert.match(calls[0][1], /reasoning_effort/);
});
// --- Composition with #7242 (GPT-5.6 → /v1/responses) ---
//
// #7242 tags the public GPT-5.6 family with targetFormat "openai-responses" so it is
// routed to /v1/responses, which natively supports tools + reasoning together. If this
// guard ignored targetFormat and only looked at provider+model-name (the pre-#7242
// shape), a GPT-5.6 request with tools + reasoning_effort would still get its reasoning
// silently stripped even though it is no longer going to /chat/completions — the worst
// of both worlds. These cases prove the composition holds.
test("gpt-5.6 routed to /v1/responses (targetFormat openai-responses) keeps reasoning_effort", () => {
const body = {
model: "gpt-5.6-sol",
reasoning_effort: "high",
tools: [{ type: "function", function: { name: "read_file" } }],
messages: [],
};
const result = stripGpt5ReasoningWhenTools(body, "openai", "gpt-5.6-sol", "openai-responses");
assert.equal(result.reasoning_effort, "high");
assert.equal(result, body, "no-op path should return the same reference");
});
test("gpt-5.6 routed to /v1/responses keeps nested reasoning.effort too", () => {
const body = {
model: "gpt-5.6-sol",
reasoning: { effort: "medium" },
tools: [{ type: "function", function: { name: "read_file" } }],
};
const result = stripGpt5ReasoningWhenTools(body, "openai", "gpt-5.6-sol", "openai-responses");
assert.deepEqual(result.reasoning, { effort: "medium" });
});
test("gpt-5.4/gpt-5.5 stay on /chat/completions (targetFormat openai) and keep stripping", () => {
for (const model of ["gpt-5.4-sol", "gpt-5.5-pro"]) {
const body = {
model,
reasoning_effort: "high",
tools: [{ type: "function", function: { name: "read_file" } }],
};
const result = stripGpt5ReasoningWhenTools(body, "openai", model, "openai");
assert.equal(result.reasoning_effort, undefined, `${model} should still be stripped`);
}
});
test("if gpt-5.6 were ever NOT routed to /v1/responses, the strip would still apply (defense in depth)", () => {
const body = {
model: "gpt-5.6-sol",
reasoning_effort: "high",
tools: [{ type: "function", function: { name: "read_file" } }],
};
const result = stripGpt5ReasoningWhenTools(body, "openai", "gpt-5.6-sol", "openai");
assert.equal(result.reasoning_effort, undefined);
});

View File

@@ -0,0 +1,56 @@
// Regression test for: Grok Build (grok-cli) inference/refresh requests bypassed the
// operator's configured proxy entirely. The executor talks to Grok's upstream via raw
// Node `https.request()` (forced IPv4, to dodge Cloudflare blocking on the direct path)
// instead of the process-wide patched `fetch()` that every other executor uses — so a
// proxy pinned to the connection/provider/global scope was silently ignored, leaking the
// real egress IP and defeating account-isolation/anonymity setups. Mirrors the class of
// bug fixed upstream in decolua/9router#2343 ("fix(oauth): honor proxy selection during
// OAuth login"), adapted to OmniRoute's actual grok-cli architecture (import-token flow,
// no device-code polling) where the leak lives in `resolveGrokRequestDispatch()`.
import { test } from "node:test";
import assert from "node:assert/strict";
import { resolveGrokRequestDispatch } from "../../open-sse/executors/grok-cli.ts";
const TARGET_URL = "https://grok.x.ai/rest/app-chat/conversations/new";
test("resolveGrokRequestDispatch: no proxy configured -> direct IPv4 dispatch (unchanged behavior)", () => {
const dispatch = resolveGrokRequestDispatch(TARGET_URL, () => ({
source: "direct",
proxyUrl: null,
}));
assert.equal(dispatch.family, 4);
assert.equal(dispatch.agent, undefined);
});
test("resolveGrokRequestDispatch: HTTP proxy configured -> request is dispatched through a proxy agent, not direct", () => {
const dispatch = resolveGrokRequestDispatch(TARGET_URL, () => ({
source: "context",
proxyUrl: "http://proxy.internal:8080",
}));
// The fix: an agent bound to the configured proxy must be present, and the
// direct-IPv4 workaround must NOT be applied (it would race the proxy tunnel).
assert.ok(dispatch.agent, "expected a proxy agent to be constructed");
assert.notEqual(dispatch.family, 4);
});
test("resolveGrokRequestDispatch: HTTPS proxy configured -> request is dispatched through a proxy agent", () => {
const dispatch = resolveGrokRequestDispatch(TARGET_URL, () => ({
source: "context",
proxyUrl: "https://user:pass@proxy.internal:8443",
}));
assert.ok(dispatch.agent, "expected a proxy agent to be constructed");
});
test("resolveGrokRequestDispatch: unsupported proxy protocol (socks5) fails closed instead of leaking direct", () => {
assert.throws(
() =>
resolveGrokRequestDispatch(TARGET_URL, () => ({
source: "context",
proxyUrl: "socks5://proxy.internal:1080",
})),
/proxy/i
);
});

View File

@@ -0,0 +1,17 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { extractJwtCookie, extractApiKey } from "../../scripts/homolog/lib/adminClient.mjs";
test("extrai o cookie JWT do set-cookie do login", () => {
const jwt = extractJwtCookie(["auth_token=abc.def.ghi; Path=/; HttpOnly; SameSite=Lax"]);
assert.equal(jwt, "auth_token=abc.def.ghi");
});
test("retorna null sem set-cookie de token", () => {
assert.equal(extractJwtCookie(["other=1; Path=/"]), null);
});
test("extrai key e id do POST /api/keys", () => {
const r = extractApiKey({ key: "or-abc123", id: "k1", name: "homolog-run" });
assert.deepEqual(r, { key: "or-abc123", id: "k1" });
});

View File

@@ -0,0 +1,29 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { evaluateParity } from "../../scripts/homolog/lib/parity.mjs";
test("parity OK quando health bate com a versão esperada", () => {
const r = evaluateParity(
{ status: "healthy", version: "3.8.49" },
{ expectedVersion: "3.8.49", httpStatus: 200 }
);
assert.equal(r.ok, true);
assert.deepEqual(r.failures, []);
});
test("parity falha listando cada divergência", () => {
const r = evaluateParity(
{ status: "degraded", version: "3.8.47" },
{ expectedVersion: "3.8.49", httpStatus: 200 }
);
assert.equal(r.ok, false);
assert.equal(r.failures.length, 2); // status!=healthy, version mismatch
});
test("parity falha em HTTP não-200 mesmo com body bom", () => {
const r = evaluateParity(
{ status: "healthy", version: "3.8.49" },
{ expectedVersion: "3.8.49", httpStatus: 503 }
);
assert.equal(r.ok, false);
});

View File

@@ -0,0 +1,18 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { promptfooToCtrf } from "../../scripts/homolog/lib/promptfooToCtrf.mjs";
test("mapeia resultados do promptfoo para tests CTRF", () => {
const ctrf = promptfooToCtrf({
results: {
results: [
{ provider: { label: "openai" }, success: true, latencyMs: 812 },
{ provider: { label: "grok" }, success: false, latencyMs: 30000, error: "timeout" },
],
},
});
assert.equal(ctrf.results.summary.tests, 2);
assert.equal(ctrf.results.summary.passed, 1);
assert.equal(ctrf.results.tests[1].status, "failed");
assert.equal(ctrf.results.tests[1].name, "provider-smoke: grok");
});

View File

@@ -0,0 +1,24 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { pickSmokeModels } from "../../scripts/homolog/lib/providerTiers.mjs";
const CATALOG = [
{ id: "openai/gpt-5-mini" },
{ id: "openai/gpt-5" },
{ id: "anthropic/claude-sonnet-5" },
{ id: "mistral/mistral-small" },
{ id: "grok/grok-4-fast" },
];
test("1 modelo por provider crítico (o primeiro do catálogo)", () => {
const picks = pickSmokeModels(CATALOG, ["openai", "anthropic", "grok"]);
assert.deepEqual(
picks.map((p) => p.model),
["openai/gpt-5-mini", "anthropic/claude-sonnet-5", "grok/grok-4-fast"]
);
});
test("provider crítico ausente do catálogo vira miss reportável", () => {
const picks = pickSmokeModels(CATALOG, ["openai", "nvidia"]);
assert.equal(picks.find((p) => p.provider === "nvidia").model, null);
});

View File

@@ -0,0 +1,28 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { parseSseChunk, summarizeStream } from "../../scripts/homolog/lib/sseCheck.mjs";
test("parseSseChunk separa eventos data: e detecta [DONE]", () => {
const events = parseSseChunk('data: {"choices":[{"delta":{"content":"O"}}]}\n\ndata: [DONE]\n\n');
assert.equal(events.length, 2);
assert.equal(events[1], "[DONE]");
});
test("parseSseChunk acha data: mesmo precedido de comment-lines SSE no mesmo bloco", () => {
// Formato real da VPS (v3.8.47): trailers de telemetria como comments (`: x-omniroute-*`)
// no MESMO bloco do data: [DONE] — o parser não pode olhar só o início do bloco.
const chunk =
'data: {"choices":[{"delta":{"content":"OK"}}]}\n\n' +
": x-omniroute-cache-hit=false\n: x-omniroute-latency-ms=67\ndata: [DONE]\n\n";
const events = parseSseChunk(chunk);
assert.deepEqual(events, ['{"choices":[{"delta":{"content":"OK"}}]}', "[DONE]"]);
});
test("summarizeStream exige >=1 delta de conteúdo e terminador [DONE]", () => {
const good = summarizeStream(['{"choices":[{"delta":{"content":"OK"}}]}', "[DONE]"]);
assert.equal(good.ok, true);
const noDone = summarizeStream(['{"choices":[{"delta":{"content":"OK"}}]}']);
assert.equal(noDone.ok, false);
const noContent = summarizeStream(["[DONE]"]);
assert.equal(noContent.ok, false);
});

View File

@@ -0,0 +1,123 @@
/**
* Regression test for #7258 — zh-TW (and other locales) rendering the raw
* `__MISSING__:<english>` sentinel written by `scripts/i18n/sync-ui-keys.mjs`
* instead of falling back to the clean English value.
*
* `deepMergeFallback` (src/i18n/request.ts) previously only substituted the
* EN value when a key was entirely `undefined`; a key that existed but still
* carried the untranslated placeholder passed through untouched and was
* rendered verbatim to the user.
*/
import { test } from "node:test";
import assert from "node:assert/strict";
import { readFileSync, readdirSync } from "node:fs";
import { fileURLToPath } from "node:url";
import path from "node:path";
import { deepMergeFallback, PLACEHOLDER_PREFIX } from "../../src/i18n/request.ts";
const messagesDir = path.join(
path.dirname(fileURLToPath(import.meta.url)),
"..",
"..",
"src",
"i18n",
"messages"
);
function loadLocale(locale: string): Record<string, unknown> {
const raw = readFileSync(path.join(messagesDir, `${locale}.json`), "utf8");
return JSON.parse(raw) as Record<string, unknown>;
}
function collectPlaceholderLeaves(
node: unknown,
pathPrefix: string,
out: string[]
): void {
if (node === null || typeof node !== "object") {
if (typeof node === "string" && node.startsWith(PLACEHOLDER_PREFIX)) {
out.push(pathPrefix);
}
return;
}
if (Array.isArray(node)) return;
for (const [key, value] of Object.entries(node as Record<string, unknown>)) {
collectPlaceholderLeaves(value, pathPrefix ? `${pathPrefix}.${key}` : key, out);
}
}
// ---------------------------------------------------------------------------
// 1. Focused repro: the exact keys from the issue report
// ---------------------------------------------------------------------------
test("#7258 repro: zh-TW keys carry a raw __MISSING__: placeholder before the fix is exercised", () => {
const zhTW = loadLocale("zh-TW");
const leaves: string[] = [];
collectPlaceholderLeaves(zhTW, "", leaves);
assert.ok(
leaves.length > 0,
"expected zh-TW.json to still contain __MISSING__: placeholders (translation content backlog)"
);
});
test("#7258: deepMergeFallback replaces an untranslated __MISSING__ placeholder with the EN fallback value", () => {
const target: Record<string, unknown> = {
localUsageCommand: `${PLACEHOLDER_PREFIX}Run this command locally`,
};
const source: Record<string, unknown> = {
localUsageCommand: "Run this command locally",
};
const result = deepMergeFallback(target, source);
assert.equal(result.localUsageCommand, "Run this command locally");
assert.ok(!(result.localUsageCommand as string).startsWith(PLACEHOLDER_PREFIX));
});
test("#7258: deepMergeFallback still lets a real (non-placeholder) locale value win", () => {
const target: Record<string, unknown> = { greeting: "Hola" };
const source: Record<string, unknown> = { greeting: "Hello" };
const result = deepMergeFallback(target, source);
assert.equal(result.greeting, "Hola");
});
test("#7258: deepMergeFallback replaces nested placeholder leaves too", () => {
const target: Record<string, unknown> = {
ns: { a: `${PLACEHOLDER_PREFIX}English A`, b: "translated B" },
};
const source: Record<string, unknown> = {
ns: { a: "English A", b: "English B" },
};
const result = deepMergeFallback(target, source);
const ns = result.ns as Record<string, unknown>;
assert.equal(ns.a, "English A");
assert.equal(ns.b, "translated B", "already-translated sibling key is untouched");
});
// ---------------------------------------------------------------------------
// 2. General regression: for every shipped locale, the REAL production merge
// (locale ⟵ EN fallback) leaves zero raw __MISSING__: leaves.
// ---------------------------------------------------------------------------
test("#7258: after the real EN-fallback merge, no locale has a raw __MISSING__: leaf", () => {
const en = loadLocale("en");
const locales = readdirSync(messagesDir)
.filter((f) => f.endsWith(".json"))
.map((f) => f.replace(/\.json$/, ""))
.filter((locale) => locale !== "en");
assert.ok(locales.length > 0, "expected at least one non-EN locale file");
const offenders: Record<string, string[]> = {};
for (const locale of locales) {
const localeMessages = loadLocale(locale);
const merged = deepMergeFallback({ ...localeMessages }, en);
const leaves: string[] = [];
collectPlaceholderLeaves(merged, "", leaves);
if (leaves.length > 0) offenders[locale] = leaves;
}
assert.deepEqual(
offenders,
{},
`expected zero __MISSING__: leaves after EN fallback merge, found: ${JSON.stringify(offenders)}`
);
});

View File

@@ -11,10 +11,10 @@
// `type: "image"` by the imageRegistry loop — and catalogDedupe.ts keys on
// (id, type, subtype), so the two distinct-`type` entries both survived.
//
// Fix: skip a synced model in the chat-catalog loop when it is already a registered
// image model for that exact provider (open-sse/config/imageRegistry.ts
// isRegisteredImageModel()) — the imageRegistry loop still adds the correctly-typed
// `type: "image"` entry.
// Fix: skip an exact-provider registered image model from the chat-catalog loop only
// when synced metadata does not explicitly advertise `chat` or `responses`. The image
// registry loop still adds the correctly typed image entry, while multi-capability
// models keep both entries.
import test from "node:test";
import assert from "node:assert/strict";
@@ -38,6 +38,7 @@ async function resetStorage() {
}
test.beforeEach(async () => {
v1ModelsCatalog.__resetCatalogBuilderRunsForTest();
await resetStorage();
});
@@ -46,19 +47,19 @@ test.after(async () => {
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
async function seedHuggingFaceConnection() {
async function seedProviderConnection(provider: string) {
return providersDb.createProviderConnection({
provider: "huggingface",
provider,
authType: "apikey",
name: `huggingface-${Math.random().toString(16).slice(2, 8)}`,
apiKey: "hf-key",
name: `${provider}-${Math.random().toString(16).slice(2, 8)}`,
apiKey: `${provider}-key`,
isActive: true,
testStatus: "active",
});
}
test("#6457 image/diffusion model discovered via live sync is NOT listed as a chat model", async () => {
const connection = await seedHuggingFaceConnection();
const connection = await seedProviderConnection("huggingface");
// Simulate what HuggingFace's live `/v1/models` discovery persists for an
// image/diffusion model: no supportedEndpoints/modality info at all — the exact
@@ -100,3 +101,36 @@ test("#6457 image/diffusion model discovered via live sync is NOT listed as a ch
assert.equal(entry.type, undefined, "the real chat model must not carry a non-chat type");
}
});
test("registered image model with explicit chat endpoints keeps both catalog entries", async () => {
const connection = await seedProviderConnection("codex");
await modelsDb.replaceSyncedAvailableModelsForConnection("codex", connection.id, [
{
id: "gpt-5.6-sol",
name: "GPT 5.6 Sol",
supportedEndpoints: ["responses"],
},
]);
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models?prefix=alias")
);
assert.equal(response.status, 200);
const body = (await response.json()) as {
data: Array<{ id: string; type?: string; supported_endpoints?: string[] }>;
};
const entries = body.data.filter((model) => model.id.endsWith("/gpt-5.6-sol"));
assert.ok(
entries.some(
(model) => model.type !== "image" && model.supported_endpoints?.includes("responses")
),
"explicit responses support must keep the synced chat entry"
);
assert.ok(
entries.some((model) => model.type === "image"),
"the registered image entry must remain available under the same model id"
);
});

View File

@@ -0,0 +1,103 @@
/**
* Issue #7071 — Ollama Cloud's 5-hour "session" usage-limit 429 is never
* recognized as quota-exhausted. The upstream returns a body like:
* "you (<account>) have reached your session usage limit"
*
* This exactly mirrors the already-fixed "weekly usage limit" gap (#3709,
* #6638): ollama-cloud is an apikey-category provider (not oauth), so the
* oauth-only `shouldUseQuotaSignal` gate in checkFallbackError skips the
* generic subscription-quota-text branch (#2321) for its 429s. Without a
* dedicated, ungated session check the account fell through to the generic
* 429 backoff (~3s, capped low) and got retried within the same 5-hour
* session window instead of cooling down for the session's duration —
* combo/LKGP routing cycled back to the "exhausted" account instead of
* advancing to the next one.
*
* This test proves: (1) the session-usage-limit text is classified as
* QUOTA_EXHAUSTED with a cooldown far longer than the generic backoff cap,
* for BOTH apikey and oauth provider categories, and (2) unrelated
* session-expired/auth wording and the sibling weekly-quota text are
* unaffected.
*/
import test from "node:test";
import assert from "node:assert/strict";
const { checkFallbackError } = await import("../../open-sse/services/accountFallback.ts");
const { isSessionUsageLimitText, buildSessionQuotaFallback, isWeeklyUsageLimitText } =
await import("../../open-sse/services/quotaTextCooldowns.ts");
const { RateLimitReason, BACKOFF_CONFIG } = await import("../../open-sse/config/constants.ts");
const { BACKOFF_CONFIG: ERROR_BACKOFF_CONFIG } = await import("../../open-sse/config/errorConfig.ts");
const SESSION_BODY = "you (acme-corp) have reached your session usage limit";
const SESSION_COOLDOWN_MS = 5 * 60 * 60 * 1000; // 5 hours
test("#7071 sanity: weekly text IS recognized (already fixed by #3709/#6638)", () => {
assert.equal(isWeeklyUsageLimitText("you (acme-corp) have reached your weekly usage limit"), true);
});
test("#7071 isSessionUsageLimitText matches the ollama-cloud 429 body", () => {
assert.equal(isSessionUsageLimitText(SESSION_BODY.toLowerCase()), true);
assert.equal(isSessionUsageLimitText("session limit reached, try later"), true);
assert.equal(isSessionUsageLimitText("rate_limit_exceeded: too many requests"), false);
// Must not false-positive on unrelated "session expired" auth errors.
assert.equal(isSessionUsageLimitText("your session has expired, please log in again"), false);
assert.equal(isSessionUsageLimitText("session token invalid"), false);
});
test("#7071 buildSessionQuotaFallback returns a 5h QUOTA_EXHAUSTED cooldown, far above the generic backoff cap", () => {
const result = buildSessionQuotaFallback(SESSION_BODY);
assert.ok(result, "expected a non-null fallback for session-usage-limit text");
assert.equal(result!.reason, RateLimitReason.QUOTA_EXHAUSTED);
assert.equal(result!.cooldownMs, SESSION_COOLDOWN_MS);
assert.ok(result!.cooldownMs > (ERROR_BACKOFF_CONFIG.max ?? BACKOFF_CONFIG.max));
});
test("#7071 buildSessionQuotaFallback returns null for unrelated error text", () => {
assert.equal(buildSessionQuotaFallback("rate_limit_exceeded: too many requests"), null);
assert.equal(buildSessionQuotaFallback("your session has expired, please log in again"), null);
});
test("#7071 BUG: checkFallbackError misclassifies ollama-cloud session-quota 429 as generic RATE_LIMIT_EXCEEDED instead of QUOTA_EXHAUSTED", () => {
const out = checkFallbackError(
429,
SESSION_BODY,
0, // backoffLevel
null, // model
"ollama-cloud", // provider (apikey category)
null, // headers
null, // profileOverride
null // structuredError
);
assert.equal(out.shouldFallback, true);
assert.equal(
out.reason,
RateLimitReason.QUOTA_EXHAUSTED,
`expected QUOTA_EXHAUSTED for session-usage-limit text, got reason=${out.reason} cooldownMs=${out.cooldownMs}`
);
assert.equal(out.cooldownMs, SESSION_COOLDOWN_MS);
});
test("#7071 checkFallbackError: oauth-category provider with session-limit text also gets the long cooldown", () => {
const out = checkFallbackError(429, SESSION_BODY, 0, null, "claude", null, null, null);
assert.equal(out.reason, RateLimitReason.QUOTA_EXHAUSTED);
assert.equal(out.cooldownMs, SESSION_COOLDOWN_MS);
});
test("#7071 checkFallbackError: ollama-cloud generic rate-limit body is unaffected (no false positive)", () => {
const out = checkFallbackError(
429,
"rate_limit_exceeded: too many requests",
0,
null,
"ollama-cloud",
null,
null,
null
);
assert.equal(out.reason, RateLimitReason.RATE_LIMIT_EXCEEDED);
assert.ok(
out.cooldownMs <= 2 * 60 * 1000,
"generic rate limit text must keep the normal short backoff, not the 5h session cooldown"
);
});

View File

@@ -0,0 +1,48 @@
import test from "node:test";
import assert from "node:assert/strict";
const { validateOpenAILikeProvider } = await import(
"../../src/lib/providers/validation/openaiFormat.ts"
);
test("#7284: a 429 chat-probe response is reported with a rate-limit warning, not plain valid", async () => {
const originalFetch = globalThis.fetch;
let callCount = 0;
globalThis.fetch = (async (url: string | URL | Request) => {
callCount += 1;
const href =
typeof url === "string" ? url : "url" in url ? url.url : url instanceof URL ? url.href : "";
if (href.includes("/models")) {
return new Response("not found", { status: 404 });
}
return new Response(JSON.stringify({ error: { message: "Too Many Requests" } }), {
status: 429,
});
}) as typeof fetch;
try {
const result = await validateOpenAILikeProvider({
provider: "opencode-zen",
apiKey: "test-key",
baseUrl: "https://opencode.ai/zen/v1",
modelId: "test-model",
providerSpecificData: {},
});
assert.equal(callCount, 2, "expected a /models probe followed by a chat probe");
const typedResult = result as { valid: boolean; error: string | null; warning?: string };
assert.equal(typedResult.valid, true, "429 on the chat probe should still be treated as valid");
assert.equal(typedResult.error, null);
assert.equal(
typeof typedResult.warning,
"string",
"429 response must carry a warning field signaling the rate limit"
);
assert.match(typedResult.warning as string, /rate limit/i);
} finally {
globalThis.fetch = originalFetch;
}
});

View File

@@ -29,6 +29,11 @@ const REAL_KIRO_IDS = [
"minimax-m2.5", // proven 200
"minimax-m2.1", // proven 200
"qwen3-coder-next", // proven 200
// Kiro's first OpenAI-family models, per kiro.dev/changelog/models
// (2026-07-14) — not yet independently live-VPS-verified like the ids above.
"gpt-5.6-sol",
"gpt-5.6-terra",
"gpt-5.6-luna",
];
test("kiro registry exposes no fabricated model ids", () => {

View File

@@ -0,0 +1,39 @@
import test from "node:test";
import assert from "node:assert/strict";
import { REGISTRY } from "@omniroute/open-sse/config/providers/index.ts";
const { getResolvedModelCapabilities } = await import("../../src/lib/modelCapabilities.ts");
// Kiro's first OpenAI-family models, announced 2026-07-14
// (kiro.dev/changelog/models): GPT-5.6 Sol / Terra / Luna, all sharing a
// 272k context window and a 128k max-output budget on the Kiro backend.
const GPT_5_6_KIRO_MODELS = ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"] as const;
test("kiro registry exposes the GPT-5.6 Sol/Terra/Luna model ids", () => {
const ids = new Set((REGISTRY.kiro?.models || []).map((m) => m.id));
for (const id of GPT_5_6_KIRO_MODELS) {
assert.ok(ids.has(id), `kiro registry must expose "${id}"`);
}
});
test("kiro GPT-5.6 models resolve the announced 272k context window", () => {
for (const model of GPT_5_6_KIRO_MODELS) {
const caps = getResolvedModelCapabilities({ provider: "kiro", model });
assert.equal(caps.contextWindow, 272000, `${model} must resolve a 272k context window`);
}
});
test("kiro GPT-5.6 models resolve a 128k max output budget", () => {
for (const model of GPT_5_6_KIRO_MODELS) {
const caps = getResolvedModelCapabilities({ provider: "kiro", model });
assert.equal(caps.maxOutputTokens, 128000, `${model} must resolve a 128k max output`);
}
});
test("kiro GPT-5.6 models resolve through the 'kr' provider alias too", () => {
for (const model of GPT_5_6_KIRO_MODELS) {
const caps = getResolvedModelCapabilities({ provider: "kr", model });
assert.equal(caps.contextWindow, 272000, `${model} must resolve via the 'kr' alias`);
}
});

View File

@@ -0,0 +1,211 @@
/**
* TDD for upstream 9router#1253 — Kiro auto-import "Bad credentials" when the
* cached AWS SSO token carries a direct `clientId` field (no `clientIdHash`).
*
* Newer kiro-auth-token.json files omit `clientIdHash` and instead store the
* OIDC `clientId` directly on the token object. Two related bugs combined to
* break refresh for these tokens:
*
* 1. `tryAwsSsoCache()` (auto-import/route.ts) only ever resolved
* clientId/clientSecret via `data.clientIdHash` -> `<hash>.json`. When the
* token instead carries a top-level `clientId`, this lookup silently does
* nothing, so the auto-import response comes back with clientId/clientSecret
* both null even though a matching client-registration file exists in the
* same cache dir.
* 2. Because auto-import lost the clientId/clientSecret pair, the dashboard's
* "Import Token" POST is sent as a plain (non-IDC) import, which routes
* through `KiroService.validateImportToken()` ->
* `readCachedClientCredentials()`. That helper scans *all* client
* registration files in `~/.aws/sso/cache` and picks one by
* region + latest-expiry, ignoring the token's own `clientId` entirely. On
* a machine with multiple stale SSO client registrations this can return a
* clientId/clientSecret pair that does not match the token's actual
* clientId, producing "Bad credentials" on refresh.
*
* Fix: both resolution paths must prefer the client-registration file whose
* `clientId` matches the token's own `clientId`, instead of a
* latest-expiry/region heuristic.
*/
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";
// ── Hermetic DATA_DIR so DB setup / requireLogin does not hit real disk ──────
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-kiro-1253-data-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.JWT_SECRET = process.env.JWT_SECRET || "test-jwt-secret-1253";
process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "test-api-key-secret-1253";
const core = await import("../../src/lib/db/core.ts");
const { GET } = await import("../../src/app/api/oauth/kiro/auto-import/route.ts");
const { KiroService } = await import("../../src/lib/oauth/services/kiro.ts");
const ORIGINAL_HOME = process.env.HOME;
const ORIGINAL_APPDATA = process.env.APPDATA;
const ORIGINAL_FETCH = globalThis.fetch;
let tmpHome: string;
test.beforeEach(() => {
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-kiro-1253-"));
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
process.env.HOME = tmpHome;
delete process.env.APPDATA;
globalThis.fetch = ORIGINAL_FETCH;
});
test.afterEach(() => {
process.env.HOME = ORIGINAL_HOME;
if (ORIGINAL_APPDATA !== undefined) {
process.env.APPDATA = ORIGINAL_APPDATA;
} else {
delete process.env.APPDATA;
}
globalThis.fetch = ORIGINAL_FETCH;
if (tmpHome) fs.rmSync(tmpHome, { recursive: true, force: true });
});
test.after(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
function cacheDirFor(home: string) {
return path.join(home, ".aws/sso/cache");
}
function writeJson(dir: string, file: string, data: Record<string, unknown>) {
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(path.join(dir, file), JSON.stringify(data));
}
async function callGet(): Promise<{ status: number; body: Record<string, unknown> }> {
const request = new Request("http://localhost/api/oauth/kiro/auto-import");
const response = await GET(request);
const body = (await response.json()) as Record<string, unknown>;
return { status: response.status, body };
}
// ── tryAwsSsoCache() (auto-import route) ─────────────────────────────────────
test("auto-import: resolves clientId/clientSecret from a direct `clientId` field (no clientIdHash) via matching registration file", async () => {
const cacheDir = cacheDirFor(tmpHome);
// The token file itself: no clientIdHash, only a direct `clientId`.
writeJson(cacheDir, "kiro-auth-token.json", {
accessToken: "aoa-access",
refreshToken: "aorAAAAAGrefresh-token",
clientId: "correct-client-id",
region: "us-east-1",
provider: "BuilderId",
authMethod: "IdC",
});
// Two STALE client registration files with a LATER expiresAt than the correct one —
// the old latest-expiry heuristic would wrongly prefer these.
writeJson(cacheDir, "stale-registration-1.json", {
clientId: "stale-client-id-1",
clientSecret: "stale-secret-1",
region: "us-east-1",
expiresAt: "2099-01-01T00:00:00Z",
});
writeJson(cacheDir, "stale-registration-2.json", {
clientId: "stale-client-id-2",
clientSecret: "stale-secret-2",
region: "us-east-1",
expiresAt: "2098-01-01T00:00:00Z",
});
// The registration file that actually matches the token's own clientId,
// deliberately given the OLDEST expiry so the heuristic must lose to the match.
writeJson(cacheDir, "correct-registration.json", {
clientId: "correct-client-id",
clientSecret: "correct-secret",
region: "us-east-1",
expiresAt: "2020-01-01T00:00:00Z",
});
const fetchedUrls: string[] = [];
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
const u = String(input);
fetchedUrls.push(u);
if (u.includes("oidc.") && u.endsWith("/token")) {
const bodyStr = String(init?.body || "{}");
const parsed = JSON.parse(bodyStr);
// Refresh must be attempted with the CORRECT client credentials.
assert.equal(parsed.clientId, "correct-client-id");
assert.equal(parsed.clientSecret, "correct-secret");
return new Response(
JSON.stringify({ accessToken: "access-refreshed", refreshToken: "aorAAAAAGrefreshed", expiresIn: 3600 }),
{ status: 200, headers: { "Content-Type": "application/json" } }
);
}
throw new Error(`[kiro-1253 test] unexpected fetch to ${u}`);
}) as typeof fetch;
const { body } = await callGet();
assert.equal(body.found, true, `expected found:true, got: ${JSON.stringify(body)}`);
assert.equal(
fetchedUrls.some((u) => u.includes("oidc.") && u.endsWith("/token")),
true,
`expected OIDC refresh to be attempted with resolved client creds, fetched: ${JSON.stringify(fetchedUrls)}`
);
});
// ── KiroService.readCachedClientCredentials() (via validateImportToken) ─────
test("KiroService.validateImportToken: prefers the client registration matching the token's own clientId over the latest-expiry heuristic", async () => {
const cacheDir = cacheDirFor(tmpHome);
writeJson(cacheDir, "stale-registration-1.json", {
clientId: "stale-client-id-1",
clientSecret: "stale-secret-1",
region: "us-east-1",
expiresAt: "2099-01-01T00:00:00Z",
});
writeJson(cacheDir, "correct-registration.json", {
clientId: "correct-client-id",
clientSecret: "correct-secret",
region: "us-east-1",
expiresAt: "2020-01-01T00:00:00Z",
});
const fetchedBodies: Record<string, unknown>[] = [];
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
const u = String(input);
if (u.includes("oidc.") && u.endsWith("/token")) {
const parsed = JSON.parse(String(init?.body || "{}"));
fetchedBodies.push(parsed);
if (parsed.clientId === "correct-client-id" && parsed.clientSecret === "correct-secret") {
return new Response(
JSON.stringify({ accessToken: "ok-access", refreshToken: "aorAAAAAGok", expiresIn: 3600 }),
{ status: 200, headers: { "Content-Type": "application/json" } }
);
}
return new Response(JSON.stringify({ message: "Bad credentials" }), { status: 400 });
}
throw new Error(`[kiro-1253 test] unexpected fetch to ${u}`);
}) as typeof fetch;
const kiroService = new KiroService();
const result = await kiroService.validateImportToken(
"aorAAAAAGrefresh-token",
"us-east-1",
"correct-client-id"
);
assert.equal(result.accessToken, "ok-access");
assert.ok(
fetchedBodies.some(
(b) => b.clientId === "correct-client-id" && b.clientSecret === "correct-secret"
),
`expected a refresh attempt using the matching client credentials, got: ${JSON.stringify(fetchedBodies)}`
);
});

View File

@@ -3,7 +3,7 @@
*
* Verifies:
* - Return shape matches §3.7 contract
* - Markdown table contains all 43 skill IDs
* - Markdown table contains all 44 skill IDs
* - Coverage bounds are within declared totals
* - metadata.source === "agent-skills-catalog"
* - metadata.generatedAt is an ISO datetime string
@@ -31,7 +31,7 @@ test("executeListCapabilities returns shape matching §3.7 contract", async () =
const { metadata } = result;
assert.ok(metadata, "metadata exists");
assert.equal(metadata.source, "agent-skills-catalog", "metadata.source matches");
assert.equal(metadata.totalSkills, 44, "metadata.totalSkills === 44 (43 + config)");
assert.equal(metadata.totalSkills, 45, "metadata.totalSkills === 45 (44 + config)");
assert.ok(metadata.coverage, "metadata.coverage exists");
assert.ok(metadata.coverage.api, "metadata.coverage.api exists");
assert.ok(metadata.coverage.cli, "metadata.coverage.cli exists");
@@ -39,12 +39,12 @@ test("executeListCapabilities returns shape matching §3.7 contract", async () =
assert.equal(metadata.coverage.cli.total, 20, "cli.total === 20");
});
test("executeListCapabilities markdown table contains all 43 API+CLI skill IDs", async () => {
test("executeListCapabilities markdown table contains all 44 API+CLI skill IDs", async () => {
const result = await executeListCapabilities(stubTask);
const content = result.artifacts[0].content;
const allIds = [...API_SKILL_IDS, ...CLI_SKILL_IDS] as string[];
assert.equal(allIds.length, 43, "API+CLI catalog declares 43 skill IDs");
assert.equal(allIds.length, 44, "API+CLI catalog declares 44 skill IDs");
for (const id of allIds) {
assert.ok(content.includes(id), `Markdown table missing skill ID: ${id}`);
@@ -58,11 +58,11 @@ test("metadata.coverage.api.have is within [0, 23]", async () => {
assert.ok(api.have <= 23, "api.have <= 23");
});
test("metadata.coverage.cli.have is within [0, 20]", async () => {
test("metadata.coverage.cli.have is within [0, 21]", async () => {
const result = await executeListCapabilities(stubTask);
const { cli } = result.metadata.coverage;
assert.ok(cli.have >= 0, "cli.have >= 0");
assert.ok(cli.have <= 20, "cli.have <= 20");
assert.ok(cli.have <= 21, "cli.have <= 21");
});
test("metadata.generatedAt is a valid ISO datetime", async () => {

View File

@@ -0,0 +1,202 @@
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import http from "node:http";
import net from "node:net";
import { getMainServerTimeoutConfig } from "../../src/shared/utils/runtimeTimeouts.ts";
// #7003 — JetBrains AI Assistant ("Test Connection" / completions) reported
// "HTTP/1.1 header parser received no bytes". The main OmniRoute server
// (scripts/dev/run-next.mjs) boots a bare `http.createServer(...)` and never
// configures `keepAliveTimeout`/`headersTimeout`, leaving Node's http.Server
// default of keepAliveTimeout=5_000ms with no `Keep-Alive: timeout=N` response
// hint. JetBrains AI Assistant's JVM `java.net.http.HttpClient` connection pool
// can reuse a socket idle for longer than that window; the server has already
// torn the socket down, so the client gets 0 response bytes back instead of a
// fresh HTTP response.
//
// This spec proves both halves:
// 1. `getMainServerTimeoutConfig()` raises the defaults well above Node's
// unconfigured 5_000ms window (the actual fix wired into run-next.mjs).
// 2. A bare http.Server left at Node's defaults drops a socket reused after
// an idle gap past 5s, while the same server configured via
// `getMainServerTimeoutConfig()` keeps serving the reused connection.
describe("#7003 getMainServerTimeoutConfig", () => {
it("defaults keepAliveTimeout/headersTimeout well above Node's 5_000ms default", () => {
const config = getMainServerTimeoutConfig({});
assert.equal(config.keepAliveTimeoutMs, 65_000);
assert.equal(config.headersTimeoutMs, 66_000);
assert.ok(config.keepAliveTimeoutMs > 5_000, "must exceed Node's unconfigured default");
assert.ok(
config.headersTimeoutMs > config.keepAliveTimeoutMs,
"headersTimeout must stay above keepAliveTimeout per Node's own requirement"
);
});
it("honors env overrides and keeps headersTimeout coherent with a raised keepAliveTimeout", () => {
const config = getMainServerTimeoutConfig({
MAIN_SERVER_KEEPALIVE_TIMEOUT_MS: "120000",
MAIN_SERVER_HEADERS_TIMEOUT_MS: "121000",
});
assert.equal(config.keepAliveTimeoutMs, 120_000);
assert.equal(config.headersTimeoutMs, 121_000);
});
it("bumps an inconsistent explicit headersTimeout override above keepAliveTimeout", () => {
const config = getMainServerTimeoutConfig({
MAIN_SERVER_KEEPALIVE_TIMEOUT_MS: "120000",
MAIN_SERVER_HEADERS_TIMEOUT_MS: "1000",
});
assert.equal(config.keepAliveTimeoutMs, 120_000);
assert.equal(config.headersTimeoutMs, 121_000);
});
it("falls back to defaults on invalid env values", () => {
const config = getMainServerTimeoutConfig({
MAIN_SERVER_KEEPALIVE_TIMEOUT_MS: "not-a-number",
});
assert.equal(config.keepAliveTimeoutMs, 65_000);
});
});
/**
* Sends a raw HTTP/1.1 GET over an already-connected keep-alive socket and
* resolves with whatever bytes arrive within a short settle window (empty
* string if nothing comes back — the exact "0 bytes back" failure mode
* JetBrains AI Assistant surfaces as "header parser received no bytes").
*
* The socket is opened with `allowHalfOpen: true` so it faithfully mimics a
* JVM/OkHttp-style client: Node's default `allowHalfOpen: false` proactively
* ends the writable side the instant it processes an incoming FIN, turning
* the reused write into a synchronous "socket has been ended" error instead
* of the real-world race — a write that is accepted locally (the server
* already destroyed the connection, so it never arrives) whose response
* settles as 0 bytes.
*/
function sendKeepAliveRequest(socket: net.Socket, port: number): Promise<string> {
return new Promise((resolve) => {
let received = "";
let settleTimer: NodeJS.Timeout;
const finish = () => {
socket.off("data", onData);
clearTimeout(settleTimer);
resolve(received);
};
// A short settle window once the full chunked response has arrived (fast
// path); a generous cap in case nothing ever comes back — the torn-down
// connection case this test proves, and a safety margin against first-run
// JIT/module-load jitter under the test runner.
const onData = (chunk: Buffer) => {
received += chunk.toString("utf8");
if (received.endsWith("0\r\n\r\n")) {
clearTimeout(settleTimer);
settleTimer = setTimeout(finish, 50);
}
};
socket.on("data", onData);
socket.write(`GET / HTTP/1.1\r\nHost: 127.0.0.1:${port}\r\nConnection: keep-alive\r\n\r\n`);
settleTimer = setTimeout(finish, 3_000);
});
}
function startEchoServer(configure: (server: http.Server) => void): Promise<http.Server> {
return new Promise((resolve) => {
const server = http.createServer((_req, res) => {
res.writeHead(200, { "content-type": "text/plain" });
res.end("ok");
});
configure(server);
server.listen(0, "127.0.0.1", () => resolve(server));
});
}
async function withServer(
configure: (server: http.Server) => void,
run: (port: number) => Promise<void>
): Promise<void> {
const server = await startEchoServer(configure);
try {
const address = server.address();
if (typeof address !== "object" || address === null) {
throw new Error("expected server to bind a TCP address");
}
await run(address.port);
} finally {
await new Promise<void>((resolve) => server.close(() => resolve()));
}
}
// Node's default keepAliveTimeout is 5_000ms, but the server only starts that
// timer once the response has fully flushed and there is a small amount of
// internal scheduling overhead before the socket is actually torn down —
// empirically ~5.8-6s end-to-end on loopback. 6.5s reliably clears that
// window without relying on a hair-trigger race.
const IDLE_GAP_MS = 6_500;
describe("#7003 keep-alive socket reuse across an idle gap", () => {
it(
"current Node defaults (keepAliveTimeout=5000ms): a pooled socket reused after 6.5s idle gets 0 bytes back",
{ timeout: 30_000 },
async () => {
await withServer(
() => {
/* leave Node's http.Server defaults untouched (keepAliveTimeout=5000ms) */
},
async (port) => {
const socket = net.connect({ port, host: "127.0.0.1", allowHalfOpen: true });
await new Promise<void>((resolve, reject) => {
socket.once("connect", () => resolve());
socket.once("error", reject);
});
const first = await sendKeepAliveRequest(socket, port);
assert.match(first, /200/, "first request on a fresh socket must succeed");
await new Promise((resolve) => setTimeout(resolve, IDLE_GAP_MS));
const second = await sendKeepAliveRequest(socket, port);
assert.equal(
second,
"",
"reusing the idle-torn-down socket must get exactly 0 bytes back (the reported bug)"
);
socket.destroy();
}
);
}
);
it(
"fixed config (getMainServerTimeoutConfig): the same reused connection stays alive past 6.5s idle",
{ timeout: 30_000 },
async () => {
const fixedTimeouts = getMainServerTimeoutConfig({});
await withServer(
(server) => {
server.keepAliveTimeout = fixedTimeouts.keepAliveTimeoutMs;
server.headersTimeout = fixedTimeouts.headersTimeoutMs;
},
async (port) => {
const socket = net.connect({ port, host: "127.0.0.1", allowHalfOpen: true });
await new Promise<void>((resolve, reject) => {
socket.once("connect", () => resolve());
socket.once("error", reject);
});
const first = await sendKeepAliveRequest(socket, port);
assert.match(first, /200/, "first request on a fresh socket must succeed");
await new Promise((resolve) => setTimeout(resolve, IDLE_GAP_MS));
const second = await sendKeepAliveRequest(socket, port);
assert.match(
second,
/200/,
"the reused connection must still get a valid response after the fix"
);
socket.destroy();
}
);
}
);
});

View File

@@ -0,0 +1,39 @@
import test from "node:test";
import assert from "node:assert";
import { getMainServerTimeoutConfig as mjsImpl } from "../../scripts/dev/main-server-timeouts.mjs";
import { getMainServerTimeoutConfig as tsImpl } from "../../src/shared/utils/runtimeTimeouts.ts";
// The shipped server-ws.mjs uses the SIBLING scripts/dev/main-server-timeouts.mjs
// (a ../../src import escapes the package after the dist copy — 2026-07-15 boot
// crash, #7065 class). This parity matrix is the anti-drift guard between the
// sibling and the canonical src/shared/utils/runtimeTimeouts.ts implementation.
const ENV_MATRIX: Record<string, string | undefined>[] = [
{},
{ MAIN_SERVER_KEEPALIVE_TIMEOUT_MS: "70000" },
{ MAIN_SERVER_HEADERS_TIMEOUT_MS: "80000" },
{ MAIN_SERVER_KEEPALIVE_TIMEOUT_MS: "90000", MAIN_SERVER_HEADERS_TIMEOUT_MS: "10000" },
{ MAIN_SERVER_KEEPALIVE_TIMEOUT_MS: "0", MAIN_SERVER_HEADERS_TIMEOUT_MS: "0" },
{ MAIN_SERVER_KEEPALIVE_TIMEOUT_MS: "abc" },
{ MAIN_SERVER_KEEPALIVE_TIMEOUT_MS: " " },
{ MAIN_SERVER_KEEPALIVE_TIMEOUT_MS: "-5" },
{ MAIN_SERVER_KEEPALIVE_TIMEOUT_MS: "1234.9" },
];
test("sibling main-server-timeouts.mjs stays in parity with runtimeTimeouts.ts", () => {
for (const env of ENV_MATRIX) {
assert.deepStrictEqual(
mjsImpl(env),
tsImpl(env),
`divergence for env ${JSON.stringify(env)}`
);
}
});
test("invalid values log through the provided logger in both implementations", () => {
const logsA: string[] = [];
const logsB: string[] = [];
mjsImpl({ MAIN_SERVER_KEEPALIVE_TIMEOUT_MS: "bogus" }, (m) => logsA.push(m));
tsImpl({ MAIN_SERVER_KEEPALIVE_TIMEOUT_MS: "bogus" }, (m) => logsB.push(m));
assert.strictEqual(logsA.length, 1);
assert.deepStrictEqual(logsA, logsB);
});

View File

@@ -0,0 +1,225 @@
import assert from "node:assert/strict";
import {
existsSync,
lstatSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
symlinkSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import test from "node:test";
// @ts-expect-error - JS build helper without type declarations
import {
materializeBundledSymlinks,
syncRebuiltNativeModuleIntoHashedEntries,
} from "../../scripts/build/assembleStandalone.mjs";
function makePkg(dir: string, name: string, marker: string) {
const pkgDir = join(dir, name);
mkdirSync(pkgDir, { recursive: true });
writeFileSync(join(pkgDir, "package.json"), JSON.stringify({ name, marker }));
return pkgDir;
}
test("materializeBundledSymlinks dereferences a live symlink into a real directory", () => {
const root = mkdtempSync(join(tmpdir(), "mbs-live-"));
try {
const realPkgHome = join(root, "external");
mkdirSync(realPkgHome, { recursive: true });
makePkg(realPkgHome, "ws", "real-ws");
const nm = join(root, "bundle", "node_modules");
mkdirSync(nm, { recursive: true });
symlinkSync(join(realPkgHome, "ws"), join(nm, "ws-a972e7ffa40ff725"), "dir");
const summary = materializeBundledSymlinks(nm);
assert.equal(summary.materialized, 1);
const target = join(nm, "ws-a972e7ffa40ff725");
assert.equal(lstatSync(target).isSymbolicLink(), false);
assert.equal(lstatSync(target).isDirectory(), true);
assert.equal(JSON.parse(readFileSync(join(target, "package.json"), "utf8")).marker, "real-ws");
} finally {
rmSync(root, { recursive: true, force: true });
}
});
test("materializeBundledSymlinks relinks a dangling hashed symlink to its sibling real package", () => {
const root = mkdtempSync(join(tmpdir(), "mbs-dangle-"));
try {
const nm = join(root, "node_modules");
mkdirSync(nm, { recursive: true });
// Sibling real package (as copied by copyNativeAssetsAndExtraModules).
makePkg(nm, "better-sqlite3", "real-bsq");
// Dangling absolute link into a build machine that does not exist here.
symlinkSync(
"/Users/runner/work/OmniRoute/OmniRoute/.build/next/standalone/node_modules/better-sqlite3",
join(nm, "better-sqlite3-90e2652d1716b047"),
"dir"
);
const summary = materializeBundledSymlinks(nm);
assert.equal(summary.relinked, 1);
const target = join(nm, "better-sqlite3-90e2652d1716b047");
assert.equal(lstatSync(target).isSymbolicLink(), false);
assert.equal(JSON.parse(readFileSync(join(target, "package.json"), "utf8")).marker, "real-bsq");
} finally {
rmSync(root, { recursive: true, force: true });
}
});
test("materializeBundledSymlinks drops a dangling link with no resolvable sibling", () => {
const root = mkdtempSync(join(tmpdir(), "mbs-drop-"));
try {
const nm = join(root, "node_modules");
mkdirSync(nm, { recursive: true });
symlinkSync(
"/nonexistent/build/machine/path/mystery",
join(nm, "mystery-deadbeefcafe0001"),
"dir"
);
const summary = materializeBundledSymlinks(nm);
assert.equal(summary.removed, 1);
assert.equal(existsSync(join(nm, "mystery-deadbeefcafe0001")), false);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
test("materializeBundledSymlinks handles scoped-package symlinks", () => {
const root = mkdtempSync(join(tmpdir(), "mbs-scope-"));
try {
const realPkgHome = join(root, "external");
mkdirSync(join(realPkgHome, "@huggingface"), { recursive: true });
makePkg(join(realPkgHome, "@huggingface"), "transformers", "real-hf");
const nm = join(root, "node_modules");
mkdirSync(join(nm, "@huggingface"), { recursive: true });
symlinkSync(
join(realPkgHome, "@huggingface", "transformers"),
join(nm, "@huggingface", "transformers-abc1234567890def"),
"dir"
);
const summary = materializeBundledSymlinks(nm);
assert.equal(summary.materialized, 1);
const target = join(nm, "@huggingface", "transformers-abc1234567890def");
assert.equal(lstatSync(target).isSymbolicLink(), false);
assert.equal(JSON.parse(readFileSync(join(target, "package.json"), "utf8")).marker, "real-hf");
} finally {
rmSync(root, { recursive: true, force: true });
}
});
test("materializeBundledSymlinks leaves real directories untouched and no-ops on missing dir", () => {
const root = mkdtempSync(join(tmpdir(), "mbs-noop-"));
try {
const nm = join(root, "node_modules");
mkdirSync(nm, { recursive: true });
makePkg(nm, "pino", "real-pino");
const summary = materializeBundledSymlinks(nm);
assert.deepEqual(summary, { materialized: 0, relinked: 0, removed: 0 });
assert.equal(existsSync(join(nm, "pino", "package.json")), true);
const missing = materializeBundledSymlinks(join(root, "does-not-exist"));
assert.deepEqual(missing, { materialized: 0, relinked: 0, removed: 0 });
} finally {
rmSync(root, { recursive: true, force: true });
}
});
test("syncRebuiltNativeModuleIntoHashedEntries overwrites a hashed entry with the rebuilt root module", () => {
const root = mkdtempSync(join(tmpdir(), "sync-hashed-"));
try {
const rootModule = makePkg(root, "better-sqlite3", "electron-abi-rebuilt");
const nm = join(root, "nested", "node_modules");
makePkg(nm, "better-sqlite3-90e2652d1716b047", "stale-node-abi");
const summary = syncRebuiltNativeModuleIntoHashedEntries(rootModule, nm);
assert.equal(summary.synced, 1);
const target = join(nm, "better-sqlite3-90e2652d1716b047");
assert.equal(
JSON.parse(readFileSync(join(target, "package.json"), "utf8")).marker,
"electron-abi-rebuilt"
);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
test("syncRebuiltNativeModuleIntoHashedEntries overwrites a plain-named entry too", () => {
const root = mkdtempSync(join(tmpdir(), "sync-plain-"));
try {
const rootModule = makePkg(root, "better-sqlite3", "electron-abi-rebuilt");
const nm = join(root, "nested", "node_modules");
makePkg(nm, "better-sqlite3", "stale-node-abi");
const summary = syncRebuiltNativeModuleIntoHashedEntries(rootModule, nm);
assert.equal(summary.synced, 1);
assert.equal(
JSON.parse(readFileSync(join(nm, "better-sqlite3", "package.json"), "utf8")).marker,
"electron-abi-rebuilt"
);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
test("syncRebuiltNativeModuleIntoHashedEntries no-ops when root module or nested node_modules is missing", () => {
const root = mkdtempSync(join(tmpdir(), "sync-noop-"));
try {
const rootModule = join(root, "does-not-exist", "better-sqlite3");
const nm = join(root, "nested", "node_modules");
makePkg(nm, "better-sqlite3", "stale-node-abi");
const missingRoot = syncRebuiltNativeModuleIntoHashedEntries(rootModule, nm);
assert.deepEqual(missingRoot, { synced: 0 });
const realRootModule = makePkg(root, "better-sqlite3", "electron-abi-rebuilt");
const missingNm = syncRebuiltNativeModuleIntoHashedEntries(
realRootModule,
join(root, "does-not-exist-nm")
);
assert.deepEqual(missingNm, { synced: 0 });
} finally {
rmSync(root, { recursive: true, force: true });
}
});
test("syncRebuiltNativeModuleIntoHashedEntries leaves unrelated entries untouched", () => {
const root = mkdtempSync(join(tmpdir(), "sync-unrelated-"));
try {
const rootModule = makePkg(root, "better-sqlite3", "electron-abi-rebuilt");
const nm = join(root, "nested", "node_modules");
makePkg(nm, "pino", "real-pino");
makePkg(nm, "better-sqlite3-helper", "unrelated-package");
const summary = syncRebuiltNativeModuleIntoHashedEntries(rootModule, nm);
assert.deepEqual(summary, { synced: 0 });
assert.equal(
JSON.parse(readFileSync(join(nm, "pino", "package.json"), "utf8")).marker,
"real-pino"
);
assert.equal(
JSON.parse(readFileSync(join(nm, "better-sqlite3-helper", "package.json"), "utf8")).marker,
"unrelated-package"
);
} finally {
rmSync(root, { recursive: true, force: true });
}
});

View File

@@ -0,0 +1,102 @@
import test from "node:test";
import assert from "node:assert/strict";
// 9router#2482: MiniMax Text-to-Image returns "404 page not found".
// MiniMax already has entries in musicRegistry.ts/audioRegistry.ts/videoRegistry.ts,
// but no entry at all in imageRegistry.ts (nor a dedicated provider handler under
// open-sse/handlers/imageGeneration/providers/), so a MiniMax image-model request
// falls through the format dispatch in imageGeneration.ts to a 400/unmatched-format
// path instead of reaching MiniMax's image_generation endpoint.
//
// handleImageGeneration is imported statically (not dynamically inside a test) so
// its transitive imports (e.g. the proxy-aware fetch dispatcher) finish installing
// their own globalThis.fetch wrapper before any test reassigns it for mocking —
// a dynamic import after the mock assignment would let that wrapper silently
// clobber the test's mock and hit the real network.
const { getImageProvider } = await import("../../open-sse/config/imageRegistry.ts");
const { handleImageGeneration } = await import("../../open-sse/handlers/imageGeneration.ts");
test("MiniMax is registered as an image provider with a dedicated minimax-image format", () => {
const cfg = getImageProvider("minimax");
assert.ok(cfg, "expected an IMAGE_PROVIDERS entry for minimax");
assert.equal(cfg.id, "minimax");
assert.equal(
cfg.format,
"minimax-image",
"MiniMax image_generation is not OpenAI-compatible, must use its own format"
);
assert.equal(cfg.authType, "apikey");
assert.equal(cfg.authHeader, "bearer");
assert.match(
cfg.baseUrl,
/api\.minimax\.io\/v1\/image_generation$/,
"image baseUrl must target MiniMax's image_generation endpoint"
);
});
test("MiniMax image provider exposes at least one text-to-image model", () => {
const cfg = getImageProvider("minimax");
const ids = (cfg?.models || []).map((m) => m.id);
assert.ok(ids.length > 0, `expected at least one MiniMax image model, got: ${ids.join(", ")}`);
assert.ok(
Array.isArray(cfg?.supportedSizes) && cfg.supportedSizes.length > 0,
"image provider must declare at least one supported size"
);
});
test("handleImageGeneration dispatches minimax-image format to the MiniMax handler and normalizes the response", async () => {
const originalFetch = globalThis.fetch;
try {
let fetchCalled = false;
globalThis.fetch = (async (url: string) => {
fetchCalled = true;
assert.match(String(url), /api\.minimax\.io\/v1\/image_generation$/);
return {
ok: true,
status: 200,
json: async () => ({
id: "abc123",
data: { image_urls: ["https://cdn.minimax.io/generated/one.png"] },
base_resp: { status_code: 0, status_msg: "success" },
}),
} as unknown as Response;
}) as typeof fetch;
const result = await handleImageGeneration({
body: { model: "minimax/image-01", prompt: "a red panda in the snow", n: 1 },
credentials: { apiKey: "test-key" },
log: null,
});
assert.equal(fetchCalled, true, "expected the MiniMax handler to call fetch");
assert.equal(result.success, true, `expected success, got: ${JSON.stringify(result)}`);
assert.ok(Array.isArray(result.data?.data) && result.data.data.length === 1);
assert.equal(result.data.data[0].url, "https://cdn.minimax.io/generated/one.png");
} finally {
globalThis.fetch = originalFetch;
}
});
test("handleImageGeneration surfaces MiniMax upstream errors without a network 404", async () => {
const originalFetch = globalThis.fetch;
try {
globalThis.fetch = (async () => {
return {
ok: false,
status: 401,
text: async () => "login fail: invalid API key",
} as unknown as Response;
}) as typeof fetch;
const result = await handleImageGeneration({
body: { model: "minimax/image-01", prompt: "a red panda in the snow", n: 1 },
credentials: { apiKey: "bad-key" },
log: null,
});
assert.equal(result.success, false);
assert.equal(result.status, 401);
} finally {
globalThis.fetch = originalFetch;
}
});

View File

@@ -29,13 +29,11 @@ describe("MiniMax M3 model registration (#3110)", () => {
assert.equal(m3.contextLength, 1_048_576);
});
it("opencode provider has minimax-m3-free with 1M context", () => {
it("opencode provider does NOT list minimax-m3-free (#6998 — delisted upstream, 401)", () => {
const entry = REGISTRY.opencode;
assert.ok(entry, "opencode registry entry must exist");
const m3 = entry.models.find((m) => m.id === "minimax-m3-free");
assert.ok(m3, "minimax-m3-free must be in opencode models");
assert.equal(m3.name, "MiniMax M3 Free");
assert.equal(m3.contextLength, 1_048_576);
assert.equal(m3, undefined, "minimax-m3-free was delisted from OpenCode Zen's free tier (#6998)");
});
it("opencode-go provider has minimax-m3 with Claude targetFormat", () => {

Some files were not shown because too many files have changed in this diff Show More