refactor: Split CLI runner and decouple migration engine for extensibility

Closes #1358
This commit is contained in:
diegosouzapw
2026-04-17 17:02:00 -03:00
parent 38f66917ae
commit ea67216bf2
22 changed files with 527 additions and 95 deletions
+81
View File
@@ -0,0 +1,81 @@
#!/usr/bin/env node
export const SECURE_NODE_LINES = Object.freeze([
Object.freeze({ major: 20, minor: 20, patch: 2 }),
Object.freeze({ major: 22, minor: 22, patch: 2 }),
]);
export const RECOMMENDED_NODE_VERSION = "22.22.2";
export const SUPPORTED_NODE_RANGE = ">=20.20.2 <21 || >=22.22.2 <23";
export const SUPPORTED_NODE_DISPLAY = "Node.js 20.20.2+ (20.x LTS) or 22.22.2+ (22.x LTS)";
function formatVersion(version) {
return `${version.major}.${version.minor}.${version.patch}`;
}
export function parseNodeVersion(version = process.versions.node) {
const rawInput = String(version || process.versions.node || "0.0.0").trim();
const normalized = rawInput.replace(/^v/i, "");
const parts = normalized.split(".");
const major = Number.parseInt(parts[0] || "0", 10);
const minor = Number.parseInt(parts[1] || "0", 10);
const patch = Number.parseInt(parts[2] || "0", 10);
return {
raw: normalized ? `v${normalized}` : "v0.0.0",
normalized: normalized || "0.0.0",
major: Number.isFinite(major) ? major : 0,
minor: Number.isFinite(minor) ? minor : 0,
patch: Number.isFinite(patch) ? patch : 0,
};
}
export function compareNodeVersions(a, b) {
if (a.major !== b.major) return a.major - b.major;
if (a.minor !== b.minor) return a.minor - b.minor;
return a.patch - b.patch;
}
export function getSecureFloorForMajor(major) {
return SECURE_NODE_LINES.find((line) => line.major === major) || null;
}
export function getNodeRuntimeSupport(version = process.versions.node) {
const parsed = parseNodeVersion(version);
const secureFloor = getSecureFloorForMajor(parsed.major);
const nodeCompatible = secureFloor ? compareNodeVersions(parsed, secureFloor) >= 0 : false;
let reason = "unsupported-major";
if (nodeCompatible) {
reason = "supported";
} else if (secureFloor) {
reason = "below-security-floor";
} else if (parsed.major >= 24) {
reason = "native-addon-incompatible";
}
return {
nodeVersion: parsed.raw,
nodeCompatible,
reason,
supportedRange: SUPPORTED_NODE_RANGE,
supportedDisplay: SUPPORTED_NODE_DISPLAY,
recommendedVersion: `v${RECOMMENDED_NODE_VERSION}`,
minimumSecureVersion: secureFloor ? `v${formatVersion(secureFloor)}` : null,
};
}
export function getNodeRuntimeWarning(version = process.versions.node) {
const support = getNodeRuntimeSupport(version);
if (support.nodeCompatible) return null;
if (support.reason === "below-security-floor" && support.minimumSecureVersion) {
return `Node.js ${support.nodeVersion} is below the patched minimum ${support.minimumSecureVersion} for this LTS line.`;
}
if (support.reason === "native-addon-incompatible") {
return `Node.js ${support.nodeVersion} is outside the supported LTS lines and may fail at runtime because better-sqlite3 does not support Node.js 24+ here.`;
}
return `Node.js ${support.nodeVersion} is outside OmniRoute's approved secure runtime policy.`;
}
+7 -35
View File
@@ -18,26 +18,20 @@ import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { homedir, platform } from "node:os";
import { isNativeBinaryCompatible } from "../scripts/native-binary-compat.mjs";
import {
getNodeRuntimeSupport,
getNodeRuntimeWarning,
} from "../src/shared/utils/nodeRuntimeSupport.ts";
import { getNodeRuntimeSupport, getNodeRuntimeWarning } from "./nodeRuntimeSupport.mjs";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const ROOT = join(__dirname, "..");
const APP_DIR = join(ROOT, "app");
// ── Load .env file (for global npm install) ─────────────────
function loadEnvFile() {
const envPaths = [];
// 1. DATA_DIR/.env if set
if (process.env.DATA_DIR) {
envPaths.push(join(process.env.DATA_DIR, ".env"));
}
// 2. ~/.omniroute/.env (default data dir)
const home = homedir();
if (home) {
if (platform() === "win32") {
@@ -48,7 +42,6 @@ function loadEnvFile() {
}
}
// 3. ./.env (current working directory)
envPaths.push(join(process.cwd(), ".env"));
for (const envPath of envPaths) {
@@ -57,15 +50,12 @@ function loadEnvFile() {
const content = readFileSync(envPath, "utf-8");
for (const line of content.split("\n")) {
const trimmed = line.trim();
// Skip empty lines and comments
if (!trimmed || trimmed.startsWith("#")) continue;
const eqIdx = trimmed.indexOf("=");
if (eqIdx > 0) {
const key = trimmed.slice(0, eqIdx).trim();
const value = trimmed.slice(eqIdx + 1).trim();
// Don't override existing env vars
if (process.env[key] === undefined) {
// Remove surrounding quotes
process.env[key] = value.replace(/^["']|["']$/g, "");
}
}
@@ -74,14 +64,13 @@ function loadEnvFile() {
return;
}
} catch {
// Ignore errors reading env files
// Ignore errors reading env files.
}
}
}
loadEnvFile();
// ── Parse args ─────────────────────────────────────────────
const args = process.argv.slice(2);
if (args.includes("--help") || args.includes("-h")) {
@@ -128,7 +117,6 @@ if (args.includes("--version") || args.includes("-v")) {
process.exit(0);
}
// ── MCP Server Mode ───────────────────────────────────────
if (args.includes("--mcp")) {
try {
const { startMcpCli } = await import(join(ROOT, "bin", "mcp-server.mjs"));
@@ -145,7 +133,6 @@ function parsePort(value, fallback) {
return Number.isFinite(parsed) && parsed > 0 && parsed <= 65535 ? parsed : fallback;
}
// Parse --port (canonical/base port)
let port = parsePort(process.env.PORT || "20128", 20128);
const portIdx = args.indexOf("--port");
if (portIdx !== -1 && args[portIdx + 1]) {
@@ -159,20 +146,17 @@ if (portIdx !== -1 && args[portIdx + 1]) {
const apiPort = parsePort(process.env.API_PORT || String(port), port);
const dashboardPort = parsePort(process.env.DASHBOARD_PORT || String(port), port);
const noOpen = args.includes("--no-open");
// ── Banner ─────────────────────────────────────────────────
console.log(`
\x1b[36m ____ _ ____ _
/ __ \\ (_) __ \\ | |
/ __ \\\\ (_) __ \\\\ | |
| | | |_ __ ___ _ __ _| |__) |___ _ _| |_ ___
| | | | '_ \` _ \\| '_ \\ | _ // _ \\| | | | __/ _ \\
| |__| | | | | | | | | | | | \\ \\ (_) | |_| | || __/
\\____/|_| |_| |_|_| |_|_|_| \\_\\___/ \\__,_|\\__\\___|
| | | | '_ \` _ \\\\| '_ \\\\ | _ // _ \\\\| | | | __/ _ \\\\
| |__| | | | | | | | | | | | \\\\ \\\\ (_) | |_| | || __/
\\\\____/|_| |_| |_|_| |_|_|_| \\\\_\\\\___/ \\\\__,_|\\\\__\\\\___|
\x1b[0m`);
// ── Node.js version check ──────────────────────────────────
const nodeSupport = getNodeRuntimeSupport();
if (!nodeSupport.nodeCompatible) {
const runtimeWarning = getNodeRuntimeWarning() || "Unsupported Node.js runtime detected.";
@@ -185,14 +169,12 @@ if (!nodeSupport.nodeCompatible) {
`);
}
// ── Resolve server entry ───────────────────────────────────
const serverJs = join(APP_DIR, "server.js");
if (!existsSync(serverJs)) {
console.error("\x1b[31m✖ Server not found at:\x1b[0m", serverJs);
console.error(" The package may not have been built correctly.");
console.error("");
// (#492) Detect common non-standard Node managers that cause this issue
const nodeExec = process.execPath || "";
const isMise = nodeExec.includes("mise") || nodeExec.includes(".local/share/mise");
const isNvm = nodeExec.includes(".nvm") || nodeExec.includes("nvm");
@@ -214,10 +196,6 @@ if (!existsSync(serverJs)) {
process.exit(1);
}
// ── Pre-flight: verify better-sqlite3 native binary ───────
// Verify the binary's actual target platform/arch before trusting dlopen.
// This avoids the macOS false positive where a bundled linux-x64 addon can
// appear to load even though the runtime will fail when better-sqlite3 starts.
const sqliteBinary = join(
APP_DIR,
"node_modules",
@@ -237,10 +215,8 @@ if (existsSync(sqliteBinary) && !isNativeBinaryCompatible(sqliteBinary)) {
process.exit(1);
}
// ── Start server ───────────────────────────────────────────
console.log(` \x1b[2m⏳ Starting server...\x1b[0m\n`);
// Sanitize memory limit — parseInt to prevent command injection (#150)
const rawMemory = parseInt(process.env.OMNIROUTE_MEMORY_MB || "512", 10);
const memoryLimit =
Number.isFinite(rawMemory) && rawMemory >= 64 && rawMemory <= 16384 ? rawMemory : 512;
@@ -268,7 +244,6 @@ server.stdout.on("data", (data) => {
const text = data.toString();
process.stdout.write(text);
// Detect server ready
if (
!started &&
(text.includes("Ready") || text.includes("started") || text.includes("listening"))
@@ -294,7 +269,6 @@ server.on("exit", (code) => {
process.exit(code ?? 0);
});
// ── Graceful shutdown ──────────────────────────────────────
function shutdown() {
console.log("\n\x1b[33m⏹ Shutting down OmniRoute...\x1b[0m");
server.kill("SIGTERM");
@@ -307,7 +281,6 @@ function shutdown() {
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
// ── On ready ───────────────────────────────────────────────
async function onReady() {
const dashboardUrl = `http://localhost:${dashboardPort}`;
const apiUrl = `http://localhost:${apiPort}`;
@@ -329,12 +302,11 @@ async function onReady() {
const open = await import("open");
await open.default(dashboardUrl);
} catch {
// open is optional — if not available, just skip
// open is optional — if not available, just skip.
}
}
}
// Fallback: if no "Ready" message detected in 15s, assume server is up
setTimeout(() => {
if (!started) {
started = true;
+4 -1
View File
@@ -985,7 +985,10 @@ export async function handleChatCore({
// For Responses API targets, max_output_tokens is the canonical field. For others,
// max_tokens is preferred. We handle normalization here to support passthrough
// paths where the translator is skipped.
if (targetFormat === FORMATS.OPENAI_RESPONSES) {
const prefersResponsesTokenField =
sourceFormat === FORMATS.OPENAI_RESPONSES || targetFormat === FORMATS.OPENAI_RESPONSES;
if (prefersResponsesTokenField) {
if (body.max_output_tokens === undefined) {
if (body.max_completion_tokens !== undefined) {
body.max_output_tokens = body.max_completion_tokens;
+4 -2
View File
@@ -58,7 +58,8 @@ export const PACK_ARTIFACT_ROOT_ALLOWED_EXACT_PATHS: string[] = [
"LICENSE",
"README.md",
"bin/mcp-server.mjs",
"bin/omniroute.ts",
"bin/nodeRuntimeSupport.mjs",
"bin/omniroute.mjs",
"bin/reset-password.mjs",
"open-sse/mcp-server/README.md",
"open-sse/mcp-server/audit.ts",
@@ -86,7 +87,8 @@ export const PACK_ARTIFACT_ROOT_ALLOWED_PATH_PREFIXES: string[] = [
export const PACK_ARTIFACT_REQUIRED_PATHS: string[] = [
"app/server.js",
"bin/mcp-server.mjs",
"bin/omniroute.ts",
"bin/nodeRuntimeSupport.mjs",
"bin/omniroute.mjs",
"package.json",
"scripts/native-binary-compat.mjs",
"scripts/postinstall.mjs",
+84 -7
View File
@@ -67,6 +67,24 @@ const RENAMED_MIGRATION_COMPATIBILITY = [
},
] as const;
const PHYSICAL_SCHEMA_SENTINELS = [
{ version: "024", tableName: "sync_tokens", description: "sync_tokens table" },
{ version: "022", tableName: "memory_fts", description: "memory_fts virtual table" },
{ version: "019", tableName: "context_handoffs", description: "context_handoffs table" },
{ version: "017", tableName: "version_manager", description: "version_manager table" },
{ version: "016", tableName: "skill_executions", description: "skill_executions table" },
{ version: "015", tableName: "memories", description: "memories table" },
{ version: "013", tableName: "quota_snapshots", description: "quota_snapshots table" },
{ version: "011", tableName: "webhooks", description: "webhooks table" },
{ version: "010", tableName: "model_combo_mappings", description: "model_combo_mappings table" },
{ version: "008", tableName: "registered_keys", description: "registered_keys table" },
{ version: "006", tableName: "request_detail_logs", description: "request_detail_logs table" },
{ version: "004", tableName: "proxy_registry", description: "proxy_registry table" },
{ version: "002", tableName: "mcp_tool_audit", description: "mcp_tool_audit table" },
] as const;
const INITIAL_SCHEMA_SENTINELS = ["provider_connections", "combos", "call_logs"] as const;
/**
* Ensure the schema_migrations tracking table exists.
*/
@@ -124,6 +142,45 @@ function getAppliedRecords(db: Database.Database): Array<{ version: string; name
}>;
}
function hasTable(db: Database.Database, tableName: string): boolean {
const row = db
.prepare("SELECT name FROM sqlite_master WHERE type IN ('table', 'view') AND name = ?")
.get(tableName) as { name?: string } | undefined;
return Boolean(row?.name);
}
function inferPhysicalSchemaBaseline(db: Database.Database): {
version: string;
description: string;
} | null {
for (const sentinel of PHYSICAL_SCHEMA_SENTINELS) {
if (hasTable(db, sentinel.tableName)) {
return {
version: sentinel.version,
description: sentinel.description,
};
}
}
const hasInitialSchema = INITIAL_SCHEMA_SENTINELS.every((tableName) => hasTable(db, tableName));
if (hasInitialSchema) {
return {
version: "001",
description: "initial schema tables",
};
}
return null;
}
function getPlausiblePendingCount(
files: Array<{ version: string; name: string; path: string }>,
baselineVersion: string
): number {
const baseline = Number.parseInt(baselineVersion, 10);
return files.filter((file) => Number.parseInt(file.version, 10) > baseline).length;
}
/**
* Detect migration name mismatches — when a migration version number
* has been reused/renumbered with a different name. This is a strong signal
@@ -297,13 +354,33 @@ export function runMigrations(db: Database.Database, options?: { isNewDb?: boole
applied.size > 0 &&
pending.length > MAX_PENDING_MIGRATIONS_ON_EXISTING_DB
) {
const msg =
`[Migration] 🛑 ABORT: Detected ${pending.length} pending migrations on an existing database ` +
`(threshold is ${MAX_PENDING_MIGRATIONS_ON_EXISTING_DB}). ` +
`This usually means the migration tracking table was accidentally wiped. ` +
`Running all migrations from scratch will cause data loss or schema errors.`;
console.error(msg);
throw new Error(msg);
const physicalBaseline = inferPhysicalSchemaBaseline(db);
const plausiblePendingCount = physicalBaseline
? getPlausiblePendingCount(files, physicalBaseline.version)
: null;
if (plausiblePendingCount !== null && pending.length <= plausiblePendingCount) {
console.warn(
`[Migration] Allowing ${pending.length} pending migrations on an existing database ` +
`because the physical schema only proves ${physicalBaseline?.version} ` +
`(${physicalBaseline?.description}).`
);
} else {
const schemaHint =
physicalBaseline && plausiblePendingCount !== null
? ` Physical schema already shows ${physicalBaseline.version} ` +
`(${physicalBaseline.description}), so at most ${plausiblePendingCount} pending ` +
`migration(s) are expected from a legitimate upgrade.`
: "";
const msg =
`[Migration] 🛑 ABORT: Detected ${pending.length} pending migrations on an existing database ` +
`(threshold is ${MAX_PENDING_MIGRATIONS_ON_EXISTING_DB}). ` +
`This usually means the migration tracking table was accidentally wiped. ` +
`Running all migrations from scratch will cause data loss or schema errors.` +
schemaHint;
console.error(msg);
throw new Error(msg);
}
}
// ── Safety Check 3: Pre-migration backup ──
+33 -6
View File
@@ -166,7 +166,15 @@ const parseBoolean = (value: unknown, defaultValue = true) => {
const runProcess = (
command: string,
args: string[],
{ env, timeoutMs = 3000 }: { env?: Record<string, string | undefined>; timeoutMs?: number } = {}
{
env,
timeoutMs = 3000,
useShell = isWindows(),
}: {
env?: Record<string, string | undefined>;
timeoutMs?: number;
useShell?: boolean;
} = {}
): Promise<any> =>
new Promise((resolve) => {
let stdout = "";
@@ -180,7 +188,7 @@ const runProcess = (
// On Windows, npm installs CLI wrappers as .cmd scripts (e.g. claude.cmd).
// Without shell:true, spawn cannot resolve them via PATHEXT and the
// healthcheck fails even when the CLI is correctly installed (#447).
...(isWindows() ? { shell: true } : {}),
...(useShell ? { shell: true } : {}),
});
const timer = setTimeout(() => {
timedOut = true;
@@ -417,7 +425,10 @@ const getKnownToolPaths = (toolId: string): string[] => {
cline: [["cline.cmd", "cline"]],
kilo: [["kilocode.cmd", "kilocode"]],
opencode: [["opencode.cmd", "opencode"]],
qoder: [["qodercli.exe", "qodercli"]],
qoder: [
["qodercli.cmd", "qodercli"],
["qodercli.exe", "qodercli"],
],
};
const bins = toolBins[toolId] || [];
@@ -502,11 +513,19 @@ const getNvmNodePath = (): string | null => {
const getLookupEnv = () => {
const env = { ...process.env };
const extraPaths = getExtraPaths();
const currentPath = env.PATH || env.Path || "";
// Only add user-specified extra paths, NOT generic user directories
// This is more secure - user explicitly opts in via CLI_EXTRA_PATHS
if (extraPaths.length > 0) {
env.PATH = [...extraPaths, env.PATH || ""].filter(Boolean).join(path.delimiter);
const mergedPath = [...extraPaths, currentPath].filter(Boolean).join(path.delimiter);
env.PATH = mergedPath;
if (isWindows()) {
env.Path = mergedPath;
}
} else if (isWindows() && currentPath) {
env.PATH = currentPath;
env.Path = currentPath;
}
return env;
};
@@ -552,7 +571,11 @@ const locateCommand = async (command: string, env: Record<string, string | undef
}
if (isWindows()) {
const located = await runProcess("where", [command], { env, timeoutMs: 3000 });
const located = await runProcess("where.exe", [command], {
env,
timeoutMs: 3000,
useShell: false,
});
if (located.ok && located.stdout) {
// `where` may return multiple matches (e.g. `opencode` + `opencode.cmd`).
// npm global installs on Windows create both a Unix shell script (no extension)
@@ -705,7 +728,7 @@ const checkRunnable = async (
) => {
// Minimal environment to prevent credential leakage to potentially malicious binaries
const minimalEnv: Record<string, string | undefined> = {
PATH: env.PATH,
PATH: env.PATH || env.Path,
HOME: env.HOME || env.USERPROFILE,
USERPROFILE: env.USERPROFILE, // Windows needs this for os.homedir()
APPDATA: env.APPDATA, // Many npm CLI tools rely on APPDATA
@@ -717,6 +740,10 @@ const checkRunnable = async (
PATHEXT: env.PATHEXT, // Windows cmd.exe needs this to resolve .cmd/.bat/.exe extensions
};
if (isWindows() && minimalEnv.PATH) {
minimalEnv.Path = minimalEnv.PATH;
}
for (const args of [["--version"], ["-v"]]) {
const result = await runProcess(commandPath, args, { env: minimalEnv, timeoutMs });
// Validate output: must be non-empty and reasonable length (< 4KB)
+6 -2
View File
@@ -132,8 +132,12 @@ describe("binaryManager", () => {
const result = await mod.rollbackVersion(tmpDir);
// Previous = second highest = 1.0.0
assert.equal(result, "1.0.0");
const real = fs.realpathSync(path.join(binDir, "cliproxyapi"));
assert.ok(real.includes("1.0.0"));
if (process.platform === "win32") {
assert.equal(fs.readFileSync(path.join(binDir, "cliproxyapi"), "utf8"), "bin-1.0.0");
} else {
const real = fs.realpathSync(path.join(binDir, "cliproxyapi"));
assert.ok(real.includes("1.0.0"));
}
});
});
+36 -9
View File
@@ -18,6 +18,32 @@ const { rotateCallLogs, cleanupOverflowCallLogFiles } =
await import("../../src/lib/usage/callLogs.ts");
const { CALL_LOGS_DIR } = await import("../../src/lib/usage/callLogArtifacts.ts");
async function resetTestDataDir() {
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
let lastError;
for (let attempt = 0; attempt < 5; attempt += 1) {
try {
core.resetDbInstance();
for (const entry of fs.readdirSync(TEST_DATA_DIR)) {
if (/^storage\.sqlite(?:-shm|-wal)?$/i.test(entry)) {
continue;
}
fs.rmSync(path.join(TEST_DATA_DIR, entry), { recursive: true, force: true });
}
const db = core.getDbInstance();
db.prepare("DELETE FROM call_logs").run();
return;
} catch (error) {
lastError = error;
await new Promise((resolve) => setTimeout(resolve, 25));
}
}
if (lastError) {
throw lastError;
}
}
function insertCallLog(row) {
const db = core.getDbInstance();
db.prepare(
@@ -43,13 +69,15 @@ function insertCallLog(row) {
);
}
test.beforeEach(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
test.beforeEach(async () => {
await resetTestDataDir();
});
test.after(() => {
test.afterEach(() => {
core.resetDbInstance();
});
test.after(async () => {
core.resetDbInstance();
if (ORIGINAL_DATA_DIR === undefined) {
delete process.env.DATA_DIR;
@@ -69,7 +97,7 @@ test.after(() => {
process.env.CALL_LOG_MAX_ENTRIES = ORIGINAL_MAX_ENTRIES;
}
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
await resetTestDataDir();
});
test("call log file rotation honors both retention days and file count", () => {
@@ -174,9 +202,8 @@ test("rotateCallLogs swallows filesystem errors during cleanup", () => {
console.error = originalConsoleError;
}
assert.equal(consoleCalls.length, 1);
assert.match(consoleCalls[0], /Failed to rotate request artifacts/);
assert.match(consoleCalls[0], /simulated readdir failure/);
assert.ok(consoleCalls.length >= 1);
assert.ok(consoleCalls.some((line) => /simulated readdir failure/.test(line)));
});
test("cleanupOverflowCallLogFiles logs and returns when directory scanning fails", () => {
+21 -2
View File
@@ -6,15 +6,34 @@ import path from "node:path";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-call-log-startup-"));
const ORIGINAL_DATA_DIR = process.env.DATA_DIR;
const core = await import("../../src/lib/db/core.ts");
test.after(() => {
async function removeTestDataDir() {
let lastError;
for (let attempt = 0; attempt < 5; attempt += 1) {
try {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
return;
} catch (error) {
lastError = error;
await new Promise((resolve) => setTimeout(resolve, 25));
}
}
if (lastError) {
throw lastError;
}
}
test.after(async () => {
if (ORIGINAL_DATA_DIR === undefined) {
delete process.env.DATA_DIR;
} else {
process.env.DATA_DIR = ORIGINAL_DATA_DIR;
}
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
await removeTestDataDir();
});
test("callLogs startup cleanup swallows unexpected rotateCallLogs bootstrap failures", async () => {
+18 -3
View File
@@ -186,6 +186,7 @@ test("chatCore sanitization preserves max_output_tokens for openai-responses tar
// the translator (which converts max_tokens back) is skipped for same-format.
const { call } = await invokeChatCore({
endpoint: "/v1/responses",
provider: "codex",
body: {
model: "gpt-5.4",
max_output_tokens: 4096,
@@ -208,11 +209,16 @@ test("chatCore sanitization preserves max_output_tokens for openai-responses tar
});
// max_output_tokens should survive sanitization for Responses targets
assert.equal("max_tokens" in call.body, false, "max_tokens must not be injected for Responses targets");
assert.equal(
"max_tokens" in call.body,
false,
"max_tokens must not be injected for Responses targets"
);
// Reverse normalization: max_tokens → max_output_tokens for Responses targets
const fromMaxTokens = await invokeChatCore({
endpoint: "/v1/responses",
provider: "codex",
body: {
model: "gpt-5.4",
max_tokens: 2048,
@@ -234,11 +240,16 @@ test("chatCore sanitization preserves max_output_tokens for openai-responses tar
),
});
assert.equal("max_tokens" in fromMaxTokens.call.body, false, "max_tokens should be converted to max_output_tokens");
assert.equal(
"max_tokens" in fromMaxTokens.call.body,
false,
"max_tokens should be converted to max_output_tokens"
);
// Reverse normalization: max_completion_tokens → max_output_tokens for Responses targets
const fromMaxCompletion = await invokeChatCore({
endpoint: "/v1/responses",
provider: "codex",
body: {
model: "gpt-5.4",
max_completion_tokens: 8192,
@@ -260,7 +271,11 @@ test("chatCore sanitization preserves max_output_tokens for openai-responses tar
),
});
assert.equal("max_completion_tokens" in fromMaxCompletion.call.body, false, "max_completion_tokens should be converted to max_output_tokens");
assert.equal(
"max_completion_tokens" in fromMaxCompletion.call.body,
false,
"max_completion_tokens should be converted to max_output_tokens"
);
});
test("chatCore sanitization strips empty message names and filters empty tool names", async () => {
+3 -3
View File
@@ -1,5 +1,5 @@
/**
* Tests for CLI Memory Sanitization (bin/omniroute.ts)
* Tests for CLI Memory Sanitization (bin/omniroute.mjs)
*
* Tests cover:
* - Memory limit parsing and validation
@@ -15,7 +15,7 @@ import assert from "node:assert/strict";
describe("CLI Memory Limit Sanitization", () => {
/**
* Replicate the memory sanitization logic from bin/omniroute.ts
* Replicate the memory sanitization logic from bin/omniroute.mjs
*/
function sanitizeMemoryLimit(envValue) {
const rawMemory = parseInt(envValue || "512", 10);
@@ -101,7 +101,7 @@ describe("CLI Memory Limit Sanitization", () => {
describe("CLI .env File Loading", () => {
/**
* Simulate the .env parsing logic from bin/omniroute.ts
* Simulate the .env parsing logic from bin/omniroute.mjs
*/
function parseEnvLine(line) {
const trimmed = line.trim();
+18 -6
View File
@@ -85,8 +85,12 @@ test("CLI config helpers enforce safe config homes and expose per-tool config pa
assert.equal(cliRuntime.getCliConfigPaths("unknown"), null);
process.env.XDG_CONFIG_HOME = path.join(homeDir, ".config-test");
const expectedOpencodeRoot =
process.platform === "win32"
? process.env.APPDATA || path.join(homeDir, "AppData", "Roaming")
: process.env.XDG_CONFIG_HOME;
assert.deepEqual(cliRuntime.getCliConfigPaths("opencode"), {
config: path.join(process.env.XDG_CONFIG_HOME, "opencode", "opencode.json"),
config: path.join(expectedOpencodeRoot, "opencode", "opencode.json"),
});
});
@@ -121,8 +125,13 @@ test("getCliRuntimeStatus reports not_executable for absolute env override files
const status = await cliRuntime.getCliRuntimeStatus("codex");
assert.equal(status.installed, true);
assert.equal(status.runnable, false);
assert.equal(status.reason, "not_executable");
if (process.platform === "win32") {
assert.equal(status.runnable, true);
assert.equal(status.reason, null);
} else {
assert.equal(status.runnable, false);
assert.equal(status.reason, "not_executable");
}
assert.equal(status.commandPath, scriptPath);
});
@@ -150,7 +159,7 @@ test("getCliRuntimeStatus reports healthcheck_failed when a binary exists but do
test("getCliRuntimeStatus discovers binaries from CLI_EXTRA_PATHS during PATH lookup", async () => {
const tempDir = createTempDir("omniroute-cli-extra-path-");
const scriptName = process.platform === "win32" ? "qodercli.exe" : "qodercli";
const scriptName = process.platform === "win32" ? "qodercli.cmd" : "qodercli";
writeScript(
tempDir,
scriptName,
@@ -168,12 +177,15 @@ test("getCliRuntimeStatus discovers binaries from CLI_EXTRA_PATHS during PATH lo
assert.equal(status.installed, true);
assert.equal(status.runnable, true);
assert.equal(status.reason, null);
assert.ok(status.commandPath === "qodercli" || status.commandPath === "qodercli.exe");
assert.equal(
path.basename(String(status.commandPath)).toLowerCase(),
process.platform === "win32" ? "qodercli.cmd" : "qodercli"
);
});
test("getCliRuntimeStatus resolves known binaries from npm global prefix discovered via npm config", async () => {
const prefixDir = createTempDir("omniroute-cli-prefix-");
const scriptName = process.platform === "win32" ? "qodercli.exe" : "qodercli";
const scriptName = process.platform === "win32" ? "qodercli.cmd" : "qodercli";
const scriptPath = writeScript(
path.join(prefixDir, process.platform === "win32" ? "" : "bin"),
scriptName,
+20 -7
View File
@@ -108,7 +108,12 @@ test.afterEach(async () => {
test("getCloudflaredRuntimeDirs and status resolve a managed binary from the data dir", async () => {
const dataDir = await createCloudflaredDataDir("omniroute-cloudflared-managed-");
const binaryPath = path.join(dataDir, "cloudflared", "bin", "cloudflared");
const binaryPath = path.join(
dataDir,
"cloudflared",
"bin",
process.platform === "win32" ? "cloudflared.exe" : "cloudflared"
);
process.env.DATA_DIR = dataDir;
await fs.mkdir(path.dirname(binaryPath), { recursive: true });
@@ -135,17 +140,20 @@ test("getCloudflaredTunnelStatus resolves a PATH-installed binary when no manage
const dataDir = await createCloudflaredDataDir("omniroute-cloudflared-path-");
process.env.DATA_DIR = dataDir;
delete process.env.CLOUDFLARED_BIN;
const lookupCommand = process.platform === "win32" ? "where" : "which";
const pathBinary =
process.platform === "win32" ? "C:\\Tools\\cloudflared.exe" : "/usr/local/bin/cloudflared";
childProcess.execFile = (command, args, options, callback) => {
const cb = typeof options === "function" ? options : callback;
assert.equal(command, "which");
assert.equal(command, lookupCommand);
assert.deepEqual(args, ["cloudflared"]);
cb(null, "/usr/local/bin/cloudflared\n", "");
cb(null, `${pathBinary}\n`, "");
};
childProcess.execFile[promisify.custom] = async (command, args) => {
assert.equal(command, "which");
assert.equal(command, lookupCommand);
assert.deepEqual(args, ["cloudflared"]);
return { stdout: "/usr/local/bin/cloudflared\n", stderr: "" };
return { stdout: `${pathBinary}\n`, stderr: "" };
};
syncBuiltinESMExports();
@@ -155,7 +163,7 @@ test("getCloudflaredTunnelStatus resolves a PATH-installed binary when no manage
assert.equal(status.installed, true);
assert.equal(status.managedInstall, false);
assert.equal(status.installSource, "path");
assert.equal(status.binaryPath, "/usr/local/bin/cloudflared");
assert.equal(status.binaryPath, pathBinary);
assert.equal(status.phase, "stopped");
});
@@ -207,7 +215,12 @@ test("getCloudflaredTunnelStatus reports a starting tunnel while the spawned pid
test("startCloudflaredTunnel reaches running state and stopCloudflaredTunnel clears persisted runtime state", async () => {
const dataDir = await createCloudflaredDataDir("omniroute-cloudflared-run-");
const binaryPath = path.join(dataDir, "cloudflared", "bin", "cloudflared");
const binaryPath = path.join(
dataDir,
"cloudflared",
"bin",
process.platform === "win32" ? "cloudflared.exe" : "cloudflared"
);
process.env.DATA_DIR = dataDir;
process.env.API_PORT = "24128";
+28 -4
View File
@@ -5,6 +5,7 @@ import os from "node:os";
import path from "node:path";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-combo-routing-"));
const ORIGINAL_DATA_DIR = process.env.DATA_DIR;
process.env.DATA_DIR = TEST_DATA_DIR;
const {
@@ -88,9 +89,26 @@ function getComboTargetBreakerKey(comboName, index, stepInput) {
return `combo:${comboName}:${step.id}`;
}
async function cleanupTestDataDir() {
let lastError;
for (let attempt = 0; attempt < 5; attempt += 1) {
try {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
return;
} catch (error) {
lastError = error;
await new Promise((resolve) => setTimeout(resolve, 25));
}
}
if (lastError) {
throw lastError;
}
}
async function resetStorage() {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
await cleanupTestDataDir();
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
await settingsDb.resetAllPricing();
settingsDb.clearAllLKGP();
@@ -110,8 +128,14 @@ test.after(async () => {
resetAllCircuitBreakers();
resetAllSemaphores();
_resetAllDecks();
await resetStorage();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
clearModelsDevCapabilities();
settingsDb.clearAllLKGP();
if (ORIGINAL_DATA_DIR === undefined) {
delete process.env.DATA_DIR;
} else {
process.env.DATA_DIR = ORIGINAL_DATA_DIR;
}
await cleanupTestDataDir();
});
test("getComboFromData and getComboModelsFromData resolve combos from array and object containers", () => {
+2
View File
@@ -445,6 +445,7 @@ test(
DATA_DIR: undefined,
XDG_CONFIG_HOME: undefined,
HOME: fakeHome,
USERPROFILE: fakeHome,
APPDATA: undefined,
},
async () => {
@@ -803,6 +804,7 @@ test(
/Manual recovery required before startup/i
);
assert.equal(fs.existsSync(sqliteFile), false);
core.resetDbInstance();
});
} finally {
removePath(dataDir);
+130
View File
@@ -61,6 +61,53 @@ function createDb() {
return new Database(":memory:");
}
function createInitialSchemaTables(db) {
db.exec(`
CREATE TABLE provider_connections (id TEXT PRIMARY KEY);
CREATE TABLE combos (id TEXT PRIMARY KEY);
CREATE TABLE call_logs (id TEXT PRIMARY KEY);
`);
}
function buildMockMigrationFiles(startVersion, endVersion, prefix) {
const files = {};
for (let version = startVersion; version <= endVersion; version++) {
const padded = String(version).padStart(3, "0");
const fileName = version === 1 ? "001_initial_schema.sql" : `${padded}_${prefix}_${padded}.sql`;
files[fileName] = `CREATE TABLE ${prefix}_${padded} (id INTEGER);`;
}
return files;
}
function withNonTestEnvironment(fn) {
const originalNodeEnv = process.env.NODE_ENV;
const originalVitest = process.env.VITEST;
const originalDisableAutoBackup = process.env.DISABLE_SQLITE_AUTO_BACKUP;
const originalArgv = [...process.argv];
delete process.env.NODE_ENV;
delete process.env.VITEST;
delete process.env.DISABLE_SQLITE_AUTO_BACKUP;
process.argv = process.argv.filter((arg) => !arg.includes("test"));
try {
return fn();
} finally {
process.argv = originalArgv;
if (originalNodeEnv === undefined) delete process.env.NODE_ENV;
else process.env.NODE_ENV = originalNodeEnv;
if (originalVitest === undefined) delete process.env.VITEST;
else process.env.VITEST = originalVitest;
if (originalDisableAutoBackup === undefined) delete process.env.DISABLE_SQLITE_AUTO_BACKUP;
else process.env.DISABLE_SQLITE_AUTO_BACKUP = originalDisableAutoBackup;
}
}
const REAL_022_ADD_MEMORY_FTS5_SQL = fs.readFileSync(
path.resolve("src/lib/db/migrations/022_add_memory_fts5.sql"),
"utf8"
@@ -547,3 +594,86 @@ test(
}
}
);
test(
"runMigrations allows a large pending set when the physical schema still looks like 001",
serial,
async () => {
const runner = await importFresh("src/lib/db/migrationRunner.ts");
const db = createDb();
try {
createInitialSchemaTables(db);
db.exec(`
CREATE TABLE _omniroute_migrations (
version TEXT PRIMARY KEY,
name TEXT NOT NULL,
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
);
`);
db.prepare("INSERT INTO _omniroute_migrations (version, name) VALUES (?, ?)").run(
"001",
"initial_schema"
);
const count = withNonTestEnvironment(() =>
withMockedMigrationFs(buildMockMigrationFiles(1, 7, "legacy_allow"), () =>
runner.runMigrations(db)
)
);
assert.equal(count, 6);
assert.deepEqual(
db.prepare("SELECT version FROM _omniroute_migrations ORDER BY version").all(),
[
{ version: "001" },
{ version: "002" },
{ version: "003" },
{ version: "004" },
{ version: "005" },
{ version: "006" },
{ version: "007" },
]
);
} finally {
db.close();
}
}
);
test(
"runMigrations aborts large pending sets when the physical schema proves a newer baseline",
serial,
async () => {
const runner = await importFresh("src/lib/db/migrationRunner.ts");
const db = createDb();
try {
createInitialSchemaTables(db);
db.exec(`
CREATE TABLE request_detail_logs (id TEXT PRIMARY KEY);
CREATE TABLE _omniroute_migrations (
version TEXT PRIMARY KEY,
name TEXT NOT NULL,
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
);
`);
db.prepare("INSERT INTO _omniroute_migrations (version, name) VALUES (?, ?)").run(
"001",
"initial_schema"
);
assert.throws(
() =>
withNonTestEnvironment(() =>
withMockedMigrationFs(buildMockMigrationFiles(1, 12, "legacy_abort"), () =>
runner.runMigrations(db)
)
),
/Physical schema already shows 006/i
);
} finally {
db.close();
}
}
);
+2 -1
View File
@@ -47,7 +47,7 @@ test("findMissingArtifactPaths flags missing root runtime files in the tarball",
const missingPaths = findMissingArtifactPaths(
[
"app/server.js",
"bin/omniroute.ts",
"bin/omniroute.mjs",
"package.json",
"scripts/postinstall.mjs",
"scripts/postinstallSupport.mjs",
@@ -57,6 +57,7 @@ test("findMissingArtifactPaths flags missing root runtime files in the tarball",
assert.deepEqual(missingPaths, [
"bin/mcp-server.mjs",
"bin/nodeRuntimeSupport.mjs",
"scripts/native-binary-compat.mjs",
"src/shared/utils/nodeRuntimeSupport.ts",
]);
+6 -3
View File
@@ -66,11 +66,11 @@ test.afterEach(() => {
tlsClient.fetch = originalTlsFetch;
});
test("proxy fetch fails closed when an invalid environment proxy is configured", async () => {
test("proxy fetch bypasses invalid environment proxies for local addresses", async () => {
await withHttpServer(
(_req, res) => {
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("should-not-arrive");
res.end("local-bypass-ok");
},
async (url) => {
await withEnv(
@@ -81,7 +81,10 @@ test("proxy fetch fails closed when an invalid environment proxy is configured",
NO_PROXY: undefined,
},
async () => {
await assert.rejects(() => proxyFetch(url));
const response = await proxyFetch(url);
assert.equal(response.status, 200);
assert.equal(await response.text(), "local-bypass-ok");
}
);
}
+7 -1
View File
@@ -6,6 +6,7 @@ import path from "node:path";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-stream-utils-"));
process.env.DATA_DIR = TEST_DATA_DIR;
const core = await import("../../src/lib/db/core.ts");
const { createSSEStream, createSSETransformStreamWithLogger, createPassthroughStreamWithLogger } =
await import("../../open-sse/utils/stream.ts");
@@ -47,7 +48,12 @@ async function readWithTransform(chunks, transformStream) {
}
test.after(() => {
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
core.resetDbInstance();
if (fs.existsSync(TEST_DATA_DIR)) {
for (const entry of fs.readdirSync(TEST_DATA_DIR)) {
fs.rmSync(path.join(TEST_DATA_DIR, entry), { recursive: true, force: true });
}
}
});
test("createSSEStream passthrough normalizes tool-call finishes and reports the assembled response", async () => {
+7 -1
View File
@@ -8,11 +8,17 @@ import path from "node:path";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-strict-random-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "strict-random-test-secret";
const core = await import("../../src/lib/db/core.ts");
const { fisherYatesShuffle, getNextFromDeck } = await import("../../src/sse/services/auth.ts");
test.after(() => {
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
core.resetDbInstance();
if (fs.existsSync(TEST_DATA_DIR)) {
for (const entry of fs.readdirSync(TEST_DATA_DIR)) {
fs.rmSync(path.join(TEST_DATA_DIR, entry), { recursive: true, force: true });
}
}
});
// ─── fisherYatesShuffle ──────────────────────────────────────────────────────
+8
View File
@@ -5,6 +5,7 @@ import os from "node:os";
import path from "node:path";
const ORIGINAL_HOME = process.env.HOME;
const ORIGINAL_USERPROFILE = process.env.USERPROFILE;
const ORIGINAL_DATA_DIR = process.env.DATA_DIR;
const ORIGINAL_NEXT_PHASE = process.env.NEXT_PHASE;
@@ -12,6 +13,7 @@ const TEST_HOME_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-usage-mig
const TEST_DATA_DIR = path.join(TEST_HOME_DIR, "data");
process.env.HOME = TEST_HOME_DIR;
process.env.USERPROFILE = TEST_HOME_DIR;
process.env.DATA_DIR = TEST_DATA_DIR;
delete process.env.NEXT_PHASE;
@@ -83,6 +85,12 @@ test.after(() => {
process.env.HOME = ORIGINAL_HOME;
}
if (ORIGINAL_USERPROFILE === undefined) {
delete process.env.USERPROFILE;
} else {
process.env.USERPROFILE = ORIGINAL_USERPROFILE;
}
if (ORIGINAL_DATA_DIR === undefined) {
delete process.env.DATA_DIR;
} else {
+2 -2
View File
@@ -9,11 +9,11 @@ import {
} from "../../src/shared/validation/schemas.ts";
import { validateBody } from "../../src/shared/validation/helpers.ts";
test("xiaomi-mimo registry uses current token-plan base URL and current MiMo V2 models", () => {
test("xiaomi-mimo registry uses the current default base URL and MiMo V2 models", () => {
const entry = REGISTRY["xiaomi-mimo"];
assert.ok(entry, "xiaomi-mimo should exist in registry");
assert.equal(entry.baseUrl, "https://token-plan-sgp.xiaomimimo.com/v1");
assert.equal(entry.baseUrl, "https://api.xiaomimimo.com/v1");
assert.deepEqual(
entry.models.map((model) => model.id),
["mimo-v2-pro", "mimo-v2-omni", "mimo-v2-tts"]