mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-25 16:42:16 +03:00
Merge remote-tracking branch 'origin/release/v3.8.50' into fix/release-v3.8.50-basereds-cluster
This commit is contained in:
@@ -370,10 +370,31 @@ test("GET /api/settings/qdrant/embedding-models — returns models array", async
|
||||
assert.strictEqual(res.status, 200);
|
||||
const body = await res.json();
|
||||
assert.ok(Array.isArray(body.models), "should have models array");
|
||||
// Should have at least the default fallback model
|
||||
assert.ok(body.models.length > 0, "should have at least one model");
|
||||
assert.strictEqual(body.models.length, 0, "should not list models without a configured provider");
|
||||
});
|
||||
|
||||
test("GET /api/settings/qdrant/embedding-models — lists only configured providers", async () => {
|
||||
await localDb.createProviderConnection({
|
||||
provider: "openai",
|
||||
authType: "apikey",
|
||||
name: "embedding-test-openai",
|
||||
apiKey: "sk-test-embedding",
|
||||
});
|
||||
|
||||
const headers = await createManagementSessionHeaders();
|
||||
const req = new Request("http://localhost/api/settings/qdrant/embedding-models", {
|
||||
method: "GET",
|
||||
headers: Object.fromEntries(headers.entries()),
|
||||
});
|
||||
|
||||
const res = await qdrantEmbeddingModelsRoute.GET(req as any);
|
||||
assert.strictEqual(res.status, 200);
|
||||
const body = await res.json();
|
||||
assert.ok(body.models.length > 0, "should list models for configured provider");
|
||||
assert.ok(body.models.every((model: any) => model.value.startsWith("openai/")));
|
||||
assert.ok(body.models.some((model: any) => model.value === "openai/text-embedding-3-small"));
|
||||
const defaultModel = body.models.find((m: any) => m.value === "openai/text-embedding-3-small");
|
||||
assert.ok(defaultModel, "should include openai/text-embedding-3-small as default");
|
||||
assert.match(defaultModel.label, /1536d/);
|
||||
});
|
||||
|
||||
test("GET /api/settings/qdrant/embedding-models — 401 without auth", async () => {
|
||||
|
||||
238
tests/unit/chat-admission-visibility-11244.test.ts
Normal file
238
tests/unit/chat-admission-visibility-11244.test.ts
Normal file
@@ -0,0 +1,238 @@
|
||||
// #11244: visibility for the STRUCTURAL chat admission gate
|
||||
// (src/shared/middleware/chatBodyAdmission.ts — the bounded heavyweight lease +
|
||||
// healthy-headroom path from #10110/#10437, NOT the adaptive shadow-mode layer in
|
||||
// open-sse/services/admission/). The 503 chat_admission_busy shed returns BEFORE
|
||||
// request logging, so today a shed is invisible: no counter, no log line, and the
|
||||
// process-wide snapshot (PerConnectionAdmissionController.snapshot()) reports only
|
||||
// live state (activeHeavy/queuedBytes/waiting/lanes) with no shed history.
|
||||
//
|
||||
// These tests pin the observability contract WITHOUT changing admission behavior:
|
||||
// (a) every structural shed (503 chat_admission_busy) increments an in-memory
|
||||
// counter — total + per reason ("queue_timeout" when the bounded wait expires,
|
||||
// "queued_bytes_budget" when the queued-bytes heap valve refuses to park) —
|
||||
// while a client abort mid-wait is NOT a shed (capacity was never denied);
|
||||
// (b) the process-wide snapshot exposes shedTotal + shedsByReason next to the
|
||||
// existing live fields;
|
||||
// (c) each shed emits exactly one structured pino warn carrying
|
||||
// reason/activeHeavy/waiting and the HMAC session fingerprint — never the raw
|
||||
// API key (resolveSessionId already fingerprints the credential).
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtempSync, readFileSync, existsSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
// Configure the shared pino logger BEFORE importing the admission module — the
|
||||
// logger builds its transports at import time (see logger-redaction-wiring.test.ts
|
||||
// for the same pattern). JSON to a temp file keeps test (c)'s capture deterministic.
|
||||
const logDir = mkdtempSync(join(tmpdir(), "omniroute-admission-11244-"));
|
||||
const logFile = join(logDir, "app.log");
|
||||
process.env.NODE_ENV = "production";
|
||||
process.env.APP_LOG_TO_FILE = "true";
|
||||
process.env.APP_LOG_FILE_PATH = logFile;
|
||||
|
||||
const {
|
||||
ChatAdmissionController,
|
||||
PerConnectionAdmissionController,
|
||||
perConnectionAdmissionController,
|
||||
admitChatStructure,
|
||||
resolveSessionId,
|
||||
} = await import("../../src/shared/middleware/chatBodyAdmission.ts");
|
||||
|
||||
function heavyBody() {
|
||||
return {
|
||||
messages: Array.from({ length: 200 }, () => ({ role: "user", content: "x".repeat(40) })),
|
||||
tools: [] as unknown[],
|
||||
};
|
||||
}
|
||||
|
||||
const heapHealthy = () => false; // "not under pressure" — the healthy-heap fast path
|
||||
const heapPressured = () => true; // forces the bounded-wait/shed path deterministically
|
||||
const silentSink = () => {}; // keep non-logging tests off the pino transport
|
||||
|
||||
test("#11244 (a): a structural shed after the bounded wait increments shedTotal and shedsByReason", async () => {
|
||||
// Primary lease (1) + bounded healthy-headroom (1): two concurrent heavy requests
|
||||
// admit on a healthy heap; the third must wait queueMs and then shed with a 503.
|
||||
const controller = new ChatAdmissionController(1, undefined, 1, silentSink);
|
||||
|
||||
const first = await admitChatStructure(heavyBody(), null, {
|
||||
controller,
|
||||
heapPressureCheck: heapHealthy,
|
||||
queueMs: 0,
|
||||
});
|
||||
const second = await admitChatStructure(heavyBody(), null, {
|
||||
controller,
|
||||
heapPressureCheck: heapHealthy,
|
||||
queueMs: 0,
|
||||
});
|
||||
assert.equal(first.admit, true, "first heavy request takes the primary lease");
|
||||
assert.equal(second.admit, true, "second heavy request takes the bounded headroom lease");
|
||||
assert.equal(controller.shedTotal, 0, "admitted requests never count as sheds");
|
||||
assert.deepEqual(controller.shedsByReason, {});
|
||||
|
||||
const shed = await admitChatStructure(heavyBody(), null, {
|
||||
controller,
|
||||
heapPressureCheck: heapHealthy,
|
||||
queueMs: 50,
|
||||
});
|
||||
assert.equal(shed.admit, false, "third heavy request must shed once both budgets are busy");
|
||||
if (!shed.admit) {
|
||||
assert.equal(shed.response.status, 503);
|
||||
const payload = await shed.response.json();
|
||||
assert.equal(payload.error.code, "chat_admission_busy");
|
||||
}
|
||||
|
||||
assert.equal(
|
||||
controller.shedTotal,
|
||||
1,
|
||||
"the shed must be counted even though it skips request logging"
|
||||
);
|
||||
assert.deepEqual(
|
||||
controller.shedsByReason,
|
||||
{ queue_timeout: 1 },
|
||||
"a bounded wait that expires with no freed capacity is a queue_timeout shed"
|
||||
);
|
||||
|
||||
// Counters are history, not live state: releasing the leases must not rewind them.
|
||||
if (first.admit) first.lease?.release();
|
||||
if (second.admit) second.lease?.release();
|
||||
assert.equal(controller.shedTotal, 1, "shed history survives lease release");
|
||||
|
||||
// And a subsequently admitted request must not be counted.
|
||||
const fourth = await admitChatStructure(heavyBody(), null, {
|
||||
controller,
|
||||
heapPressureCheck: heapHealthy,
|
||||
queueMs: 0,
|
||||
});
|
||||
assert.equal(fourth.admit, true);
|
||||
assert.equal(controller.shedTotal, 1);
|
||||
if (fourth.admit) fourth.lease?.release();
|
||||
});
|
||||
|
||||
test("#11244 (a2): the queued-bytes heap valve rejection is counted with its own reason", async () => {
|
||||
// maxQueuedBytes smaller than the conservative 256KB structural wait weight: the
|
||||
// valve refuses to park and the shed must be distinguishable from a queue timeout.
|
||||
const controller = new ChatAdmissionController(1, 1024, 0, silentSink);
|
||||
|
||||
const first = await admitChatStructure(heavyBody(), null, {
|
||||
controller,
|
||||
heapPressureCheck: heapPressured,
|
||||
queueMs: 0,
|
||||
});
|
||||
assert.equal(first.admit, true);
|
||||
|
||||
const shed = await admitChatStructure(heavyBody(), null, {
|
||||
controller,
|
||||
heapPressureCheck: heapPressured,
|
||||
queueMs: 1000,
|
||||
});
|
||||
assert.equal(shed.admit, false);
|
||||
if (!shed.admit) assert.equal(shed.response.status, 503);
|
||||
assert.equal(controller.shedTotal, 1);
|
||||
assert.deepEqual(controller.shedsByReason, { queued_bytes_budget: 1 });
|
||||
|
||||
if (first.admit) first.lease?.release();
|
||||
});
|
||||
|
||||
test("#11244 (a3): a client abort while parked is not a shed — capacity was never denied", async () => {
|
||||
const controller = new ChatAdmissionController(1, undefined, 0, silentSink);
|
||||
|
||||
const first = await admitChatStructure(heavyBody(), null, {
|
||||
controller,
|
||||
heapPressureCheck: heapPressured,
|
||||
queueMs: 0,
|
||||
});
|
||||
assert.equal(first.admit, true);
|
||||
|
||||
const abort = new AbortController();
|
||||
const pending = admitChatStructure(heavyBody(), null, {
|
||||
controller,
|
||||
heapPressureCheck: heapPressured,
|
||||
queueMs: 5000,
|
||||
signal: abort.signal,
|
||||
});
|
||||
setTimeout(() => abort.abort(), 20);
|
||||
const result = await pending;
|
||||
assert.equal(
|
||||
result.admit,
|
||||
false,
|
||||
"the caller still answers a (dropped) 503 on the dead connection"
|
||||
);
|
||||
assert.equal(controller.shedTotal, 0, "an aborted wait frees capacity instead of shedding");
|
||||
assert.deepEqual(controller.shedsByReason, {});
|
||||
|
||||
if (first.admit) first.lease?.release();
|
||||
});
|
||||
|
||||
test("#11244 (b): the process-wide snapshot exposes shed counters next to the live fields", async () => {
|
||||
const empty = perConnectionAdmissionController.snapshot();
|
||||
assert.equal(typeof empty.activeHeavy, "number");
|
||||
assert.equal(typeof empty.queuedBytes, "number");
|
||||
assert.equal(typeof empty.waiting, "number");
|
||||
assert.ok(Array.isArray(empty.lanes));
|
||||
assert.equal(
|
||||
empty.shedTotal,
|
||||
0,
|
||||
"no shed happened through the production singleton in this process"
|
||||
);
|
||||
assert.deepEqual(empty.shedsByReason, {});
|
||||
|
||||
// A shed recorded through a session's controller surfaces in the aggregate snapshot.
|
||||
const pc = new PerConnectionAdmissionController(1, { onShed: silentSink });
|
||||
const controller = pc.getController("key_visibility11244");
|
||||
controller.recordShed("queue_timeout", "key_visibility11244");
|
||||
controller.recordShed("queue_timeout", "key_visibility11244");
|
||||
controller.recordShed("queued_bytes_budget", "key_visibility11244");
|
||||
|
||||
const snap = pc.snapshot();
|
||||
assert.equal(snap.shedTotal, 3);
|
||||
assert.deepEqual(snap.shedsByReason, { queue_timeout: 2, queued_bytes_budget: 1 });
|
||||
});
|
||||
|
||||
test("#11244 (c): each shed logs one structured warn with the session fingerprint, never the raw key", async () => {
|
||||
const rawKey = "visRAWSECRETtoken11244xyz"; // matches no logRedaction pattern — a leak would show verbatim
|
||||
const fingerprint = resolveSessionId(
|
||||
new Request("http://localhost/v1/chat/completions", {
|
||||
headers: { authorization: `Bearer ${rawKey}` },
|
||||
})
|
||||
);
|
||||
assert.ok(fingerprint.startsWith("key_"), "resolveSessionId returns the HMAC fingerprint");
|
||||
assert.ok(!fingerprint.includes(rawKey));
|
||||
|
||||
// Default sink (no injected onShed): the shed must go through the shared pino logger.
|
||||
const controller = new ChatAdmissionController(1);
|
||||
const primary = controller.tryAcquireHeavy();
|
||||
assert.ok(primary);
|
||||
|
||||
const shed = await admitChatStructure(heavyBody(), null, {
|
||||
controller,
|
||||
heapPressureCheck: heapPressured,
|
||||
queueMs: 25,
|
||||
sessionId: fingerprint,
|
||||
});
|
||||
assert.equal(shed.admit, false);
|
||||
primary.release();
|
||||
|
||||
// Poll the worker-thread-written log file until the shed line lands.
|
||||
const deadline = Date.now() + 4000;
|
||||
let contents = "";
|
||||
while (Date.now() < deadline) {
|
||||
if (existsSync(logFile)) {
|
||||
contents = readFileSync(logFile, "utf8");
|
||||
if (contents.includes("chat_admission_busy")) break;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
}
|
||||
|
||||
assert.ok(contents.includes("chat_admission_busy"), "the shed log line names the rejection code");
|
||||
assert.match(
|
||||
contents,
|
||||
/"level":(40|"warn")/,
|
||||
"sheds log at warn level (numeric 40 when the file transport strips the level formatter)"
|
||||
);
|
||||
assert.ok(contents.includes('"module":"chat-admission"'), "the log is scoped to the gate");
|
||||
assert.ok(contents.includes('"reason":"queue_timeout"'), "the shed reason is structured");
|
||||
assert.ok(contents.includes('"activeHeavy":1'), "live state travels with the log line");
|
||||
assert.ok(contents.includes(fingerprint), "the lane fingerprint allows per-key correlation");
|
||||
assert.ok(!contents.includes(rawKey), "the raw API key must never reach the shed log");
|
||||
});
|
||||
@@ -71,10 +71,13 @@ test("installed package contract requires sql.js metadata, entrypoint, and WASM"
|
||||
[]
|
||||
);
|
||||
|
||||
present.delete(path.join("/pkg", "dist/node_modules/sql.js/dist/sql-wasm.wasm"));
|
||||
// Dependency-based packaging (#11242): sql.js is a declared dependency, so the
|
||||
// contract path is the npm-installed <packageRoot>/node_modules/sql.js location,
|
||||
// never the old vendored dist/node_modules one (banned from the tarball).
|
||||
present.delete(path.join("/pkg", "node_modules/sql.js/dist/sql-wasm.wasm"));
|
||||
assert.deepEqual(
|
||||
findMissingSqlJsRuntimeFiles("/pkg", (file) => present.has(file)),
|
||||
["dist/node_modules/sql.js/dist/sql-wasm.wasm"]
|
||||
["node_modules/sql.js/dist/sql-wasm.wasm"]
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
131
tests/unit/cli-auth-export-wiring.test.ts
Normal file
131
tests/unit/cli-auth-export-wiring.test.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
// #11226 — `omniroute auth export` crashed with "cmd.optsWithGlobals is not a
|
||||
// function" because the command was registered as `.command("auth export")`:
|
||||
// commander parses the bare word `export` as a REQUIRED POSITIONAL ARGUMENT, so
|
||||
// the action received ("export", options, command) while its signature expected
|
||||
// (options, command) — the classic opts/cmd swap. The fix registers `export` as
|
||||
// a proper nested subcommand of `auth`, restoring the documented CLI surface
|
||||
// (docs/reference/CLI-TOOLS.md): `omniroute auth export [--force] [--id] [--format] [--out]`.
|
||||
//
|
||||
// These tests exercise the REAL commander wiring via createProgram() — no DB is
|
||||
// touched on any of these paths (the no-force gate prints and returns before any
|
||||
// DB access; an invalid --format fails validation before opening the DB).
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { createProgram } from "../../bin/cli/program.mjs";
|
||||
|
||||
function captureConsole(): { captured: { logs: string[]; errors: string[] }; restore: () => void } {
|
||||
const originalLog = console.log;
|
||||
const originalError = console.error;
|
||||
const captured = { logs: [] as string[], errors: [] as string[] };
|
||||
console.log = (msg?: unknown) => {
|
||||
captured.logs.push(String(msg ?? ""));
|
||||
};
|
||||
console.error = (msg?: unknown) => {
|
||||
captured.errors.push(String(msg ?? ""));
|
||||
};
|
||||
return {
|
||||
captured,
|
||||
restore: () => {
|
||||
console.log = originalLog;
|
||||
console.error = originalError;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function stubProcessExit(): { exitCodes: number[]; restore: () => void } {
|
||||
const originalExit = process.exit;
|
||||
const exitCodes: number[] = [];
|
||||
process.exit = ((code?: number) => {
|
||||
exitCodes.push(code ?? 0);
|
||||
}) as typeof process.exit;
|
||||
return {
|
||||
exitCodes,
|
||||
restore: () => {
|
||||
process.exit = originalExit;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test("auth command exposes 'export' as a subcommand, not a positional argument", () => {
|
||||
const program = createProgram();
|
||||
const auth = program.commands.find((c) => c.name() === "auth");
|
||||
assert.ok(auth, "auth command exists");
|
||||
|
||||
const exportCmd = auth.commands.find((c) => c.name() === "export");
|
||||
assert.ok(exportCmd, "export must be a nested subcommand of auth");
|
||||
|
||||
const registeredArgs = (auth as unknown as { registeredArguments?: unknown[] })
|
||||
.registeredArguments;
|
||||
assert.equal(
|
||||
registeredArgs?.length ?? 0,
|
||||
0,
|
||||
"auth must not declare positional arguments (a bare word in .command() becomes one)"
|
||||
);
|
||||
});
|
||||
|
||||
test("auth export action receives (options, command): flags reach the handler end-to-end", async () => {
|
||||
const program = createProgram();
|
||||
const exitStub = stubProcessExit();
|
||||
const { captured, restore } = captureConsole();
|
||||
try {
|
||||
// --format bogus makes runAuthExportCommand return 1 BEFORE any DB access;
|
||||
// the action must then call process.exit(1). With the opts/cmd swap this
|
||||
// parse rejects with "cmd.optsWithGlobals is not a function" instead.
|
||||
await program.parseAsync([
|
||||
"node",
|
||||
"omniroute",
|
||||
"auth",
|
||||
"export",
|
||||
"--force",
|
||||
"--format",
|
||||
"bogus",
|
||||
]);
|
||||
} finally {
|
||||
restore();
|
||||
exitStub.restore();
|
||||
}
|
||||
|
||||
assert.deepEqual(
|
||||
exitStub.exitCodes,
|
||||
[1],
|
||||
"handler must receive --format and exit 1 on bogus value"
|
||||
);
|
||||
assert.ok(
|
||||
captured.errors.join("\n").includes("Invalid format"),
|
||||
`expected the invalid-format error, got: ${captured.errors.join(" | ")}`
|
||||
);
|
||||
});
|
||||
|
||||
test("auth export without --force prints the confirmation gate (no crash, no DB)", async () => {
|
||||
const program = createProgram();
|
||||
const exitStub = stubProcessExit();
|
||||
const { captured, restore } = captureConsole();
|
||||
try {
|
||||
await program.parseAsync(["node", "omniroute", "auth", "export"]);
|
||||
} finally {
|
||||
restore();
|
||||
exitStub.restore();
|
||||
}
|
||||
|
||||
assert.deepEqual(exitStub.exitCodes, [], "dry run exits 0 without calling process.exit");
|
||||
assert.ok(
|
||||
captured.logs.join("\n").includes("DECRYPTED"),
|
||||
`expected the confirmation gate, got: ${captured.logs.join(" | ")}`
|
||||
);
|
||||
});
|
||||
|
||||
test("auth rejects an unknown positional (was silently accepted as the 'export' argument)", async () => {
|
||||
const program = createProgram();
|
||||
await assert.rejects(
|
||||
program.parseAsync(["node", "omniroute", "auth", "bogus-word"]),
|
||||
(err: unknown) => {
|
||||
assert.ok(err instanceof Error);
|
||||
assert.match(
|
||||
(err as { code?: string }).code || "",
|
||||
/commander\.(unknownCommand|helpDisplayed)/
|
||||
);
|
||||
return true;
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -27,7 +27,14 @@ async function withComboEnv(fn: (dataDir: string) => Promise<void>) {
|
||||
} finally {
|
||||
console.log = originalLog;
|
||||
globalThis.fetch = ORIGINAL_FETCH;
|
||||
fs.rmSync(dataDir, { recursive: true, force: true });
|
||||
// On Windows the SQLite file may still be held open by the db module when
|
||||
// the test ends, and rmSync then throws EPERM, failing a test whose
|
||||
// assertions all passed. Retry, then give up quietly.
|
||||
try {
|
||||
fs.rmSync(dataDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
|
||||
} catch {
|
||||
// best effort: the OS reclaims its own temp dir
|
||||
}
|
||||
|
||||
if (ORIGINAL_DATA_DIR === undefined) delete process.env.DATA_DIR;
|
||||
else process.env.DATA_DIR = ORIGINAL_DATA_DIR;
|
||||
|
||||
@@ -108,13 +108,46 @@ test("runOAuthStatus consumes the connections envelope", async () => {
|
||||
const parsed = JSON.parse(out);
|
||||
assert.deepEqual(
|
||||
parsed.map((connection: { id: string }) => connection.id),
|
||||
["conn1", "conn2"],
|
||||
["conn1", "conn2"]
|
||||
);
|
||||
} finally {
|
||||
globalThis.fetch = origFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("runOAuthStatus tolerates an out-of-contract 200 payload (#11236)", async () => {
|
||||
// Bug 5 residual: #10491 added the `data.connections ??` envelope, but a 200
|
||||
// whose body is an object without connections/providers/items still fell
|
||||
// through to `data` itself and crashed on `.filter is not a function`
|
||||
// (followed by a libuv teardown assertion on Windows). The guard must coerce
|
||||
// to an empty list and warn on stderr — never throw a raw TypeError.
|
||||
const origFetch = globalThis.fetch;
|
||||
// `as unknown as` (not `as any`): this file's no-explicit-any suppression is
|
||||
// frozen at its pre-existing count, so new casts must be any-free.
|
||||
globalThis.fetch = (() =>
|
||||
Promise.resolve(makeResp({ status: "ok" }))) as unknown as typeof globalThis.fetch;
|
||||
|
||||
const stderrChunks: string[] = [];
|
||||
const origStderr = process.stderr.write.bind(process.stderr);
|
||||
process.stderr.write = ((chunk: string | Uint8Array) => {
|
||||
if (typeof chunk === "string") stderrChunks.push(chunk);
|
||||
return true;
|
||||
}) as typeof process.stderr.write;
|
||||
|
||||
try {
|
||||
const { runOAuthStatus } = await import("../../bin/cli/commands/oauth.mjs");
|
||||
const out = await captureStdout(() => runOAuthStatus({}, makeCmd()));
|
||||
assert.deepEqual(JSON.parse(out), []);
|
||||
} finally {
|
||||
globalThis.fetch = origFetch;
|
||||
process.stderr.write = origStderr;
|
||||
}
|
||||
|
||||
const warning = stderrChunks.join("");
|
||||
assert.ok(warning.length > 0, "a sanitized warning must be written to stderr");
|
||||
assert.ok(!warning.includes("at /"), "warning must not leak a stack trace");
|
||||
});
|
||||
|
||||
test("runOAuthRevoke com --yes chama endpoint de revogação", async () => {
|
||||
let capturedUrl = "";
|
||||
let capturedMethod = "";
|
||||
|
||||
41
tests/unit/cli/_helpers/shellArgs.mjs
Normal file
41
tests/unit/cli/_helpers/shellArgs.mjs
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Reverse the Windows `shell: true` argument escaping so assertions can be
|
||||
* written against the logical argv on every platform.
|
||||
*
|
||||
* `bin/cli/commands/run.mjs` escapes argv before spawning, because on win32 the
|
||||
* launchers must go through cmd.exe to run npm `.cmd` shims (CVE-2024-27980)
|
||||
* and Node's `shell: true` joins argv with no escaping at all (DEP0190). That
|
||||
* escaping is correct and deliberate, but it means `plan.args` holds
|
||||
* `^^^"--model^^^"` on Windows where it holds `--model` elsewhere.
|
||||
*
|
||||
* Tests care about *which* arguments a plan carries, not about how they survive
|
||||
* cmd.exe, so they normalise first. Keep this in sync with
|
||||
* `escapeWindowsShellArg` in bin/cli/utils/winShellArgs.mjs.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {unknown} arg
|
||||
* @returns {string}
|
||||
*/
|
||||
export function unescapeWindowsShellArg(arg) {
|
||||
let s = String(arg);
|
||||
// 1. undo the two caret passes applied to cmd.exe metacharacters
|
||||
s = s.replace(/\^(.)/g, "$1").replace(/\^(.)/g, "$1");
|
||||
// 2. drop the wrapping quotes added by the CRT argv layer
|
||||
if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) s = s.slice(1, -1);
|
||||
// 3. undo the doubled backslashes and the escaped embedded quotes
|
||||
s = s.replace(/\\\\/g, "\\").replace(/\\"/g, '"');
|
||||
return s;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalise a plan's argv to its logical form. A no-op off Windows.
|
||||
*
|
||||
* @param {unknown[]} args
|
||||
* @param {NodeJS.Platform|string} [platform]
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export function logicalArgs(args, platform = process.platform) {
|
||||
const list = [...(args ?? [])].map(String);
|
||||
return platform === "win32" ? list.map(unescapeWindowsShellArg) : list;
|
||||
}
|
||||
@@ -18,7 +18,7 @@ import { spawnSync } from "node:child_process";
|
||||
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
|
||||
import {
|
||||
resolveAlias,
|
||||
@@ -30,6 +30,13 @@ import {
|
||||
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
||||
const REPO_ROOT = join(__dirname, "..", "..", "..");
|
||||
|
||||
// The child scripts below `import()` these paths, and `import()` resolves its
|
||||
// specifier as a URL. Replacing backslashes with forward slashes is not enough
|
||||
// on Windows: the leading drive letter is then parsed as the URL scheme `e:`,
|
||||
// which the ESM loader rejects with ERR_UNSUPPORTED_ESM_URL_SCHEME. Emit a
|
||||
// real file:// URL instead.
|
||||
const repoFileUrl = (relPath) => pathToFileURL(join(REPO_ROOT, relPath)).href;
|
||||
|
||||
describe("aliasResolver.resolveAlias (pure)", () => {
|
||||
it("returns null for non-@/ specifiers (lets Node/tsx handle them)", () => {
|
||||
assert.equal(resolveAlias("node:fs", REPO_ROOT), null);
|
||||
@@ -260,11 +267,11 @@ describe("aliasResolver end-to-end (#7791 regression)", () => {
|
||||
const script = `
|
||||
await import("tsx/esm");
|
||||
import { join } from "node:path";
|
||||
import { registerAliasResolver } from "${join(REPO_ROOT, "bin/aliasResolver.mjs").replace(/\\/g, "/")}";
|
||||
import { registerAliasResolver } from ${JSON.stringify(repoFileUrl("bin/aliasResolver.mjs"))};
|
||||
const ok = await registerAliasResolver(${JSON.stringify(REPO_ROOT)});
|
||||
if (!ok) { console.error("FAIL: registerAliasResolver returned false"); process.exit(2); }
|
||||
try {
|
||||
const m = await import(${JSON.stringify(join(REPO_ROOT, "src/shared/network/outboundUrlGuard.ts").replace(/\\/g, "/"))});
|
||||
const m = await import(${JSON.stringify(repoFileUrl("src/shared/network/outboundUrlGuard.ts"))});
|
||||
const keys = Object.keys(m).sort().join(",");
|
||||
console.log("OK:" + keys);
|
||||
} catch (err) {
|
||||
@@ -286,7 +293,7 @@ describe("aliasResolver end-to-end (#7791 regression)", () => {
|
||||
|
||||
it("does not interfere with bare/relative specifiers (regression guard)", () => {
|
||||
const script = `
|
||||
import { registerAliasResolver } from "${join(REPO_ROOT, "bin/aliasResolver.mjs").replace(/\\/g, "/")}";
|
||||
import { registerAliasResolver } from ${JSON.stringify(repoFileUrl("bin/aliasResolver.mjs"))};
|
||||
await registerAliasResolver(${JSON.stringify(REPO_ROOT)});
|
||||
// node:fs must still resolve via the default resolver
|
||||
const fs = await import("node:fs");
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
resolveModelFromTargetOptions,
|
||||
runCliTarget,
|
||||
} from "../../../bin/cli/commands/run.mjs";
|
||||
import { logicalArgs } from "./_helpers/shellArgs.mjs";
|
||||
|
||||
test("resolveRunTarget resolves aliases", () => {
|
||||
assert.equal(resolveRunTarget("claude"), "claude");
|
||||
@@ -39,7 +40,7 @@ test("buildRunPlan for claude includes env diff and model injection", async () =
|
||||
assert.equal(plan.target, "claude");
|
||||
assert.equal(plan.baseUrl, "http://localhost:20128");
|
||||
assert.equal(plan.model, "gpt-5");
|
||||
assert.equal(plan.args.includes("--help"), true);
|
||||
assert.equal(logicalArgs(plan.args).includes("--help"), true);
|
||||
assert.equal(plan.envDiff.changedOrAdded.includes("ANTHROPIC_AUTH_TOKEN"), true);
|
||||
assert.equal(plan.authSource, "option");
|
||||
assert.equal(plan.command.includes("claude"), true);
|
||||
@@ -54,9 +55,9 @@ test("buildRunPlan for codex injects model into provider args", async () => {
|
||||
assert.equal(plan.target, "codex");
|
||||
assert.equal(plan.baseUrl, "http://localhost:20128");
|
||||
assert.equal(plan.model, "glm/glm-4.5");
|
||||
assert.equal(plan.args.includes("--help"), true);
|
||||
assert.equal(logicalArgs(plan.args).includes("--help"), true);
|
||||
assert.equal(
|
||||
plan.args.some((a) => String(a).includes("model_providers.omniroute.model")),
|
||||
logicalArgs(plan.args).some((a) => a.includes("model_providers.omniroute.model")),
|
||||
true
|
||||
);
|
||||
assert.equal(plan.authSource, "option");
|
||||
@@ -70,7 +71,7 @@ test("buildRunPlan for Aider uses its OpenAI-compatible root endpoint", async ()
|
||||
);
|
||||
assert.equal(plan.target, "aider");
|
||||
assert.equal(plan.baseUrl, "https://relay.example.test");
|
||||
assert.deepEqual(plan.args.slice(0, 2), ["--model", "openai/glm/glm-5.2"]);
|
||||
assert.deepEqual(logicalArgs(plan.args).slice(0, 2), ["--model", "openai/glm/glm-5.2"]);
|
||||
assert.equal(plan.envDiff.changedOrAdded.includes("OPENAI_API_BASE"), true);
|
||||
assert.equal(plan.envDiff.changedOrAdded.includes("OPENAI_API_KEY"), true);
|
||||
});
|
||||
@@ -82,7 +83,7 @@ test("buildRunPlan for Goose injects provider and model without writing config",
|
||||
["session"]
|
||||
);
|
||||
assert.equal(plan.target, "goose");
|
||||
assert.deepEqual(plan.args, ["session"]);
|
||||
assert.deepEqual(logicalArgs(plan.args), ["session"]);
|
||||
assert.equal(plan.envDiff.changedOrAdded.includes("GOOSE_PROVIDER"), true);
|
||||
assert.equal(plan.envDiff.changedOrAdded.includes("GOOSE_MODEL"), true);
|
||||
assert.equal(plan.envDiff.changedOrAdded.includes("OPENAI_HOST"), true);
|
||||
@@ -95,7 +96,7 @@ test("buildRunPlan for OpenCode uses an ephemeral compatible config", async () =
|
||||
["run", "reply OK"]
|
||||
);
|
||||
assert.equal(plan.target, "opencode");
|
||||
assert.deepEqual(plan.args.slice(0, 2), ["--model", "omniroute/glm/glm-5.2"]);
|
||||
assert.deepEqual(logicalArgs(plan.args).slice(0, 2), ["--model", "omniroute/glm/glm-5.2"]);
|
||||
assert.equal(plan.envDiff.changedOrAdded.includes("OPENCODE_CONFIG_CONTENT"), true);
|
||||
assert.equal(plan.envDiff.changedOrAdded.includes("OMNIROUTE_API_KEY"), true);
|
||||
assert.equal(plan.configOverlay, "OPENCODE_CONFIG_CONTENT (process environment only)");
|
||||
@@ -109,7 +110,7 @@ test("buildRunPlan for Qwen requires a deterministic model and injects only env
|
||||
["-p", "reply OK"]
|
||||
);
|
||||
assert.equal(plan.target, "qwen");
|
||||
assert.deepEqual(plan.args.slice(0, 2), ["--model", "glm/glm-5.2"]);
|
||||
assert.deepEqual(logicalArgs(plan.args).slice(0, 2), ["--model", "glm/glm-5.2"]);
|
||||
assert.equal(plan.envDiff.changedOrAdded.includes("OMNIROUTE_API_KEY"), true);
|
||||
assert.equal(plan.configOverlay, "temporary QWEN_HOME (removed after exit)");
|
||||
await assert.rejects(
|
||||
@@ -126,7 +127,7 @@ test("buildRunPlan for Gemini points the CLI at the /v1beta surface via env", as
|
||||
);
|
||||
assert.equal(plan.target, "gemini");
|
||||
assert.equal(plan.baseUrl, "https://relay.example.test");
|
||||
assert.deepEqual(plan.args.slice(0, 2), ["--model", "glm/glm-5.2"]);
|
||||
assert.deepEqual(logicalArgs(plan.args).slice(0, 2), ["--model", "glm/glm-5.2"]);
|
||||
assert.equal(plan.envDiff.changedOrAdded.includes("GOOGLE_GEMINI_BASE_URL"), true);
|
||||
assert.equal(plan.envDiff.changedOrAdded.includes("GEMINI_API_KEY"), true);
|
||||
assert.equal(plan.envDiff.changedOrAdded.includes("GEMINI_DEFAULT_AUTH_TYPE"), true);
|
||||
|
||||
68
tests/unit/cli/windows-esm-import-paths.test.ts
Normal file
68
tests/unit/cli/windows-esm-import-paths.test.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
|
||||
const PROJECT_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
|
||||
|
||||
// Regression guard for the Windows-only ESM loader failure:
|
||||
//
|
||||
// Error: Only URLs with a scheme in: file, data, and node are supported by
|
||||
// the default ESM loader. On Windows, absolute paths must be valid file://
|
||||
// URLs. Received protocol 'e:'
|
||||
//
|
||||
// `import()` resolves its specifier as a URL. A POSIX absolute path like
|
||||
// /home/x/src/lib/db/combos.ts happens to also be a valid relative URL, so
|
||||
// interpolating it works by accident. A Windows absolute path is
|
||||
// E:\checkout\src\lib\db\combos.ts, whose leading drive letter the loader
|
||||
// parses as the URL scheme `e:` and rejects. Every such call site must go
|
||||
// through pathToFileURL().
|
||||
//
|
||||
// This broke `omniroute combo list/create/delete/switch` on Windows whenever
|
||||
// the CLI fell back to direct DB access with the server offline.
|
||||
|
||||
const CLI_DIR = path.join(PROJECT_ROOT, "bin", "cli");
|
||||
|
||||
function collectMjsFiles(dir: string): string[] {
|
||||
const out: string[] = [];
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) out.push(...collectMjsFiles(full));
|
||||
else if (entry.name.endsWith(".mjs")) out.push(full);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
test("bin/cli never passes an interpolated absolute path to dynamic import()", () => {
|
||||
// Matches import(`${ANY_ROOT_CONST}/...`) — a raw filesystem path, not a URL.
|
||||
const badImport = /\bimport\(\s*`\$\{[A-Za-z_$][\w$]*\}\//;
|
||||
|
||||
const offenders: string[] = [];
|
||||
for (const file of collectMjsFiles(CLI_DIR)) {
|
||||
const source = fs.readFileSync(file, "utf8");
|
||||
source.split(/\r?\n/).forEach((line, i) => {
|
||||
if (badImport.test(line)) {
|
||||
offenders.push(`${path.relative(PROJECT_ROOT, file)}:${i + 1}: ${line.trim()}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
assert.deepEqual(
|
||||
offenders,
|
||||
[],
|
||||
"dynamic import() of an interpolated absolute path fails on Windows; " +
|
||||
`wrap the path in pathToFileURL(...).href instead:\n${offenders.join("\n")}`,
|
||||
);
|
||||
});
|
||||
|
||||
test("runtime.mjs resolves db modules to a file:// URL", async () => {
|
||||
const source = fs.readFileSync(path.join(CLI_DIR, "runtime.mjs"), "utf8");
|
||||
assert.match(source, /pathToFileURL/, "runtime.mjs must build file:// URLs for dynamic imports");
|
||||
|
||||
// The real proof: the db fallback modules actually load on this platform.
|
||||
const runtime = await import(pathToFileURL(path.join(CLI_DIR, "runtime.mjs")).href);
|
||||
const ctx = await runtime.withDb(async (c: { kind: string; db: unknown }) => c);
|
||||
assert.equal(ctx.kind, "db");
|
||||
assert.ok(ctx.db);
|
||||
});
|
||||
@@ -22,3 +22,13 @@ test("valid input yields no rejected keys", () => {
|
||||
assert.deepEqual(r.rejected, []);
|
||||
assert.deepEqual(r.sanitized, { rpm: 10, tpm: 20 });
|
||||
});
|
||||
|
||||
// #11251 added `maxWaitMs` to the Zod validation schema and the
|
||||
// EditConnectionModal UI, but not to this separate allowlist — saving the
|
||||
// field from the dashboard threw "Refusing to persist rateLimitOverrides
|
||||
// with rejected keys: maxWaitMs" (500) on every attempt.
|
||||
test("sanitizeRateLimitOverrides accepts maxWaitMs (#11251 follow-up)", () => {
|
||||
const r = sanitizeRateLimitOverrides({ minTime: 500, maxWaitMs: 30000 });
|
||||
assert.deepEqual(r.rejected, []);
|
||||
assert.deepEqual(r.sanitized, { minTime: 500, maxWaitMs: 30000 });
|
||||
});
|
||||
|
||||
83
tests/unit/combo-max-global-attempts-config.test.ts
Normal file
83
tests/unit/combo-max-global-attempts-config.test.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* tests/unit/combo-max-global-attempts-config.test.ts
|
||||
*
|
||||
* Issue #11134: the shared per-request combo attempt budget was the hardcoded
|
||||
* `MAX_GLOBAL_ATTEMPTS = 30` in comboPredicates.ts, with no env/config override
|
||||
* (confirmed by the repo owner on the issue). Operators running large combos
|
||||
* (or wanting to fail fast on a dead pool) could neither raise nor lower it.
|
||||
*
|
||||
* This mirrors the established `clampComboDepth` pattern exactly: an operator
|
||||
* knob (`config.maxGlobalAttempts`) that can raise the default (30) or lower it,
|
||||
* but never above `MAX_GLOBAL_ATTEMPTS_HARD_CAP` — an unbounded attempt budget
|
||||
* is the same runaway-request DoS risk that motivated MAX_COMBO_DEPTH_HARD_CAP.
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
test("clampGlobalAttempts — clamps to [1, hard cap]; invalid → default 30", async () => {
|
||||
const { clampGlobalAttempts, MAX_GLOBAL_ATTEMPTS, MAX_GLOBAL_ATTEMPTS_HARD_CAP } =
|
||||
await import("../../open-sse/services/combo.ts");
|
||||
assert.equal(MAX_GLOBAL_ATTEMPTS, 30, "default budget unchanged");
|
||||
assert.equal(MAX_GLOBAL_ATTEMPTS_HARD_CAP, 200, "absolute safety ceiling");
|
||||
|
||||
// Honors a LOWER configured budget (fail fast on a dead pool — the #11134 symptom).
|
||||
assert.equal(clampGlobalAttempts(1), 1);
|
||||
assert.equal(clampGlobalAttempts(5), 5);
|
||||
// Honors a HIGHER configured budget (large combos legitimately need more).
|
||||
assert.equal(clampGlobalAttempts(120), 120);
|
||||
// …but never past the hard cap.
|
||||
assert.equal(clampGlobalAttempts(10_000), 200, "hard cap at 200");
|
||||
// Invalid values fall back to the default, never disabling the budget.
|
||||
assert.equal(clampGlobalAttempts(0), 30, "0 invalid → default 30");
|
||||
assert.equal(clampGlobalAttempts(-4), 30, "negative → default 30");
|
||||
assert.equal(clampGlobalAttempts(undefined), 30, "undefined → default 30");
|
||||
assert.equal(clampGlobalAttempts("abc"), 30, "non-numeric → default 30");
|
||||
assert.equal(clampGlobalAttempts(Number.NaN), 30, "NaN → default 30");
|
||||
assert.equal(clampGlobalAttempts(Infinity), 30, "Infinity → default 30 (never unbounded)");
|
||||
assert.equal(clampGlobalAttempts(4.9), 4, "floors to 4");
|
||||
});
|
||||
|
||||
test("DEFAULT_COMBO_CONFIG — exposes maxGlobalAttempts so the cascade can override it", async () => {
|
||||
const { getDefaultComboConfig, resolveComboConfig } =
|
||||
await import("../../open-sse/services/comboConfig.ts");
|
||||
assert.equal(getDefaultComboConfig().maxGlobalAttempts, 30, "default present in config surface");
|
||||
|
||||
// Per-combo config wins over the global default (standard cascade).
|
||||
const resolved = resolveComboConfig({ config: { maxGlobalAttempts: 7 } }, {});
|
||||
assert.equal(resolved.maxGlobalAttempts, 7);
|
||||
|
||||
// settings.comboDefaults layer also applies.
|
||||
const fromGlobal = resolveComboConfig({}, { comboDefaults: { maxGlobalAttempts: 50 } });
|
||||
assert.equal(fromGlobal.maxGlobalAttempts, 50);
|
||||
});
|
||||
|
||||
test("dispatchPrelude — configured budget reaches nesting.attemptBudget.limit", async () => {
|
||||
const { buildDefaultNesting } = await import("../../open-sse/services/combo/dispatchPrelude.ts");
|
||||
// buildDefaultNesting only reads maxComboDepth/maxGlobalAttempts off config;
|
||||
// the full resolved-config type is irrelevant to this assertion.
|
||||
const build = (cfg: Record<string, unknown>) =>
|
||||
(
|
||||
buildDefaultNesting as (
|
||||
n: null,
|
||||
name: string,
|
||||
c: unknown
|
||||
) => { attemptBudget: { limit: number } }
|
||||
)(null, "c", cfg);
|
||||
|
||||
// Unset → historical default of 30.
|
||||
assert.equal(build({}).attemptBudget.limit, 30);
|
||||
// Configured lower → honored (fail fast).
|
||||
assert.equal(build({ maxGlobalAttempts: 6 }).attemptBudget.limit, 6);
|
||||
// Configured higher → honored.
|
||||
assert.equal(build({ maxGlobalAttempts: 90 }).attemptBudget.limit, 90);
|
||||
// Absurd → hard-capped, never unbounded.
|
||||
assert.equal(build({ maxGlobalAttempts: 1e9 }).attemptBudget.limit, 200);
|
||||
});
|
||||
|
||||
test("combo schema — accepts maxGlobalAttempts within the hard cap, rejects beyond", async () => {
|
||||
const { comboRuntimeConfigSchema: schema } =
|
||||
await import("../../src/shared/validation/schemas/combo.ts");
|
||||
assert.equal(schema.parse({ maxGlobalAttempts: 45 }).maxGlobalAttempts, 45);
|
||||
assert.equal(schema.safeParse({ maxGlobalAttempts: 201 }).success, false, "beyond hard cap");
|
||||
assert.equal(schema.safeParse({ maxGlobalAttempts: 0 }).success, false, "0 rejected");
|
||||
});
|
||||
@@ -0,0 +1,164 @@
|
||||
// @vitest-environment jsdom
|
||||
import React, { act } from "react";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
|
||||
vi.mock("next-intl", () => ({
|
||||
useTranslations: () => (key: string) => key,
|
||||
}));
|
||||
|
||||
vi.mock("@/store/notificationStore", () => ({
|
||||
useNotificationStore: () => ({ notify: vi.fn() }),
|
||||
}));
|
||||
|
||||
vi.mock("@/store/emailPrivacyStore", () => ({
|
||||
default: () => ({ hidden: false, toggle: vi.fn() }),
|
||||
}));
|
||||
|
||||
// Expanding "Advanced settings" also mounts ProviderTierField (#7818), which
|
||||
// fetches its current tier override on mount independent of this modal's own
|
||||
// save flow. Mock it out — its network call is unrelated to maxWaitMs.
|
||||
vi.mock(
|
||||
"@/app/(dashboard)/dashboard/providers/[id]/components/modals/providerTierFieldApi",
|
||||
() => ({
|
||||
fetchProviderTierOverride: vi.fn().mockResolvedValue(""),
|
||||
saveProviderTierOverride: vi.fn().mockResolvedValue(undefined),
|
||||
})
|
||||
);
|
||||
|
||||
const { default: EditConnectionModal } =
|
||||
await import("../../../src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditConnectionModal.tsx");
|
||||
|
||||
let container: HTMLDivElement;
|
||||
let root: Root;
|
||||
|
||||
beforeEach(() => {
|
||||
container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
act(() => root.unmount());
|
||||
container.remove();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
function renderModal(connection: Record<string, unknown>) {
|
||||
act(() => {
|
||||
root.render(
|
||||
<EditConnectionModal
|
||||
isOpen={true}
|
||||
connection={connection}
|
||||
providerId={connection.provider as string}
|
||||
onSave={vi.fn().mockResolvedValue(undefined)}
|
||||
onClose={vi.fn()}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function expandAdvancedSettings() {
|
||||
// The "Rate Limit Overrides" section (like the rest of the advanced fields)
|
||||
// is collapsed by default behind the "Advanced settings" disclosure toggle.
|
||||
const toggle = container.querySelector(
|
||||
'button[aria-controls="edit-connection-advanced-settings"]'
|
||||
) as HTMLButtonElement | null;
|
||||
expect(toggle).not.toBeNull();
|
||||
act(() => {
|
||||
toggle!.click();
|
||||
});
|
||||
}
|
||||
|
||||
function findMaxWaitMsInput(): HTMLInputElement | null {
|
||||
const label = Array.from(container.querySelectorAll("label")).find(
|
||||
(el) => el.textContent === "rateLimitOverridesMaxWaitMsLabel"
|
||||
);
|
||||
const forId = label?.getAttribute("for");
|
||||
return forId ? (container.querySelector(`#${forId}`) as HTMLInputElement | null) : null;
|
||||
}
|
||||
|
||||
function clickSave() {
|
||||
const button = Array.from(container.querySelectorAll("button")).find(
|
||||
(b) => b.textContent === "save"
|
||||
);
|
||||
expect(button).toBeTruthy();
|
||||
button!.click();
|
||||
}
|
||||
|
||||
describe("EditConnectionModal — maxWaitMs rate-limit override", () => {
|
||||
it("renders an empty maxWaitMs field for a connection with no override", () => {
|
||||
renderModal({
|
||||
id: "conn-1",
|
||||
provider: "nvidia",
|
||||
authType: "apikey",
|
||||
name: "key",
|
||||
rateLimitOverrides: { rpm: 30 },
|
||||
});
|
||||
expandAdvancedSettings();
|
||||
const input = findMaxWaitMsInput();
|
||||
expect(input).not.toBeNull();
|
||||
expect(input?.value).toBe("");
|
||||
});
|
||||
|
||||
it("preserves a persisted maxWaitMs override in form state", () => {
|
||||
renderModal({
|
||||
id: "conn-2",
|
||||
provider: "nvidia",
|
||||
authType: "apikey",
|
||||
name: "key",
|
||||
rateLimitOverrides: { maxWaitMs: 45000 },
|
||||
});
|
||||
expandAdvancedSettings();
|
||||
const input = findMaxWaitMsInput();
|
||||
expect(input?.value).toBe("45000");
|
||||
});
|
||||
|
||||
it("submits the entered maxWaitMs as rateLimitOverrides.maxWaitMs", async () => {
|
||||
// The "Rate Limit Overrides" section only renders for non-OAuth
|
||||
// connections (`{!isOAuth && (...)}` wraps it, same gate as
|
||||
// rpm/minTime/maxConcurrent). formData.apiKey stays "" (untouched by this
|
||||
// test), so handleSubmit's `!isOAuth && formData.apiKey` validation-fetch
|
||||
// branch is skipped and the save completes synchronously without mocking
|
||||
// `fetch`.
|
||||
const onSave = vi.fn().mockResolvedValue(undefined);
|
||||
act(() => {
|
||||
root.render(
|
||||
<EditConnectionModal
|
||||
isOpen={true}
|
||||
connection={{
|
||||
id: "conn-3",
|
||||
provider: "nvidia",
|
||||
authType: "apikey",
|
||||
name: "key",
|
||||
}}
|
||||
providerId="nvidia"
|
||||
onSave={onSave}
|
||||
onClose={vi.fn()}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
expandAdvancedSettings();
|
||||
const input = findMaxWaitMsInput();
|
||||
expect(input).not.toBeNull();
|
||||
const setter = Object.getOwnPropertyDescriptor(
|
||||
window.HTMLInputElement.prototype,
|
||||
"value"
|
||||
)!.set!;
|
||||
await act(async () => {
|
||||
setter.call(input, "45000");
|
||||
input!.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
clickSave();
|
||||
});
|
||||
|
||||
expect(onSave).toHaveBeenCalledTimes(1);
|
||||
const updates = onSave.mock.calls[0][0] as {
|
||||
rateLimitOverrides: Record<string, number> | null;
|
||||
};
|
||||
expect(updates.rateLimitOverrides?.maxWaitMs).toBe(45000);
|
||||
});
|
||||
});
|
||||
112
tests/unit/effort-tiers-loop-catalog-e2e.test.ts
Normal file
112
tests/unit/effort-tiers-loop-catalog-e2e.test.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* effort_tiers loop — I1 end-to-end proof: a set recorded through the REAL
|
||||
* record path (executor-style connection key) surfaces in the REAL catalog
|
||||
* response (/api/v1/models), including the learned-only variant entry.
|
||||
* Never "fix" this test by injecting the same string on both sides.
|
||||
*/
|
||||
import { test, after, beforeEach } 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-effort-loop-e2e-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "loop-e2e-secret";
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const providersDb = await import("../../src/lib/db/providers.ts");
|
||||
const modelsDb = await import("../../src/lib/db/models.ts");
|
||||
const v1ModelsCatalog = await import("../../src/app/api/v1/models/catalog.ts");
|
||||
const { recordLearnedReasoningEffort, __test_resetLearnedReasoningEffortCaps } =
|
||||
await import("../../open-sse/services/learnedReasoningEffortCaps.ts");
|
||||
|
||||
async function resetStorage() {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
// Copied verbatim from sync-reasoning-supported-efforts-7694.test.ts
|
||||
async function seedProviderConnection(provider: string) {
|
||||
return providersDb.createProviderConnection({
|
||||
provider,
|
||||
authType: "apikey",
|
||||
name: `${provider}-${Math.random().toString(16).slice(2, 8)}`,
|
||||
apiKey: `${provider}-key`,
|
||||
isActive: true,
|
||||
testStatus: "active",
|
||||
});
|
||||
}
|
||||
|
||||
test.beforeEach(async () => {
|
||||
__test_resetLearnedReasoningEffortCaps();
|
||||
await resetStorage();
|
||||
});
|
||||
|
||||
test.after(async () => {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("learned set flows end-to-end into /v1/models capabilities and variant entries", async () => {
|
||||
// Non-namespaced id on purpose: mirrors the real incident model
|
||||
// (x-preview-f-free) where sm.id === the executor-visible post-strip id.
|
||||
const MODEL_ID = "loop-model-e2e";
|
||||
const connection = await seedProviderConnection("huggingface");
|
||||
await modelsDb.replaceSyncedAvailableModelsForConnection("huggingface", connection.id, [
|
||||
{
|
||||
id: MODEL_ID,
|
||||
name: "Loop Model E2E",
|
||||
supportedThinkingEfforts: ["none", "low", "medium", "high"],
|
||||
},
|
||||
]);
|
||||
|
||||
// Simulate the real 400 learning path (base.ts calls exactly this, with the
|
||||
// executor's CONNECTION id as provider key):
|
||||
recordLearnedReasoningEffort("openai-compatible-chat-eaff6869", MODEL_ID, ["low", "high", "max"]);
|
||||
|
||||
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
|
||||
new Request("http://localhost/api/v1/models")
|
||||
);
|
||||
assert.equal(response.status, 200);
|
||||
const body = (await response.json()) as {
|
||||
data: Array<{ id: string; capabilities?: { effort_tiers?: string[] } }>;
|
||||
};
|
||||
|
||||
const baseEntry = body.data.find((m) => m.id.endsWith(MODEL_ID));
|
||||
assert.ok(baseEntry, "base entry present");
|
||||
assert.deepEqual(baseEntry!.capabilities?.effort_tiers, ["low", "high", "max"]);
|
||||
|
||||
const maxVariant = body.data.find((m) => m.id === `${baseEntry!.id}-max`);
|
||||
assert.ok(maxVariant, "learned-only tier synthesized as a variant entry");
|
||||
});
|
||||
|
||||
test("excluded provider (glm) never surfaces effort_tiers, learned or synced", async () => {
|
||||
const MODEL_ID = "glm-4-flash";
|
||||
const connection = await seedProviderConnection("glm");
|
||||
await modelsDb.replaceSyncedAvailableModelsForConnection("glm", connection.id, [
|
||||
{
|
||||
id: MODEL_ID,
|
||||
name: "GLM 4 Flash",
|
||||
supportedThinkingEfforts: ["none", "low", "medium", "high"],
|
||||
},
|
||||
]);
|
||||
recordLearnedReasoningEffort("glm-connection-1", MODEL_ID, ["low", "high"]);
|
||||
|
||||
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
|
||||
new Request("http://localhost/api/v1/models")
|
||||
);
|
||||
assert.equal(response.status, 200);
|
||||
const body = (await response.json()) as {
|
||||
data: Array<{ id: string; capabilities?: { effort_tiers?: string[] } }>;
|
||||
};
|
||||
|
||||
const baseEntry = body.data.find((m) => m.id.endsWith(MODEL_ID));
|
||||
assert.ok(baseEntry, "base entry present");
|
||||
assert.equal(
|
||||
baseEntry!.capabilities?.effort_tiers,
|
||||
undefined,
|
||||
"glm owns its own -{effort} suffix mechanism — the catalog must not also expose effort_tiers"
|
||||
);
|
||||
});
|
||||
@@ -10,7 +10,7 @@
|
||||
* GET https://g4f.space/api/groq/... → live Groq backend
|
||||
*
|
||||
* Verifies each of the 5 sub-path providers is wired end-to-end the same way as
|
||||
* the other no-key gateway providers (hackclub, uncloseai):
|
||||
* the other no-key gateway providers (uncloseai):
|
||||
* - present in the executor REGISTRY with a no-key OpenAI-compatible shape
|
||||
* - resolvable through getExecutor() (falls through to DefaultExecutor)
|
||||
* - listed in AGGREGATOR_PROVIDER_IDS so it shows up in the aggregator
|
||||
@@ -84,7 +84,7 @@ for (const [id, subPath] of Object.entries(SUB_PATHS)) {
|
||||
test(`#6650 ${id} is classified as an aggregator/gateway provider`, () => {
|
||||
assert.ok(
|
||||
AGGREGATOR_PROVIDER_IDS.has(id),
|
||||
`${id} must be listed in AGGREGATOR_PROVIDER_IDS alongside hackclub/uncloseai`
|
||||
`${id} must be listed in AGGREGATOR_PROVIDER_IDS alongside uncloseai`
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
139
tests/unit/hackclub-removed.test.ts
Normal file
139
tests/unit/hackclub-removed.test.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* #11176 — Hack Club AI (hackclub) must be FULLY removed from the live catalogs.
|
||||
*
|
||||
* PR #11118/#11123 removed the provider from the open-sse REGISTRY, but the
|
||||
* canonical shared catalog (`src/shared/constants/providers/`) kept the entry,
|
||||
* so the dashboard, the alias resolver, the icon set, the onboarding i18n
|
||||
* strings and the generated provider reference kept advertising a provider the
|
||||
* router can no longer serve. This test pins the complete removal:
|
||||
*
|
||||
* 1. no canonical catalog (API-key / web-cookie / OAuth / no-auth / local /
|
||||
* search / audio / upstream-proxy / cloud-agent / system) has a `hackclub` entry;
|
||||
* 2. no provider in any catalog claims the `hc` alias (it belonged to hackclub);
|
||||
* 3. the provider/catalog source trees carry no `hackclub` mention at all
|
||||
* (structural grep — catches comments referencing it as a living provider);
|
||||
* 4. the icon registry and the shipped SVG asset are gone;
|
||||
* 5. the onboarding i18n description key is gone (en + all locale mirrors).
|
||||
*
|
||||
* Historical mentions intentionally KEPT (release records, not catalog):
|
||||
* CHANGELOG.md, docs/i18n/*\/CHANGELOG.md, and the removal migration
|
||||
* src/lib/db/migrations/162_remove_hackclub_provider.sql (it IS the removal).
|
||||
*/
|
||||
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 {
|
||||
APIKEY_PROVIDERS,
|
||||
WEB_COOKIE_PROVIDERS,
|
||||
OAUTH_PROVIDERS,
|
||||
FREE_PROVIDERS,
|
||||
NOAUTH_PROVIDERS,
|
||||
LOCAL_PROVIDERS,
|
||||
SEARCH_PROVIDERS,
|
||||
AUDIO_ONLY_PROVIDERS,
|
||||
UPSTREAM_PROXY_PROVIDERS,
|
||||
CLOUD_AGENT_PROVIDERS,
|
||||
SYSTEM_PROVIDERS,
|
||||
} from "../../src/shared/constants/providers.ts";
|
||||
|
||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
||||
|
||||
const CATALOGS: Record<string, Record<string, { id?: string; alias?: string }>> = {
|
||||
APIKEY_PROVIDERS,
|
||||
WEB_COOKIE_PROVIDERS,
|
||||
OAUTH_PROVIDERS: OAUTH_PROVIDERS as Record<string, { id?: string; alias?: string }>,
|
||||
FREE_PROVIDERS: FREE_PROVIDERS as Record<string, { id?: string; alias?: string }>,
|
||||
NOAUTH_PROVIDERS: NOAUTH_PROVIDERS as Record<string, { id?: string; alias?: string }>,
|
||||
LOCAL_PROVIDERS: LOCAL_PROVIDERS as Record<string, { id?: string; alias?: string }>,
|
||||
SEARCH_PROVIDERS: SEARCH_PROVIDERS as Record<string, { id?: string; alias?: string }>,
|
||||
AUDIO_ONLY_PROVIDERS: AUDIO_ONLY_PROVIDERS as Record<string, { id?: string; alias?: string }>,
|
||||
UPSTREAM_PROXY_PROVIDERS: UPSTREAM_PROXY_PROVIDERS as Record<string, { id?: string; alias?: string }>,
|
||||
CLOUD_AGENT_PROVIDERS: CLOUD_AGENT_PROVIDERS as Record<string, { id?: string; alias?: string }>,
|
||||
SYSTEM_PROVIDERS: SYSTEM_PROVIDERS as Record<string, { id?: string; alias?: string }>,
|
||||
};
|
||||
|
||||
function walk(dir: string): string[] {
|
||||
const out: string[] = [];
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) out.push(...walk(full));
|
||||
else if (/\.(ts|tsx|mts|json)$/.test(entry.name)) out.push(full);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
test("hackclub is absent from every canonical provider catalog", () => {
|
||||
for (const [name, catalog] of Object.entries(CATALOGS)) {
|
||||
assert.equal(
|
||||
"hackclub" in catalog,
|
||||
false,
|
||||
`${name} still contains a hackclub entry (#11176)`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("no provider in any catalog claims the `hc` alias (it belonged to hackclub)", () => {
|
||||
const holders: string[] = [];
|
||||
for (const [name, catalog] of Object.entries(CATALOGS)) {
|
||||
for (const [key, p] of Object.entries(catalog)) {
|
||||
if (p?.alias === "hc" || key === "hc") holders.push(`${name}:${key}`);
|
||||
}
|
||||
}
|
||||
assert.deepEqual(holders, [], `alias "hc" still claimed by: ${holders.join(", ")}`);
|
||||
});
|
||||
|
||||
test("no hackclub mention survives in the provider/catalog source trees", () => {
|
||||
const scopedDirs = [
|
||||
path.join(ROOT, "src", "shared", "constants", "providers"),
|
||||
path.join(ROOT, "open-sse", "config"),
|
||||
];
|
||||
const offenders: string[] = [];
|
||||
for (const dir of scopedDirs) {
|
||||
for (const file of walk(dir)) {
|
||||
if (/hack\s*club|hackclub/i.test(fs.readFileSync(file, "utf8"))) {
|
||||
offenders.push(path.relative(ROOT, file));
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.deepEqual(
|
||||
offenders,
|
||||
[],
|
||||
`hackclub mentions left in catalog sources: ${offenders.join(", ")}`
|
||||
);
|
||||
});
|
||||
|
||||
test("hackclub icon registration and shipped SVG asset are gone", () => {
|
||||
const iconSource = fs.readFileSync(
|
||||
path.join(ROOT, "src", "shared", "components", "ProviderIcon.tsx"),
|
||||
"utf8"
|
||||
);
|
||||
assert.equal(
|
||||
/hackclub/i.test(iconSource),
|
||||
false,
|
||||
"ProviderIcon.tsx still registers hackclub (#11176)"
|
||||
);
|
||||
assert.equal(
|
||||
fs.existsSync(path.join(ROOT, "public", "providers", "hackclub.svg")),
|
||||
false,
|
||||
"public/providers/hackclub.svg still shipped (#11176)"
|
||||
);
|
||||
});
|
||||
|
||||
test("onboarding i18n description for hackclub is gone from every locale", () => {
|
||||
const messagesDir = path.join(ROOT, "src", "i18n", "messages");
|
||||
const offenders: string[] = [];
|
||||
for (const file of fs.readdirSync(messagesDir)) {
|
||||
if (!file.endsWith(".json")) continue;
|
||||
const messages = JSON.parse(fs.readFileSync(path.join(messagesDir, file), "utf8"));
|
||||
const descriptions = messages?.providers?.onboardingProviderDescriptions;
|
||||
if (descriptions && "hackclub" in descriptions) offenders.push(file);
|
||||
}
|
||||
assert.deepEqual(
|
||||
offenders,
|
||||
[],
|
||||
`onboardingProviderDescriptions.hackclub still present in: ${offenders.join(", ")}`
|
||||
);
|
||||
});
|
||||
@@ -18,7 +18,7 @@ after(() => {
|
||||
|
||||
// ── REASONING_EFFORT_ORDER ──────────────────────────────────────────────────
|
||||
|
||||
test("REASONING_EFFORT_ORDER is none < minimal < low < medium < high < xhigh < max", () => {
|
||||
test("REASONING_EFFORT_ORDER is none < minimal < low < medium < high < xhigh < max < ultra", () => {
|
||||
assert.deepEqual(REASONING_EFFORT_ORDER, [
|
||||
"none",
|
||||
"minimal",
|
||||
@@ -27,6 +27,24 @@ test("REASONING_EFFORT_ORDER is none < minimal < low < medium < high < xhigh < m
|
||||
"high",
|
||||
"xhigh",
|
||||
"max",
|
||||
"ultra",
|
||||
]);
|
||||
});
|
||||
|
||||
test("REASONING_EFFORT_ORDER ends with ultra", () => {
|
||||
assert.equal(REASONING_EFFORT_ORDER.at(-1), "ultra");
|
||||
});
|
||||
test("parseReasoningEffortEnum extracts please use low, high, or max", () => {
|
||||
const err =
|
||||
"This model always engages in thinking and cannot be disabled; please use low, high, or max";
|
||||
assert.deepEqual(parseReasoningEffortEnum(err), ["low", "high", "max"]);
|
||||
});
|
||||
test("parseReasoningEffortEnum extracts please use with ultra", () => {
|
||||
assert.deepEqual(parseReasoningEffortEnum("please use low, high, max, ultra"), [
|
||||
"low",
|
||||
"high",
|
||||
"max",
|
||||
"ultra",
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -70,9 +88,15 @@ test("records the highest recognized value from the accepted list", () => {
|
||||
"medium",
|
||||
"low",
|
||||
"minimal",
|
||||
]);
|
||||
assert.equal(learned, "high");
|
||||
assert.equal(getLearnedReasoningEffort("ovh", "qwen3-coder-30b-a3b-instruct"), "high");
|
||||
]) as unknown as Set<string>;
|
||||
assert.ok(learned instanceof Set);
|
||||
assert.ok(learned.has("high"));
|
||||
assert.equal(
|
||||
(
|
||||
getLearnedReasoningEffort("ovh", "qwen3-coder-30b-a3b-instruct") as unknown as Set<string>
|
||||
).has("high"),
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
test("returns null and stores nothing when acceptedValues has no recognized token", () => {
|
||||
@@ -89,26 +113,84 @@ test("monotonic decrease: a later, higher accepted-list never ratchets the cap b
|
||||
"medium",
|
||||
"high",
|
||||
"xhigh",
|
||||
]);
|
||||
assert.equal(learned, "medium");
|
||||
assert.equal(getLearnedReasoningEffort("acme", "model-x"), "medium");
|
||||
]) as unknown as Set<string>;
|
||||
assert.equal(learned.size, 3);
|
||||
assert.ok(learned.has("medium"));
|
||||
assert.equal((getLearnedReasoningEffort("acme", "model-x") as unknown as Set<string>).size, 3);
|
||||
});
|
||||
|
||||
test("a later, lower accepted-list does ratchet the cap down", () => {
|
||||
recordLearnedReasoningEffort("acme", "model-x", ["none", "low", "medium", "high"]);
|
||||
const learned = recordLearnedReasoningEffort("acme", "model-x", ["none", "low"]);
|
||||
assert.equal(learned, "low");
|
||||
assert.equal(getLearnedReasoningEffort("acme", "model-x"), "low");
|
||||
const learned = recordLearnedReasoningEffort("acme", "model-x", [
|
||||
"none",
|
||||
"low",
|
||||
]) as unknown as Set<string>;
|
||||
assert.equal(learned.size, 2);
|
||||
assert.ok(learned.has("low"));
|
||||
assert.equal((getLearnedReasoningEffort("acme", "model-x") as unknown as Set<string>).size, 2);
|
||||
});
|
||||
|
||||
test("clampToLearned medium→low when accepted is low,high,max", async () => {
|
||||
const { clampToLearned } = await import("../../open-sse/services/learnedReasoningEffortCaps.ts");
|
||||
assert.equal(clampToLearned("medium", new Set(["low", "high", "max"])), "low");
|
||||
});
|
||||
test("clampToLearned xhigh→high when accepted is low,high,max", async () => {
|
||||
const { clampToLearned } = await import("../../open-sse/services/learnedReasoningEffortCaps.ts");
|
||||
assert.equal(clampToLearned("xhigh", new Set(["low", "high", "max"])), "high");
|
||||
});
|
||||
test("clampToLearned ultra→max when accepted is low,high,max", async () => {
|
||||
const { clampToLearned } = await import("../../open-sse/services/learnedReasoningEffortCaps.ts");
|
||||
assert.equal(clampToLearned("ultra", new Set(["low", "high", "max"])), "max");
|
||||
});
|
||||
test("clampToLearned ultra→medium when accepted is low,medium", async () => {
|
||||
const { clampToLearned } = await import("../../open-sse/services/learnedReasoningEffortCaps.ts");
|
||||
assert.equal(clampToLearned("ultra", new Set(["low", "medium"])), "medium");
|
||||
});
|
||||
test("clampToLearned high→medium when accepted is low,medium", async () => {
|
||||
const { clampToLearned } = await import("../../open-sse/services/learnedReasoningEffortCaps.ts");
|
||||
assert.equal(clampToLearned("high", new Set(["low", "medium"])), "medium");
|
||||
});
|
||||
test("clampToLearned returns null when already accepted", async () => {
|
||||
const { clampToLearned } = await import("../../open-sse/services/learnedReasoningEffortCaps.ts");
|
||||
assert.equal(clampToLearned("low", new Set(["low", "high", "max"])), null);
|
||||
});
|
||||
test("clampToLearned returns null when effort < min (no upgrade)", async () => {
|
||||
const { clampToLearned } = await import("../../open-sse/services/learnedReasoningEffortCaps.ts");
|
||||
assert.equal(clampToLearned("low", new Set(["high", "max"])), null);
|
||||
});
|
||||
test("clampToLearned returns null for turbo (not in ORDER)", async () => {
|
||||
const { clampToLearned } = await import("../../open-sse/services/learnedReasoningEffortCaps.ts");
|
||||
assert.equal(clampToLearned("turbo", new Set(["low", "high", "max"])), null);
|
||||
});
|
||||
test("clampToLearned returns null when effort is none but accepted is low,high,max", async () => {
|
||||
const { clampToLearned } = await import("../../open-sse/services/learnedReasoningEffortCaps.ts");
|
||||
assert.equal(clampToLearned("none", new Set(["low", "high", "max"])), null);
|
||||
});
|
||||
test("recordLearned stores Set and getLearned returns Set", () => {
|
||||
const s = recordLearnedReasoningEffort("acme", "m1", ["low", "high", "max"]);
|
||||
assert.ok(s instanceof Set);
|
||||
assert.deepEqual([...(s as unknown as Set<string>)].sort(), ["high", "low", "max"]);
|
||||
const g = getLearnedReasoningEffort("acme", "m1");
|
||||
assert.ok(g instanceof Set);
|
||||
});
|
||||
test("monotonicity incomparable: keep existing when neither subset", () => {
|
||||
recordLearnedReasoningEffort("acme", "m4", ["low", "high", "max"]);
|
||||
const s4 = recordLearnedReasoningEffort("acme", "m4", ["low", "medium"]);
|
||||
assert.equal((s4 as unknown as Set<string>).size, 3);
|
||||
assert.ok((s4 as unknown as Set<string>).has("high"));
|
||||
});
|
||||
test("getLearnedReasoningEffort returns null for unknown provider+model", () => {
|
||||
assert.equal(getLearnedReasoningEffort("acme", "unknown-model"), null);
|
||||
});
|
||||
|
||||
test("getLearnedReasoningEffort is keyed case-insensitively on provider+model", () => {
|
||||
recordLearnedReasoningEffort("OVH", "Qwen3-Coder-30B", ["none", "high"]);
|
||||
assert.equal(getLearnedReasoningEffort("ovh", "qwen3-coder-30b"), "high");
|
||||
assert.equal(getLearnedReasoningEffort("OVH", "QWEN3-CODER-30B"), "high");
|
||||
assert.ok(
|
||||
(getLearnedReasoningEffort("ovh", "qwen3-coder-30b") as unknown as Set<string>).has("high")
|
||||
);
|
||||
assert.ok(
|
||||
(getLearnedReasoningEffort("OVH", "QWEN3-CODER-30B") as unknown as Set<string>).has("high")
|
||||
);
|
||||
});
|
||||
|
||||
test("different providers for the same model id have independent caps", () => {
|
||||
@@ -124,3 +206,45 @@ test("handles empty/null provider or model gracefully", () => {
|
||||
assert.equal(recordLearnedReasoningEffort("", "m", ["high"]), null);
|
||||
assert.equal(recordLearnedReasoningEffort("p", "", ["high"]), null);
|
||||
});
|
||||
|
||||
// ── getLearnedReasoningEffortForModel ────────────────────────────────────────
|
||||
|
||||
import { getLearnedReasoningEffortForModel } from "../../open-sse/services/learnedReasoningEffortCaps.ts";
|
||||
|
||||
test("getLearnedReasoningEffortForModel finds a set recorded under any provider key", () => {
|
||||
recordLearnedReasoningEffort("openai-compatible-chat-eaff6869", "X-Preview-F-Free", [
|
||||
"low",
|
||||
"high",
|
||||
"max",
|
||||
]);
|
||||
const set = getLearnedReasoningEffortForModel("x-preview-f-free");
|
||||
assert.ok(set);
|
||||
assert.deepEqual([...set].sort(), ["high", "low", "max"]);
|
||||
});
|
||||
|
||||
test("getLearnedReasoningEffortForModel intersects when multiple providers disagree", () => {
|
||||
recordLearnedReasoningEffort("conn-a", "shared-model", ["low", "high", "max"]);
|
||||
recordLearnedReasoningEffort("conn-b", "shared-model", ["low"]);
|
||||
const set = getLearnedReasoningEffortForModel("shared-model");
|
||||
assert.ok(set);
|
||||
assert.deepEqual([...set], ["low"]);
|
||||
});
|
||||
|
||||
test("getLearnedReasoningEffortForModel returns null when nothing learned or empty model", () => {
|
||||
assert.equal(getLearnedReasoningEffortForModel("never-learned"), null);
|
||||
assert.equal(getLearnedReasoningEffortForModel(""), null);
|
||||
assert.equal(getLearnedReasoningEffortForModel(undefined), null);
|
||||
});
|
||||
|
||||
test("recordLearnedReasoningEffort warns when every token is unrecognized", () => {
|
||||
const warnings: string[] = [];
|
||||
const orig = console.warn;
|
||||
console.warn = (msg: string) => warnings.push(msg);
|
||||
try {
|
||||
const result = recordLearnedReasoningEffort("p", "m", ["bogus-one", "bogus-two"]);
|
||||
assert.equal(result, null);
|
||||
assert.ok(warnings.some((w) => w.includes("reasoning_effort") && w.includes("bogus-one")));
|
||||
} finally {
|
||||
console.warn = orig;
|
||||
}
|
||||
});
|
||||
|
||||
144
tests/unit/lmstudio-connection-baseurl-11233.test.ts
Normal file
144
tests/unit/lmstudio-connection-baseurl-11233.test.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
const TEST_DATA_DIR = mkdtempSync(join(tmpdir(), "omniroute-lmstudio-embedding-11233-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
|
||||
const { parseEmbeddingModel } = await import("../../open-sse/config/embeddingRegistry.ts");
|
||||
const { handleEmbedding } = await import("../../open-sse/handlers/embeddings.ts");
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const { createProviderConnection } = await import("../../src/lib/db/providers.ts");
|
||||
const { createEmbeddingResponse } = await import("../../src/lib/embeddings/service.ts");
|
||||
|
||||
test.after(() => {
|
||||
core.resetDbInstance();
|
||||
rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
// Issue #11233: the dashboard stores LM Studio connections under the provider
|
||||
// id "lm-studio" (hyphenated), but the embedding registry keys the provider as
|
||||
// "lmstudio" with no alias. Two symptoms resulted:
|
||||
// 1. "lm-studio/<model>" embedding requests failed with 400 unknown provider.
|
||||
// 2. "lmstudio/<model>" requests always hit the hardcoded localhost:1234
|
||||
// endpoint, ignoring the baseUrl of the configured connection.
|
||||
// The fix mirrors the ollama-local pattern from #2824/#9225: an embedding
|
||||
// provider alias plus optional (non-auth) connection hydration and the same
|
||||
// baseUrl normalization in the handler.
|
||||
|
||||
test("lm-studio model strings resolve to the lmstudio embedding provider", () => {
|
||||
assert.deepEqual(parseEmbeddingModel("lm-studio/nomic-embed-text"), {
|
||||
provider: "lmstudio",
|
||||
model: "nomic-embed-text",
|
||||
});
|
||||
});
|
||||
|
||||
test("lmstudio routes to the configured connection baseUrl", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
let capturedUrl: string | null = null;
|
||||
globalThis.fetch = async (url) => {
|
||||
capturedUrl = String(url);
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
data: [{ object: "embedding", embedding: [0.1, 0.2], index: 0 }],
|
||||
usage: { prompt_tokens: 2, total_tokens: 2 },
|
||||
}),
|
||||
{ status: 200, headers: { "content-type": "application/json" } }
|
||||
);
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await handleEmbedding({
|
||||
body: { model: "lmstudio/nomic-embed-text", input: "hello" },
|
||||
resolvedProvider: {
|
||||
id: "lmstudio",
|
||||
baseUrl: "http://localhost:1234/v1/embeddings",
|
||||
authType: "none",
|
||||
authHeader: "none",
|
||||
models: [],
|
||||
},
|
||||
resolvedModel: "nomic-embed-text",
|
||||
credentials: {
|
||||
providerSpecificData: { baseUrl: "http://192.168.1.50:1234/v1" },
|
||||
},
|
||||
log: null,
|
||||
});
|
||||
|
||||
assert.equal(result.success, true);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
|
||||
assert.equal(capturedUrl, "http://192.168.1.50:1234/v1/embeddings");
|
||||
});
|
||||
|
||||
test("lmstudio keeps the static localhost default without credentials", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
let capturedUrl: string | null = null;
|
||||
globalThis.fetch = async (url) => {
|
||||
capturedUrl = String(url);
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
data: [{ object: "embedding", embedding: [0.3, 0.4], index: 0 }],
|
||||
usage: { prompt_tokens: 2, total_tokens: 2 },
|
||||
}),
|
||||
{ status: 200, headers: { "content-type": "application/json" } }
|
||||
);
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await handleEmbedding({
|
||||
body: { model: "lmstudio/nomic-embed-text", input: "hello" },
|
||||
credentials: null,
|
||||
log: null,
|
||||
});
|
||||
|
||||
assert.equal(result.success, true);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
|
||||
assert.equal(capturedUrl, "http://localhost:1234/v1/embeddings");
|
||||
});
|
||||
|
||||
test("lmstudio service hydrates the lm-studio connection host without requiring a key", async () => {
|
||||
await createProviderConnection({
|
||||
provider: "lm-studio",
|
||||
authType: "none",
|
||||
name: "LAN LM Studio",
|
||||
isActive: true,
|
||||
providerSpecificData: { baseUrl: "http://10.20.0.60:1234/v1/" },
|
||||
});
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
let captured: { url: string; headers: Record<string, string> } | null = null;
|
||||
globalThis.fetch = async (url, options = {}) => {
|
||||
captured = {
|
||||
url: String(url),
|
||||
headers: (options.headers as Record<string, string>) || {},
|
||||
};
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
data: [{ object: "embedding", embedding: [0.5, 0.6], index: 0 }],
|
||||
usage: { prompt_tokens: 2, total_tokens: 2 },
|
||||
}),
|
||||
{ status: 200, headers: { "content-type": "application/json" } }
|
||||
);
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await createEmbeddingResponse({
|
||||
model: "lm-studio/nomic-embed-text",
|
||||
input: "hello",
|
||||
});
|
||||
assert.equal(response.status, 200);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
|
||||
assert.ok(captured);
|
||||
assert.equal(captured.url, "http://10.20.0.60:1234/v1/embeddings");
|
||||
assert.equal(captured.headers.Authorization, undefined);
|
||||
});
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
buildSessionsSummary,
|
||||
buildTelemetryPayload,
|
||||
projectAdaptiveAdmissionSummary,
|
||||
projectChatAdmissionSummary,
|
||||
} from "../../src/lib/monitoring/observability.ts";
|
||||
|
||||
test("buildSessionsSummary returns sticky counts and ordered top sessions", () => {
|
||||
@@ -336,3 +337,71 @@ test("buildHealthPayload projects allowlisted adaptiveAdmission aggregates only"
|
||||
assert.equal(projectAdaptiveAdmissionSummary(null), null);
|
||||
assert.equal(projectAdaptiveAdmissionSummary(undefined), null);
|
||||
});
|
||||
|
||||
// #11244: the STRUCTURAL chat-admission gate (chatBodyAdmission.ts) must surface in
|
||||
// the health payload next to — never instead of — the adaptive snapshot, with only
|
||||
// the documented low-cardinality fields projected.
|
||||
test("buildHealthPayload projects allowlisted structural chatAdmission fields only", () => {
|
||||
const snapshot = {
|
||||
activeHeavy: 1,
|
||||
activeHealthyHeadroom: 1,
|
||||
waiting: 2,
|
||||
queuedBytes: 524_288,
|
||||
shedTotal: 3,
|
||||
shedsByReason: { queue_timeout: 2, queued_bytes_budget: 1 },
|
||||
lanes: [
|
||||
{ key: "key_c49d1c242feda590", waiting: 1 },
|
||||
{ key: "anonymous", waiting: 1 },
|
||||
],
|
||||
// Extra keys that must never leak into the public payload.
|
||||
internalController: { secret: "controller-state" },
|
||||
rawAuthorization: "Bearer raw-SHOULD-NOT-LEAK",
|
||||
} as unknown as import("../../src/lib/monitoring/observability.ts").ChatAdmissionSnapshot;
|
||||
|
||||
const payload = buildHealthPayload({
|
||||
appVersion: "9.9.9",
|
||||
settings: { setupComplete: false },
|
||||
connections: [],
|
||||
circuitBreakers: [],
|
||||
rateLimitStatus: {},
|
||||
learnedLimits: {},
|
||||
lockouts: {},
|
||||
localProviders: {},
|
||||
inflightRequests: 0,
|
||||
quotaMonitorSummary: {
|
||||
active: 0,
|
||||
alerting: 0,
|
||||
exhausted: 0,
|
||||
errors: 0,
|
||||
statusCounts: { starting: 0, idle: 0, healthy: 0, warning: 0, exhausted: 0, error: 0 },
|
||||
byProvider: {},
|
||||
},
|
||||
quotaMonitorMonitors: [],
|
||||
activeSessions: [],
|
||||
chatAdmission: snapshot,
|
||||
});
|
||||
|
||||
assert.deepEqual(payload.chatAdmission, {
|
||||
activeHeavy: 1,
|
||||
activeHealthyHeadroom: 1,
|
||||
waiting: 2,
|
||||
queuedBytes: 524_288,
|
||||
shedTotal: 3,
|
||||
shedsByReason: { queue_timeout: 2, queued_bytes_budget: 1 },
|
||||
lanes: [
|
||||
{ key: "key_c49d1c242feda590", waiting: 1 },
|
||||
{ key: "anonymous", waiting: 1 },
|
||||
],
|
||||
});
|
||||
// The adaptive projection is untouched by the new key.
|
||||
assert.equal(payload.adaptiveAdmission, null);
|
||||
|
||||
const json = JSON.stringify(payload);
|
||||
assert.equal(json.includes("controller-state"), false);
|
||||
assert.equal(json.includes("raw-SHOULD-NOT-LEAK"), false);
|
||||
assert.equal(json.includes("internalController"), false);
|
||||
|
||||
// Absent / null snapshot projects to null (degraded path parity).
|
||||
assert.equal(projectChatAdmissionSummary(null), null);
|
||||
assert.equal(projectChatAdmissionSummary(undefined), null);
|
||||
});
|
||||
|
||||
152
tests/unit/openrouter-key-validation-auth-endpoint.test.ts
Normal file
152
tests/unit/openrouter-key-validation-auth-endpoint.test.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
// #11226 — OpenRouter key validation was vacuous: the probe targeted the PUBLIC
|
||||
// /api/v1/models endpoint, which answers 200 to any key (or no key at all), so a
|
||||
// bad key was saved as "valid" and only failed later on real chat traffic with the
|
||||
// upstream 401 "User not found.". The authenticated key-info endpoint
|
||||
// (/api/v1/auth/key) is the correct probe: 200 = valid, 401 = invalid.
|
||||
//
|
||||
// The fetch stubs below mimic the REAL OpenRouter behavior verified live:
|
||||
// GET /api/v1/models → 200 without any auth (public catalog)
|
||||
// GET /api/v1/auth/key → 401 {"error":{"message":"User not found.","code":401}} for a bad key
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const { getRegistryEntry } = await import("../../open-sse/config/providerRegistry.ts");
|
||||
const { validateProviderApiKey } = await import("../../src/lib/providers/validation.ts");
|
||||
const { testProviderApiKey } = await import("../../bin/cli/provider-test.mjs");
|
||||
|
||||
const AUTH_KEY_URL = "https://openrouter.ai/api/v1/auth/key";
|
||||
const PUBLIC_MODELS_URL = "https://openrouter.ai/api/v1/models";
|
||||
|
||||
const BAD_KEY = "sk-or-v1-definitely-invalid-key";
|
||||
const GOOD_KEY = "sk-or-v1-valid-key";
|
||||
|
||||
interface RecordedCall {
|
||||
url: string;
|
||||
authorization: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stub fetch with the real OpenRouter behavior: /models is public (always 200),
|
||||
* /auth/key requires a valid bearer (401 "User not found." otherwise).
|
||||
*/
|
||||
function stubRealOpenRouter() {
|
||||
const calls: RecordedCall[] = [];
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => {
|
||||
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
||||
const headers = new Headers(
|
||||
init?.headers ?? (input instanceof Request ? input.headers : undefined)
|
||||
);
|
||||
calls.push({ url, authorization: headers.get("authorization") });
|
||||
|
||||
if (url.startsWith(AUTH_KEY_URL)) {
|
||||
const bearer = headers.get("authorization") || "";
|
||||
if (bearer === `Bearer ${GOOD_KEY}`) {
|
||||
return new Response(JSON.stringify({ data: { label: "ok", is_free_tier: false } }), {
|
||||
status: 200,
|
||||
});
|
||||
}
|
||||
return new Response(JSON.stringify({ error: { message: "User not found.", code: 401 } }), {
|
||||
status: 401,
|
||||
});
|
||||
}
|
||||
if (url.includes("/models")) {
|
||||
// Public catalog — answers 200 regardless of the Authorization header.
|
||||
return new Response(JSON.stringify({ data: [] }), { status: 200 });
|
||||
}
|
||||
return new Response("{}", { status: 404 });
|
||||
}) as typeof fetch;
|
||||
return {
|
||||
calls,
|
||||
restore: () => {
|
||||
globalThis.fetch = originalFetch;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("openrouter registry — authenticated key-validation endpoint (#11226)", () => {
|
||||
it("declares the authenticated /auth/key probe as its key-test endpoint", () => {
|
||||
const entry = getRegistryEntry("openrouter");
|
||||
assert.ok(entry, "openrouter must be registered in the execution registry");
|
||||
assert.equal(entry.testKeyModelsUrl, AUTH_KEY_URL);
|
||||
});
|
||||
|
||||
it("marks a bad key INVALID even though the public /models endpoint answers 200", async () => {
|
||||
const stub = stubRealOpenRouter();
|
||||
try {
|
||||
const result = await validateProviderApiKey({ provider: "openrouter", apiKey: BAD_KEY });
|
||||
assert.equal(result.valid, false, "bad key must not validate against the public catalog");
|
||||
assert.equal(result.error, "Invalid API key");
|
||||
assert.deepEqual(
|
||||
stub.calls.map((c) => c.url),
|
||||
[AUTH_KEY_URL],
|
||||
"must probe the authenticated key endpoint, not the public /models"
|
||||
);
|
||||
assert.equal(stub.calls[0].authorization, `Bearer ${BAD_KEY}`);
|
||||
} finally {
|
||||
stub.restore();
|
||||
}
|
||||
});
|
||||
|
||||
it("marks a good key VALID via /auth/key and never falls back to the chat probe", async () => {
|
||||
const stub = stubRealOpenRouter();
|
||||
try {
|
||||
const result = await validateProviderApiKey({ provider: "openrouter", apiKey: GOOD_KEY });
|
||||
assert.equal(result.valid, true);
|
||||
assert.equal(result.error, null);
|
||||
assert.deepEqual(
|
||||
stub.calls.map((c) => c.url),
|
||||
[AUTH_KEY_URL]
|
||||
);
|
||||
} finally {
|
||||
stub.restore();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("omniroute providers test — openrouter probe (#11226)", () => {
|
||||
it("marks a bad key INVALID even though the public /models endpoint answers 200", async () => {
|
||||
const stub = stubRealOpenRouter();
|
||||
try {
|
||||
const result = await testProviderApiKey({ provider: "openrouter", apiKey: BAD_KEY });
|
||||
assert.equal(result.valid, false, "CLI test must not trust the public /models endpoint");
|
||||
assert.equal(result.error, "Invalid API key");
|
||||
assert.deepEqual(
|
||||
stub.calls.map((c) => c.url),
|
||||
[AUTH_KEY_URL]
|
||||
);
|
||||
} finally {
|
||||
stub.restore();
|
||||
}
|
||||
});
|
||||
|
||||
it("marks a good key VALID via /auth/key", async () => {
|
||||
const stub = stubRealOpenRouter();
|
||||
try {
|
||||
const result = await testProviderApiKey({ provider: "openrouter", apiKey: GOOD_KEY });
|
||||
assert.equal(result.valid, true);
|
||||
assert.equal(result.error, null);
|
||||
assert.deepEqual(
|
||||
stub.calls.map((c) => c.url),
|
||||
[AUTH_KEY_URL]
|
||||
);
|
||||
} finally {
|
||||
stub.restore();
|
||||
}
|
||||
});
|
||||
|
||||
it("does not change the probe for other OpenAI-like providers (openai still uses /models)", async () => {
|
||||
const stub = stubRealOpenRouter();
|
||||
try {
|
||||
const result = await testProviderApiKey({ provider: "openai", apiKey: GOOD_KEY });
|
||||
assert.equal(result.valid, true);
|
||||
assert.deepEqual(
|
||||
stub.calls.map((c) => c.url),
|
||||
["https://api.openai.com/v1/models"]
|
||||
);
|
||||
assert.ok(!stub.calls.some((c) => c.url === PUBLIC_MODELS_URL));
|
||||
} finally {
|
||||
stub.restore();
|
||||
}
|
||||
});
|
||||
});
|
||||
78
tests/unit/pack-boot-runtime-paths.test.ts
Normal file
78
tests/unit/pack-boot-runtime-paths.test.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
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 {
|
||||
REQUIRED_MACHINE_TOKEN_RUNTIME_FILES,
|
||||
REQUIRED_SQLJS_RUNTIME_FILES,
|
||||
} from "../../scripts/check/check-pack-boot.mjs";
|
||||
import { PACK_ARTIFACT_NEVER_ALLOWED_SEGMENTS } from "../../scripts/build/pack-artifact-policy.ts";
|
||||
import * as sqliteRuntime from "../../bin/cli/runtime/sqliteRuntime.mjs";
|
||||
|
||||
// Coherence guard for the v3.8.50 publish blocker (#11242): check:pack-artifact
|
||||
// FAILS any tarball path containing a node_modules segment (files[] excludes them
|
||||
// via "!**/node_modules/**"), while check:pack-boot REQUIRED sql.js under the
|
||||
// vendored dist/node_modules/ location — a path the tarball can never contain,
|
||||
// so the two gates could never be green at the same time. The npm packaging
|
||||
// model is now dependency-based: sql.js and node-machine-id are declared
|
||||
// `dependencies` that a clean install places under <packageRoot>/node_modules/,
|
||||
// and better-sqlite3 is an optionalDependency installed natively per platform.
|
||||
// These tests pin that contract so neither gate can drift back into conflict.
|
||||
|
||||
const REPO_ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
||||
const PKG = JSON.parse(readFileSync(path.join(REPO_ROOT, "package.json"), "utf8")) as {
|
||||
dependencies?: Record<string, string>;
|
||||
optionalDependencies?: Record<string, string>;
|
||||
};
|
||||
|
||||
test("pack-boot required runtime files never reference a never-publishable vendored path", () => {
|
||||
const requiredFiles = [...REQUIRED_SQLJS_RUNTIME_FILES, ...REQUIRED_MACHINE_TOKEN_RUNTIME_FILES];
|
||||
assert.ok(requiredFiles.length > 0, "pack-boot must require at least one runtime file");
|
||||
for (const requiredPath of requiredFiles) {
|
||||
for (const segment of PACK_ARTIFACT_NEVER_ALLOWED_SEGMENTS) {
|
||||
const vendoredPrefix = `dist/${segment}/`;
|
||||
assert.ok(
|
||||
!requiredPath.includes(vendoredPrefix),
|
||||
`"${requiredPath}" lives under ${vendoredPrefix} — check:pack-artifact bans any ` +
|
||||
`tarball path with a "${segment}" segment, so check:pack-boot must require the ` +
|
||||
`dependency-installed location (node_modules/<pkg>) instead (#11242)`
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("sql.js and node-machine-id are declared runtime dependencies (npm installs them)", () => {
|
||||
assert.ok(
|
||||
PKG.dependencies?.["sql.js"],
|
||||
"sql.js must stay in dependencies so a clean install provides node_modules/sql.js"
|
||||
);
|
||||
assert.ok(
|
||||
PKG.dependencies?.["node-machine-id"],
|
||||
"node-machine-id must stay in dependencies so a clean install provides node_modules/node-machine-id"
|
||||
);
|
||||
});
|
||||
|
||||
test("the lazy better-sqlite3 runtime install targets the declared optionalDependency major", () => {
|
||||
const spec = (sqliteRuntime as Record<string, unknown>).BETTER_SQLITE3_VERSION;
|
||||
assert.equal(
|
||||
typeof spec,
|
||||
"string",
|
||||
"bin/cli/runtime/sqliteRuntime.mjs must export BETTER_SQLITE3_VERSION"
|
||||
);
|
||||
const declared = PKG.optionalDependencies?.["better-sqlite3"];
|
||||
assert.ok(declared, "package.json must declare better-sqlite3 as an optionalDependency");
|
||||
|
||||
const majorOf = (versionSpec: string): number => {
|
||||
const match = versionSpec.match(/(\d+)\./);
|
||||
assert.ok(match, `"${versionSpec}" must contain a semver major`);
|
||||
return Number(match[1]);
|
||||
};
|
||||
assert.equal(
|
||||
majorOf(spec as string),
|
||||
majorOf(declared),
|
||||
`lazy runtime install "${spec}" drifted from optionalDependencies.better-sqlite3 ` +
|
||||
`"${declared}" — the fallback install must track the same major (#11242)`
|
||||
);
|
||||
});
|
||||
@@ -5,7 +5,8 @@
|
||||
* iteration order silently won, emitting a startup warning and shadowing a real
|
||||
* provider:
|
||||
* - "kimi" → kimi-web (shadowed the kimi provider that gained a dedicated executor)
|
||||
* - "hc" → hackclub (shadowed huggingchat)
|
||||
* - "hc" → the provider that held it shadowed huggingchat (it was later
|
||||
* removed entirely, #11176; huggingchat keeps its own id as alias)
|
||||
*
|
||||
* The decision: the primary provider keeps the short alias; the web/secondary
|
||||
* variant takes its own id as alias. This test pins both the global uniqueness
|
||||
|
||||
@@ -11,9 +11,23 @@ function parse(overrides: unknown) {
|
||||
}
|
||||
|
||||
test("rateLimitOverrides: valid object with all fields", () => {
|
||||
const r = parse({ rpm: 100, tpm: 50000, tpd: 1000000, minTime: 100, maxConcurrent: 5 });
|
||||
const r = parse({
|
||||
rpm: 100,
|
||||
tpm: 50000,
|
||||
tpd: 1000000,
|
||||
minTime: 100,
|
||||
maxConcurrent: 5,
|
||||
maxWaitMs: 45000,
|
||||
});
|
||||
assert.ok(r.success, String(r.error));
|
||||
assert.deepEqual(r.data.rateLimitOverrides, { rpm: 100, tpm: 50000, tpd: 1000000, minTime: 100, maxConcurrent: 5 });
|
||||
assert.deepEqual(r.data.rateLimitOverrides, {
|
||||
rpm: 100,
|
||||
tpm: 50000,
|
||||
tpd: 1000000,
|
||||
minTime: 100,
|
||||
maxConcurrent: 5,
|
||||
maxWaitMs: 45000,
|
||||
});
|
||||
});
|
||||
|
||||
test("rateLimitOverrides: partial fields", () => {
|
||||
@@ -71,3 +85,32 @@ test("rateLimitOverrides: all zeros is valid", () => {
|
||||
const r = parse({ rpm: 0, tpm: 0, tpd: 0, minTime: 0, maxConcurrent: 0 });
|
||||
assert.ok(r.success, String(r.error));
|
||||
});
|
||||
|
||||
test("rateLimitOverrides: valid maxWaitMs", () => {
|
||||
const r = parse({ maxWaitMs: 45000 });
|
||||
assert.ok(r.success, String(r.error));
|
||||
assert.deepEqual(r.data.rateLimitOverrides, { maxWaitMs: 45000 });
|
||||
});
|
||||
|
||||
test("rateLimitOverrides: maxWaitMs coerced from string", () => {
|
||||
const r = parse({ maxWaitMs: "30000" });
|
||||
assert.ok(r.success, String(r.error));
|
||||
assert.equal(r.data.rateLimitOverrides.maxWaitMs, 30000);
|
||||
});
|
||||
|
||||
test("rateLimitOverrides: rejects negative maxWaitMs", () => {
|
||||
assert.equal(parse({ maxWaitMs: -1 }).success, false);
|
||||
});
|
||||
|
||||
test("rateLimitOverrides: rejects float maxWaitMs", () => {
|
||||
assert.equal(parse({ maxWaitMs: 1.5 }).success, false);
|
||||
});
|
||||
|
||||
test("rateLimitOverrides: rejects maxWaitMs above 120000 ceiling", () => {
|
||||
assert.equal(parse({ maxWaitMs: 120001 }).success, false);
|
||||
});
|
||||
|
||||
test("rateLimitOverrides: maxWaitMs of 0 is valid (no override)", () => {
|
||||
const r = parse({ maxWaitMs: 0 });
|
||||
assert.ok(r.success, String(r.error));
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* (free tier via Discord signup).
|
||||
*
|
||||
* Verifies the new provider is wired end-to-end the same way as the other
|
||||
* aggregator/gateway providers (hackclub, chutes, glhf, ...):
|
||||
* aggregator/gateway providers (chutes, glhf, ...):
|
||||
* - present in the executor REGISTRY with an OpenAI-compatible shape
|
||||
* - resolvable through getExecutor() (falls through to DefaultExecutor,
|
||||
* same as every other `executor: "default"` registry entry)
|
||||
@@ -41,7 +41,7 @@ test("#6670 freetheai resolves through getExecutor() as a DefaultExecutor instan
|
||||
test("#6670 freetheai is classified as an aggregator/gateway provider", () => {
|
||||
assert.ok(
|
||||
AGGREGATOR_PROVIDER_IDS.has("freetheai"),
|
||||
"freetheai must be listed in AGGREGATOR_PROVIDER_IDS alongside hackclub/chutes/etc"
|
||||
"freetheai must be listed in AGGREGATOR_PROVIDER_IDS alongside chutes/etc"
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
// merge-train batch — independently bumped the gateways family too, landing at 231; Freebuff
|
||||
// (gateways, #10531) brings it to 232. #8864 moves uncloseai (gateways family) into
|
||||
// NOAUTH_PROVIDERS, dropping the APIKEY_PROVIDERS count to 231. Logfare (gateways, #10987) brings it back to 232.
|
||||
// #11176 removes hackclub (gateways family), landing at 231.
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
@@ -54,12 +55,12 @@ test("barrel still exports every catalog + key helpers", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("APIKEY_PROVIDERS merges the 6 family files into 232 entries (no loss / no dup)", async () => {
|
||||
test("APIKEY_PROVIDERS merges the 6 family files into 231 entries (no loss / no dup)", async () => {
|
||||
const keys = Object.keys((P as Record<string, object>).APIKEY_PROVIDERS);
|
||||
assert.equal(keys.length, 232);
|
||||
assert.equal(new Set(keys).size, 232, "duplicate keys after spread-merge");
|
||||
assert.equal(keys.length, 231);
|
||||
assert.equal(new Set(keys).size, 231, "duplicate keys after spread-merge");
|
||||
// the merged object's entry-count equals the sum of the 6 semantic family files; families are a
|
||||
// strict partition (every provider in exactly one), so the sum must be exactly 232.
|
||||
// strict partition (every provider in exactly one), so the sum must be exactly 231.
|
||||
const families: [string, string][] = [
|
||||
["gateways", "APIKEY_PROVIDERS_GATEWAYS"],
|
||||
["frontier-labs", "APIKEY_PROVIDERS_FRONTIER"],
|
||||
@@ -79,7 +80,7 @@ test("APIKEY_PROVIDERS merges the 6 family files into 232 entries (no loss / no
|
||||
seen.add(k);
|
||||
}
|
||||
}
|
||||
assert.equal(famTotal, 232, "families must partition all 232 providers");
|
||||
assert.equal(famTotal, 231, "families must partition all 231 providers");
|
||||
});
|
||||
|
||||
test("AI_PROVIDERS Proxy aggregates all sections; lookups resolve", () => {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Issue #6674 — Add Naga.ac and ChatAnywhere as gpt4free-ecosystem aggregator providers.
|
||||
*
|
||||
* Verifies both providers are wired end-to-end the same way as the other aggregator
|
||||
* gateway providers (g4f-groq, freetheai, hackclub):
|
||||
* gateway providers (g4f-groq, freetheai):
|
||||
* - present in the executor REGISTRY with an OpenAI-compatible shape
|
||||
* - resolvable through getExecutor() (falls through to DefaultExecutor)
|
||||
* - listed in AGGREGATOR_PROVIDER_IDS so they show up in the aggregator category
|
||||
|
||||
303
tests/unit/quota-exhaustion-cutoff-opencode.test.ts
Normal file
303
tests/unit/quota-exhaustion-cutoff-opencode.test.ts
Normal file
@@ -0,0 +1,303 @@
|
||||
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";
|
||||
|
||||
/**
|
||||
* #11234 — opencode-go quota preflight ignored the dashboard quota snapshots.
|
||||
*
|
||||
* Root cause (two gaps):
|
||||
*
|
||||
* A) `fetchOpencodeQuota` (open-sse/services/opencodeQuotaFetcher.ts) only
|
||||
* consulted the live upstream endpoint, which has no public quota API
|
||||
* (404 — see module JSDoc). It never read the quota snapshots the
|
||||
* dashboard scrape (`getOpenCodeGoUsage`, keyed session/weekly/mcp_monthly)
|
||||
* persists through `src/domain/quotaCache.ts`. Every preflight therefore
|
||||
* evaluated `null` and proceeded (fail-open), even with a sister
|
||||
* connection sitting at 0% weekly remaining in plain sight on the
|
||||
* dashboard.
|
||||
*
|
||||
* B) The sibling-selection latency gate in
|
||||
* `src/sse/services/auth.ts::getProviderCredentialsWithQuotaPreflight`
|
||||
* never consulted `resilience.quotaPreflight.enabled`
|
||||
* (QUOTA_PREFLIGHT_CUTOFF_ENABLED). That flag only armed the auto-strategy
|
||||
* candidate builder and the per-target cutoff for pinned connections, so
|
||||
* a priority combo over sibling opencode-go connections (connectionId
|
||||
* null at combo level) skipped preflight entirely.
|
||||
*
|
||||
* Fix:
|
||||
* A) The fetcher now synthesizes its triple-window quota from the cached
|
||||
* dashboard snapshots (read-only, accessors only, no re-scrape on the hot
|
||||
* path) when the live endpoint yields nothing — mapping
|
||||
* session→window_5h, weekly→window_weekly, mcp_monthly→window_monthly and
|
||||
* mirroring `getQuotaWindowStatus` semantics (expired resetAt = window has
|
||||
* rolled over = must not count as exhausted).
|
||||
* B) `resilience.quotaPreflight.enabled === true` now arms the
|
||||
* sibling-selection latency gate as well.
|
||||
*
|
||||
* These tests are the regression guards: fetcher-level for (A), selector-level
|
||||
* for (B).
|
||||
*/
|
||||
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omni-quota-11234-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
// Part (B): the operator flag must be ON before the resilience settings module
|
||||
// is first imported (its defaults are computed at module load).
|
||||
process.env.QUOTA_PREFLIGHT_CUTOFF_ENABLED = "true";
|
||||
process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "quota-11234-secret";
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
const coreDb = await import("../../src/lib/db/core.ts");
|
||||
const quotaSnapshotsDb = await import("../../src/lib/db/quotaSnapshots.ts");
|
||||
const quotaCache = await import("../../src/domain/quotaCache.ts");
|
||||
const providersDb = await import("../../src/lib/db/providers.ts");
|
||||
const apiKeysDb = await import("../../src/lib/db/apiKeys.ts");
|
||||
const { fetchOpencodeQuota, invalidateOpencodeQuotaCache } = await import(
|
||||
"../../open-sse/services/opencodeQuotaFetcher.ts"
|
||||
);
|
||||
const { evaluateQuotaCutoff, registerQuotaFetcher } = await import(
|
||||
"../../open-sse/services/quotaPreflight.ts"
|
||||
);
|
||||
const { buildAutoQuotaThresholds } = await import(
|
||||
"../../open-sse/services/combo/quotaExhaustionCutoff.ts"
|
||||
);
|
||||
const { resolveResilienceSettings } = await import("../../src/lib/resilience/settings.ts");
|
||||
const auth = await import("../../src/sse/services/auth.ts");
|
||||
|
||||
const PROVIDER = "opencode-go";
|
||||
// Dashboard scrape window keys (opencodeOllamaUsage.ts::OPENCODE_GO_QUOTA_ORDER)
|
||||
const DASH_SESSION = "session";
|
||||
const DASH_WEEKLY = "weekly";
|
||||
// Fetcher/preflight window keys (opencodeQuotaFetcher.ts registry)
|
||||
const WINDOW_5H = "window_5h";
|
||||
const WINDOW_WEEKLY = "window_weekly";
|
||||
|
||||
function seedSnapshot(
|
||||
connectionId: string,
|
||||
windowKey: string,
|
||||
remainingPercentage: number,
|
||||
nextResetAt: string | null
|
||||
) {
|
||||
quotaSnapshotsDb.saveQuotaSnapshot({
|
||||
provider: PROVIDER,
|
||||
connection_id: connectionId,
|
||||
window_key: windowKey,
|
||||
remaining_percentage: remainingPercentage,
|
||||
is_exhausted: remainingPercentage <= 0 ? 1 : 0,
|
||||
next_reset_at: nextResetAt,
|
||||
window_duration_ms: null,
|
||||
raw_data: null,
|
||||
});
|
||||
}
|
||||
|
||||
function dashboardConfiguredConnection(apiKey: string): Record<string, unknown> {
|
||||
// Mirrors the operator-configured dashboard scrape
|
||||
// (opencodeOllamaUsage.ts::resolveOpenCodeGoDashboardConfig).
|
||||
return {
|
||||
apiKey,
|
||||
providerSpecificData: {
|
||||
openCodeGoWorkspaceId: "ws-11234",
|
||||
openCodeGoAuthCookie: "auth-cookie-11234",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function hoursFromNow(hours: number): string {
|
||||
return new Date(Date.now() + hours * 3_600_000).toISOString();
|
||||
}
|
||||
|
||||
test.after(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
coreDb.resetDbInstance();
|
||||
apiKeysDb.resetApiKeyState();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test.afterEach(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
quotaCache.__clearForTests();
|
||||
});
|
||||
|
||||
// ─── (A) fetcher bridge: dashboard snapshots → QuotaInfo ────────────────────
|
||||
|
||||
test("#11234 dashboard snapshots feed the quota cutoff when the live endpoint has no quota API", async () => {
|
||||
const connectionId = `oc-11234-block-${Date.now()}`;
|
||||
let fetchCalls = 0;
|
||||
globalThis.fetch = async () => {
|
||||
fetchCalls += 1;
|
||||
return new Response(null, { status: 404 });
|
||||
};
|
||||
|
||||
// Dashboard shows: weekly fully drained (0% remaining, reset in 3 days),
|
||||
// session healthy (80% remaining).
|
||||
seedSnapshot(connectionId, DASH_WEEKLY, 0, hoursFromNow(72));
|
||||
seedSnapshot(connectionId, DASH_SESSION, 80, hoursFromNow(2));
|
||||
|
||||
const quota = await fetchOpencodeQuota(connectionId, dashboardConfiguredConnection("sk-test"));
|
||||
|
||||
assert.ok(quota, "fetcher must synthesize quota from dashboard snapshots when the live endpoint 404s");
|
||||
assert.equal(fetchCalls, 1, "snapshot bridge must be read-only — no re-scrape on the hot path");
|
||||
|
||||
// Key mapping: weekly → window_weekly (0% remaining = 100% used),
|
||||
// session → window_5h (80% remaining = 20% used).
|
||||
assert.equal(quota.windows?.[WINDOW_WEEKLY]?.percentUsed, 1);
|
||||
assert.ok(
|
||||
Math.abs((quota.windows?.[WINDOW_5H]?.percentUsed ?? 0) - 0.2) < 1e-9,
|
||||
`window_5h percentUsed should be ~0.2, got ${quota.windows?.[WINDOW_5H]?.percentUsed}`
|
||||
);
|
||||
|
||||
const decision = evaluateQuotaCutoff(
|
||||
quota,
|
||||
buildAutoQuotaThresholds(PROVIDER, undefined, null)
|
||||
);
|
||||
assert.equal(decision.proceed, false, "weekly at 0% remaining must block the connection");
|
||||
assert.equal(decision.reason, "quota_exhausted");
|
||||
|
||||
invalidateOpencodeQuotaCache(connectionId);
|
||||
});
|
||||
|
||||
test("#11234 a snapshot whose reset already passed must not count as exhausted", async () => {
|
||||
const connectionId = `oc-11234-expired-${Date.now()}`;
|
||||
globalThis.fetch = async () => new Response(null, { status: 404 });
|
||||
|
||||
// Weekly hit 0% but its reset is 1h in the PAST — the window rolled into a
|
||||
// fresh period, so the stale 0% must not block (mirrors
|
||||
// getQuotaWindowStatus: expired resetAt → reachedThreshold = false).
|
||||
seedSnapshot(connectionId, DASH_WEEKLY, 0, hoursFromNow(-1));
|
||||
seedSnapshot(connectionId, DASH_SESSION, 80, hoursFromNow(2));
|
||||
|
||||
const quota = await fetchOpencodeQuota(connectionId, dashboardConfiguredConnection("sk-test"));
|
||||
|
||||
assert.ok(quota, "the healthy session snapshot should still synthesize");
|
||||
assert.equal(
|
||||
quota.windows?.[WINDOW_WEEKLY],
|
||||
undefined,
|
||||
"an expired weekly window must be dropped from the synthesized quota"
|
||||
);
|
||||
|
||||
const decision = evaluateQuotaCutoff(
|
||||
quota,
|
||||
buildAutoQuotaThresholds(PROVIDER, undefined, null)
|
||||
);
|
||||
assert.equal(decision.proceed, true, "an expired weekly window must not block the connection");
|
||||
|
||||
invalidateOpencodeQuotaCache(connectionId);
|
||||
});
|
||||
|
||||
test("#11234 per-window threshold overrides apply to the mapped window_weekly key", async () => {
|
||||
const connectionId = `oc-11234-threshold-${Date.now()}`;
|
||||
globalThis.fetch = async () => new Response(null, { status: 404 });
|
||||
|
||||
// Weekly at 40% remaining — above the factory 2% cutoff (would proceed),
|
||||
// but below an operator override of 50% min-remaining for window_weekly.
|
||||
seedSnapshot(connectionId, DASH_WEEKLY, 40, hoursFromNow(72));
|
||||
seedSnapshot(connectionId, DASH_SESSION, 90, hoursFromNow(2));
|
||||
|
||||
const quota = await fetchOpencodeQuota(connectionId, dashboardConfiguredConnection("sk-test"));
|
||||
assert.ok(quota);
|
||||
|
||||
const factoryDecision = evaluateQuotaCutoff(
|
||||
quota,
|
||||
buildAutoQuotaThresholds(PROVIDER, undefined, null)
|
||||
);
|
||||
assert.equal(
|
||||
factoryDecision.proceed,
|
||||
true,
|
||||
"factory 2% cutoff must not block a window at 40% remaining"
|
||||
);
|
||||
|
||||
const settings = resolveResilienceSettings({
|
||||
resilienceSettings: {
|
||||
quotaPreflight: {
|
||||
enabled: true,
|
||||
providerWindowDefaults: { [PROVIDER]: { [WINDOW_WEEKLY]: 50 } },
|
||||
},
|
||||
},
|
||||
});
|
||||
const overrideDecision = evaluateQuotaCutoff(
|
||||
quota,
|
||||
buildAutoQuotaThresholds(PROVIDER, undefined, settings)
|
||||
);
|
||||
assert.equal(
|
||||
overrideDecision.proceed,
|
||||
false,
|
||||
"a 50% window_weekly override must block at 40% remaining — the override resolves against the mapped key"
|
||||
);
|
||||
|
||||
invalidateOpencodeQuotaCache(connectionId);
|
||||
});
|
||||
|
||||
test("#11234 fail-open preserved: configured dashboard with no snapshots still returns null", async () => {
|
||||
const connectionId = `oc-11234-failopen-${Date.now()}`;
|
||||
globalThis.fetch = async () => new Response(null, { status: 404 });
|
||||
|
||||
const quota = await fetchOpencodeQuota(connectionId, dashboardConfiguredConnection("sk-test"));
|
||||
assert.equal(quota, null, "no snapshots → fail-open (null), exactly as before");
|
||||
|
||||
invalidateOpencodeQuotaCache(connectionId);
|
||||
});
|
||||
|
||||
// ─── (B) flag scope: sibling-selection latency gate ─────────────────────────
|
||||
|
||||
test("#11234 quotaPreflight.enabled arms sibling selection: the exhausted sister is skipped for the healthy one", async () => {
|
||||
const tag = Date.now();
|
||||
|
||||
const exhausted = await providersDb.createProviderConnection({
|
||||
provider: PROVIDER,
|
||||
authType: "apikey",
|
||||
name: `oc-11234-exhausted-${tag}`,
|
||||
apiKey: "sk-oc-11234-exhausted",
|
||||
priority: 1,
|
||||
isActive: true,
|
||||
testStatus: "active",
|
||||
});
|
||||
const healthy = await providersDb.createProviderConnection({
|
||||
provider: PROVIDER,
|
||||
authType: "apikey",
|
||||
name: `oc-11234-healthy-${tag}`,
|
||||
apiKey: "sk-oc-11234-healthy",
|
||||
priority: 2,
|
||||
isActive: true,
|
||||
testStatus: "active",
|
||||
});
|
||||
|
||||
// Stub the upstream quota signal: the priority-1 sister is fully drained,
|
||||
// the priority-2 sister is healthy. No per-connection overrides, no
|
||||
// per-(provider, window) defaults, no legacy quotaPreflightEnabled flag,
|
||||
// factory 2% global threshold — so TODAY the latency gate skips preflight
|
||||
// entirely and the selector returns the exhausted sister. With
|
||||
// resilience.quotaPreflight.enabled arming the gate, preflight must run and
|
||||
// skip her.
|
||||
registerQuotaFetcher(PROVIDER, async (connectionId: string) => {
|
||||
if (connectionId === exhausted.id) {
|
||||
return {
|
||||
used: 100,
|
||||
total: 100,
|
||||
percentUsed: 1.0,
|
||||
resetAt: hoursFromNow(1),
|
||||
};
|
||||
}
|
||||
return { used: 0, total: 100, percentUsed: 0, resetAt: null };
|
||||
});
|
||||
|
||||
try {
|
||||
const selection = await auth.getProviderCredentialsWithQuotaPreflight(
|
||||
PROVIDER,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
const result = selection as { connectionId?: string } | null;
|
||||
|
||||
assert.equal(
|
||||
result?.connectionId,
|
||||
healthy.id,
|
||||
"with quotaPreflight.enabled the selector must skip the exhausted priority-1 sister and pick the healthy one"
|
||||
);
|
||||
} finally {
|
||||
await providersDb.deleteProviderConnection(exhausted.id);
|
||||
await providersDb.deleteProviderConnection(healthy.id);
|
||||
}
|
||||
});
|
||||
@@ -189,6 +189,48 @@ test("#6593 zai-web receives a provider-scoped 60s scheduling budget", () => {
|
||||
assert.equal(rateLimitManager.resolveRequestQueueMaxWaitMs("ZAI-WEB", 90_000), 90_000);
|
||||
});
|
||||
|
||||
test("#6593 connection maxWaitMs override takes priority over the zai-web scheduling budget", () => {
|
||||
rateLimitManager.refreshConnectionRateLimits("conn-maxwait-override", { maxWaitMs: 45_000 });
|
||||
try {
|
||||
// Non-special provider: override wins over the passed-in configured default.
|
||||
assert.equal(
|
||||
rateLimitManager.resolveRequestQueueMaxWaitMs("openai", 15_000, "conn-maxwait-override"),
|
||||
45_000
|
||||
);
|
||||
// zai-web: override wins over its hardcoded 60s floor too.
|
||||
assert.equal(
|
||||
rateLimitManager.resolveRequestQueueMaxWaitMs("zai-web", 15_000, "conn-maxwait-override"),
|
||||
45_000
|
||||
);
|
||||
} finally {
|
||||
rateLimitManager.refreshConnectionRateLimits("conn-maxwait-override", null);
|
||||
}
|
||||
});
|
||||
|
||||
test("#6593 a connection without a maxWaitMs override keeps the zai-web 60s floor", () => {
|
||||
rateLimitManager.refreshConnectionRateLimits("conn-no-maxwait-override", { rpm: 10 });
|
||||
try {
|
||||
assert.equal(
|
||||
rateLimitManager.resolveRequestQueueMaxWaitMs("zai-web", 15_000, "conn-no-maxwait-override"),
|
||||
rateLimitManager.ZAI_WEB_REQUEST_QUEUE_MAX_WAIT_MS
|
||||
);
|
||||
} finally {
|
||||
rateLimitManager.refreshConnectionRateLimits("conn-no-maxwait-override", null);
|
||||
}
|
||||
});
|
||||
|
||||
test("#6593 a maxWaitMs override of 0 is treated as no override", () => {
|
||||
rateLimitManager.refreshConnectionRateLimits("conn-zero-maxwait-override", { maxWaitMs: 0 });
|
||||
try {
|
||||
assert.equal(
|
||||
rateLimitManager.resolveRequestQueueMaxWaitMs("openai", 15_000, "conn-zero-maxwait-override"),
|
||||
15_000
|
||||
);
|
||||
} finally {
|
||||
rateLimitManager.refreshConnectionRateLimits("conn-zero-maxwait-override", null);
|
||||
}
|
||||
});
|
||||
|
||||
test("#6593 DEFAULT_REQUEST_QUEUE_MAX_DEPTH defaults to 0 (disabled) absent an env override", () => {
|
||||
assert.equal(process.env.RATE_LIMIT_MAX_QUEUE_DEPTH, undefined);
|
||||
assert.equal(resilienceSettings.DEFAULT_REQUEST_QUEUE_MAX_DEPTH, 0);
|
||||
|
||||
@@ -66,9 +66,8 @@ test("422 'unknown variant xhigh, expected one of ...' clamps reasoning_effort a
|
||||
assert.equal(capturedBodies.length, 2);
|
||||
assert.equal(capturedBodies[0].reasoning_effort, "xhigh");
|
||||
assert.equal(capturedBodies[1].reasoning_effort, "high");
|
||||
assert.equal(
|
||||
getLearnedReasoningEffort("openai-compatible-chat-eaff6869", "qwen3-coder-30b-a3b-instruct"),
|
||||
"high"
|
||||
assert.ok(
|
||||
(getLearnedReasoningEffort("openai-compatible-chat-eaff6869", "qwen3-coder-30b-a3b-instruct") as unknown as Set<string>).has("high")
|
||||
);
|
||||
assert.equal(result.response.status, 200);
|
||||
} finally {
|
||||
@@ -108,3 +107,125 @@ test("a second request for the same provider+model sends the learned value on th
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("400 please use low, high, or max clamps and retries once", async () => {
|
||||
const executor = new SimpleExecutor();
|
||||
const originalFetch = globalThis.fetch;
|
||||
const capturedBodies: Record<string, unknown>[] = [];
|
||||
const BODY_400_PLEASE_USE = JSON.stringify({
|
||||
error: { message: "This model always engages in thinking and cannot be disabled; please use low, high, or max" },
|
||||
});
|
||||
|
||||
globalThis.fetch = async (_url: string | URL | Request, init: RequestInit = {}) => {
|
||||
const body = JSON.parse(String(init.body));
|
||||
capturedBodies.push(body);
|
||||
if (capturedBodies.length === 1) {
|
||||
return new Response(BODY_400_PLEASE_USE, {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
return new Response(JSON.stringify({ ok: true }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await executor.execute({
|
||||
model: "x-preview-f-free",
|
||||
body: { reasoning_effort: "medium" },
|
||||
stream: false,
|
||||
credentials: {},
|
||||
});
|
||||
assert.equal(capturedBodies.length, 2);
|
||||
assert.equal(capturedBodies[0].reasoning_effort, "medium");
|
||||
assert.equal(capturedBodies[1].reasoning_effort, "low");
|
||||
const learned = getLearnedReasoningEffort("openai-compatible-chat-eaff6869", "x-preview-f-free") as unknown as Set<string>;
|
||||
assert.ok(learned instanceof Set);
|
||||
assert.ok(learned.has("low"));
|
||||
assert.ok(learned.has("high"));
|
||||
assert.equal(result.response.status, 200);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("400 please use low, medium with ultra retries to medium", async () => {
|
||||
const executor = new SimpleExecutor();
|
||||
const originalFetch = globalThis.fetch;
|
||||
const capturedBodies: Record<string, unknown>[] = [];
|
||||
const BODY_400_ULTRA = JSON.stringify({
|
||||
error: { message: "please use low, medium" },
|
||||
});
|
||||
|
||||
globalThis.fetch = async (_url: string | URL | Request, init: RequestInit = {}) => {
|
||||
const body = JSON.parse(String(init.body));
|
||||
capturedBodies.push(body);
|
||||
if (capturedBodies.length === 1) {
|
||||
return new Response(BODY_400_ULTRA, {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
return new Response(JSON.stringify({ ok: true }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await executor.execute({
|
||||
model: "x-preview-f-free-2",
|
||||
body: { reasoning_effort: "ultra" },
|
||||
stream: false,
|
||||
credentials: {},
|
||||
});
|
||||
assert.equal(capturedBodies.length, 2);
|
||||
assert.equal(capturedBodies[0].reasoning_effort, "ultra");
|
||||
assert.equal(capturedBodies[1].reasoning_effort, "medium");
|
||||
assert.equal(result.response.status, 200);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("no-op clamp does not retry: learned {high,max} with low request stays single-fetch", async () => {
|
||||
const executor = new SimpleExecutor();
|
||||
const originalFetch = globalThis.fetch;
|
||||
const capturedBodies: Record<string, unknown>[] = [];
|
||||
const BODY_400_HIGH_MAX = JSON.stringify({
|
||||
error: { message: "please use high, or max" },
|
||||
});
|
||||
|
||||
globalThis.fetch = async (_url: string | URL | Request, init: RequestInit = {}) => {
|
||||
const body = JSON.parse(String(init.body));
|
||||
capturedBodies.push(body);
|
||||
if (capturedBodies.length === 1) {
|
||||
return new Response(BODY_400_HIGH_MAX, {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
return new Response(JSON.stringify({ ok: true }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
// low is below the learned minimum {high,max}: downgrade-only passthrough,
|
||||
// sanitizer leaves the body unchanged -> no identical-body retry.
|
||||
const result = await executor.execute({
|
||||
model: "x-preview-f-free-3",
|
||||
body: { reasoning_effort: "low" },
|
||||
stream: false,
|
||||
credentials: {},
|
||||
});
|
||||
assert.equal(capturedBodies.length, 1);
|
||||
assert.equal(capturedBodies[0].reasoning_effort, "low");
|
||||
assert.equal(result.response.status, 400);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -89,3 +89,67 @@ test("deepseek's non-ordinal max<->xhigh translation is untouched by the learned
|
||||
// deepseek's special case returns early — xhigh -> max, never reaches the catch-all.
|
||||
assert.equal(result.reasoning_effort, "max");
|
||||
});
|
||||
|
||||
test("proactive clamp: medium→low for learned {low,high,max}", () => {
|
||||
recordLearnedReasoningEffort("opencode-zen-direct", "x-preview-f-free", ["low", "high", "max"]);
|
||||
const out = sanitizeReasoningEffortForProvider(
|
||||
{ reasoning_effort: "medium", model: "x-preview-f-free" },
|
||||
"opencode-zen-direct",
|
||||
"x-preview-f-free"
|
||||
) as { reasoning_effort: string };
|
||||
assert.equal(out.reasoning_effort, "low");
|
||||
});
|
||||
test("proactive clamp: xhigh→high for learned {low,high,max}", () => {
|
||||
recordLearnedReasoningEffort("opencode-zen-direct", "x-preview-f-free-2", ["low", "high", "max"]);
|
||||
const out = sanitizeReasoningEffortForProvider(
|
||||
{ reasoning_effort: "xhigh", model: "x-preview-f-free-2" },
|
||||
"opencode-zen-direct",
|
||||
"x-preview-f-free-2"
|
||||
) as { reasoning_effort: string };
|
||||
assert.equal(out.reasoning_effort, "high");
|
||||
});
|
||||
test("proactive clamp: ultra→max for learned {low,high,max}", () => {
|
||||
recordLearnedReasoningEffort("opencode-zen-direct", "x-preview-f-free-3", ["low", "high", "max"]);
|
||||
const out = sanitizeReasoningEffortForProvider(
|
||||
{ reasoning_effort: "ultra", model: "x-preview-f-free-3" },
|
||||
"opencode-zen-direct",
|
||||
"x-preview-f-free-3"
|
||||
) as { reasoning_effort: string };
|
||||
assert.equal(out.reasoning_effort, "max");
|
||||
});
|
||||
test("proactive clamp: ultra→medium for learned {low,medium}", () => {
|
||||
recordLearnedReasoningEffort("acme", "m", ["low", "medium"]);
|
||||
const out = sanitizeReasoningEffortForProvider(
|
||||
{ reasoning_effort: "ultra", model: "m" },
|
||||
"acme",
|
||||
"m"
|
||||
) as { reasoning_effort: string };
|
||||
assert.equal(out.reasoning_effort, "medium");
|
||||
});
|
||||
test("proactive clamp: high→medium for learned {low,medium}", () => {
|
||||
recordLearnedReasoningEffort("acme", "m2", ["low", "medium"]);
|
||||
const out = sanitizeReasoningEffortForProvider(
|
||||
{ reasoning_effort: "high", model: "m2" },
|
||||
"acme",
|
||||
"m2"
|
||||
) as { reasoning_effort: string };
|
||||
assert.equal(out.reasoning_effort, "medium");
|
||||
});
|
||||
test("no upgrade: low stays low for learned {high,max}", () => {
|
||||
recordLearnedReasoningEffort("acme", "m3", ["high", "max"]);
|
||||
const out = sanitizeReasoningEffortForProvider(
|
||||
{ reasoning_effort: "low", model: "m3" },
|
||||
"acme",
|
||||
"m3"
|
||||
) as { reasoning_effort: string };
|
||||
assert.equal(out.reasoning_effort, "low");
|
||||
});
|
||||
test("custom model ultra→medium for learned {low,medium}", () => {
|
||||
recordLearnedReasoningEffort("openai-compatible-chat-eaff6869", "qwen3-coder-30b-a3b-instruct-2", ["low", "medium"]);
|
||||
const out = sanitizeReasoningEffortForProvider(
|
||||
{ reasoning_effort: "ultra", model: "qwen3-coder-30b-a3b-instruct-2" },
|
||||
"openai-compatible-chat-eaff6869",
|
||||
"qwen3-coder-30b-a3b-instruct-2"
|
||||
) as { reasoning_effort: string };
|
||||
assert.equal(out.reasoning_effort, "medium");
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
parseQuotaData,
|
||||
hasFixedQuotaOrder,
|
||||
} from "@/app/(dashboard)/dashboard/usage/components/ProviderLimits/quotaParsing";
|
||||
import { resolveQuotaDisplayOrder } from "@/app/(dashboard)/dashboard/usage/components/ProviderLimits/parts/QuotaCardExpanded";
|
||||
|
||||
const quotaName = (quota: { name: string }) => quota.name;
|
||||
|
||||
@@ -62,3 +63,128 @@ test("#7764: providers WITHOUT a fixed order still sort worst-status-first (no r
|
||||
const rendered = topQuotas(quotas, 3, "some-other-provider").map(quotaName);
|
||||
assert.deepEqual(rendered, ["beta", "gamma", "alpha"]);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// #7764 residual: the original fix only whitelisted codex / GLM family / Kimi
|
||||
// Coding in `hasFixedQuotaOrder`. Every OTHER provider that reports the same
|
||||
// session + weekly rolling windows still gets re-sorted by remaining %, so two
|
||||
// accounts of the SAME provider render the two bars in opposite positions
|
||||
// depending on which window happens to be more depleted — the exact symptom in
|
||||
// the report ("the indicators are not located in the same position per card").
|
||||
//
|
||||
// Quota names below are the real upstream keys, not simplified ones:
|
||||
// claude → open-sse/services/usage/claude.ts:107,112 "session (5h)" / "weekly (7d)"
|
||||
// minimax → open-sse/services/usage/minimax.ts:312,325 "session (5h)" / "weekly (7d)"
|
||||
// zai → routed to getGlmUsage (open-sse/services/usage.ts:191-194)
|
||||
// so it emits "5 Hours Quota" / "Weekly Quota" (glm.ts:33-34)
|
||||
// command-code → open-sse/services/usage/command-code.ts:193,196 "five_hour" / "weekly"
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Two refreshes of the same account family: in A the weekly window is the
|
||||
* depleted one, in B it is the session window. A remaining-% sort flips the
|
||||
* row order between the two; a canonical window order does not. */
|
||||
function windowPair(sessionKey: string, weeklyKey: string) {
|
||||
return {
|
||||
depletedWeekly: {
|
||||
quotas: {
|
||||
[sessionKey]: { used: 9, total: 100, remainingPercentage: 91, resetAt: null },
|
||||
[weeklyKey]: { used: 97, total: 100, remainingPercentage: 3, resetAt: null },
|
||||
},
|
||||
},
|
||||
depletedSession: {
|
||||
quotas: {
|
||||
[sessionKey]: { used: 99, total: 100, remainingPercentage: 1, resetAt: null },
|
||||
[weeklyKey]: { used: 43, total: 100, remainingPercentage: 57, resetAt: null },
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const WINDOW_PROVIDERS: Array<{ provider: string; session: string; weekly: string }> = [
|
||||
{ provider: "claude", session: "session (5h)", weekly: "weekly (7d)" },
|
||||
{ provider: "minimax", session: "session (5h)", weekly: "weekly (7d)" },
|
||||
{ provider: "minimax-cn", session: "session (5h)", weekly: "weekly (7d)" },
|
||||
{ provider: "zai", session: "5 Hours Quota", weekly: "Weekly Quota" },
|
||||
{ provider: "command-code", session: "five_hour", weekly: "weekly" },
|
||||
];
|
||||
|
||||
for (const { provider, session, weekly } of WINDOW_PROVIDERS) {
|
||||
test(`#7764 residual: ${provider} keeps session before weekly in the collapsed card across refreshes`, () => {
|
||||
const { depletedWeekly, depletedSession } = windowPair(session, weekly);
|
||||
const parsedA = parseQuotaData(provider, depletedWeekly);
|
||||
const parsedB = parseQuotaData(provider, depletedSession);
|
||||
|
||||
// parseQuotaData already yields the canonical upstream order for both.
|
||||
assert.deepEqual(parsedA.map(quotaName), [session, weekly]);
|
||||
assert.deepEqual(parsedB.map(quotaName), [session, weekly]);
|
||||
|
||||
assert.deepEqual(
|
||||
topQuotas(parsedA, 3, provider).map(quotaName),
|
||||
[session, weekly],
|
||||
`${provider}: collapsed card must not reorder rolling windows by remaining %`
|
||||
);
|
||||
assert.deepEqual(
|
||||
topQuotas(parsedB, 3, provider).map(quotaName),
|
||||
[session, weekly],
|
||||
`${provider}: window order must be identical on the sibling account`
|
||||
);
|
||||
});
|
||||
|
||||
test(`#7764 residual: ${provider} expanded card window order matches the collapsed card`, () => {
|
||||
const { depletedWeekly, depletedSession } = windowPair(session, weekly);
|
||||
const parsedA = parseQuotaData(provider, depletedWeekly);
|
||||
const parsedB = parseQuotaData(provider, depletedSession);
|
||||
|
||||
assert.deepEqual(resolveQuotaDisplayOrder(provider, parsedA).map(quotaName), [session, weekly]);
|
||||
assert.deepEqual(resolveQuotaDisplayOrder(provider, parsedB).map(quotaName), [session, weekly]);
|
||||
});
|
||||
}
|
||||
|
||||
test("#7764 residual: a card whose quotas are NOT rolling windows still sorts worst-first", () => {
|
||||
// Antigravity-style per-model buckets: no canonical chronological order
|
||||
// exists, so the worst-status-first sort remains the useful one.
|
||||
const parsed = parseQuotaData("antigravity", {
|
||||
quotas: {
|
||||
"gemini-3-pro": { used: 10, total: 100, remainingPercentage: 90 },
|
||||
"gemini-3-flash": { used: 95, total: 100, remainingPercentage: 5 },
|
||||
},
|
||||
});
|
||||
assert.deepEqual(topQuotas(parsed, 3, "antigravity").map(quotaName), [
|
||||
"gemini-3-flash",
|
||||
"gemini-3-pro",
|
||||
]);
|
||||
});
|
||||
|
||||
test("#7764 residual: a single rolling window plus credits is left to the remaining-% sort", () => {
|
||||
// Only ONE window → no two windows to keep in a stable relative order, so
|
||||
// nothing is claimed and the pre-existing behaviour is preserved.
|
||||
const quotas = [
|
||||
{ name: "credits", used: 0, total: 0, remainingPercentage: 90, isCredits: true },
|
||||
{ name: "session (5h)", used: 95, total: 100, remainingPercentage: 5 },
|
||||
];
|
||||
assert.deepEqual(topQuotas(quotas, 3, "some-credit-provider").map(quotaName), [
|
||||
"session (5h)",
|
||||
"credits",
|
||||
]);
|
||||
});
|
||||
|
||||
test("#7764 residual: Claude per-model weekly windows keep upstream order and credits sink last", () => {
|
||||
// Anthropic reports extra `weekly <model> (7d)` buckets plus an extra_usage
|
||||
// credits row. The window sort must be STABLE: same-rank siblings keep the
|
||||
// order parseQuotaData produced, and the credits row is not promoted.
|
||||
const parsed = parseQuotaData("claude", {
|
||||
quotas: {
|
||||
"session (5h)": { used: 9, total: 100, remainingPercentage: 91 },
|
||||
"weekly (7d)": { used: 97, total: 100, remainingPercentage: 3 },
|
||||
"weekly designer (7d)": { used: 50, total: 100, remainingPercentage: 50 },
|
||||
},
|
||||
extraUsage: { is_enabled: true, monthly_limit: 100, used_credits: 10, utilization: 10 },
|
||||
});
|
||||
|
||||
assert.deepEqual(topQuotas(parsed, 4, "claude").map(quotaName), [
|
||||
"session (5h)",
|
||||
"weekly (7d)",
|
||||
"weekly designer (7d)",
|
||||
"extra_usage",
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* temp-directory filesystem.
|
||||
*/
|
||||
|
||||
import { describe, it, beforeEach, after } from "node:test";
|
||||
import { describe, it, beforeEach, after, mock } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
@@ -68,8 +68,11 @@ describe("resolveSpawnArgs (#6877 — real filesystem)", () => {
|
||||
});
|
||||
|
||||
it("uses the .exe command name on Windows", async () => {
|
||||
const originalPlatformDescriptor = Object.getOwnPropertyDescriptor(process, "platform");
|
||||
Object.defineProperty(process, "platform", { value: "win32", configurable: true });
|
||||
// resolveSpawnArgs reads os.platform() at call time (#11236 — a
|
||||
// process.platform literal is constant-folded away by the Linux build of
|
||||
// the published artifact), so the Windows host is simulated through the
|
||||
// same runtime os.platform() seam binaryManager.test.ts uses for #10244.
|
||||
const platformMock = mock.method(os, "platform", () => "win32");
|
||||
|
||||
try {
|
||||
const { resolveSpawnArgs } =
|
||||
@@ -78,9 +81,7 @@ describe("resolveSpawnArgs (#6877 — real filesystem)", () => {
|
||||
|
||||
assert.equal(result.command, path.join(dataDir, "bin", "cliproxyapi.exe"));
|
||||
} finally {
|
||||
if (originalPlatformDescriptor) {
|
||||
Object.defineProperty(process, "platform", originalPlatformDescriptor);
|
||||
}
|
||||
platformMock.mock.restore();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
parseLsofPid,
|
||||
parseNetstatPid,
|
||||
parseSsPid,
|
||||
parseWindowsNetstatPid,
|
||||
resolvePortPid,
|
||||
} from "@/lib/services/portProbe";
|
||||
|
||||
@@ -65,8 +66,7 @@ test("parseNetstatPid matches on the local address, not the foreign one", () =>
|
||||
});
|
||||
|
||||
test("parseNetstatPid reads macOS process:pid output", () => {
|
||||
const stdout =
|
||||
"tcp4 0 0 127.0.0.1.20128 *.* LISTEN 0 0 131072 131072 node:596922 00100\n";
|
||||
const stdout = "tcp4 0 0 127.0.0.1.20128 *.* LISTEN 0 0 131072 131072 node:596922 00100\n";
|
||||
assert.equal(parseNetstatPid(stdout, 20128), 596922);
|
||||
});
|
||||
|
||||
@@ -77,6 +77,42 @@ test("parseNetstatPid ignores non-listening rows and unknown ports", () => {
|
||||
assert.equal(parseNetstatPid("", 20128), null);
|
||||
});
|
||||
|
||||
/**
|
||||
* Realistic `netstat -ano` sample from Windows 11 (#11236 bug 6): the pid is
|
||||
* the last whitespace-separated column and only exists on rows whose state is
|
||||
* LISTENING. This is the only pid probe available on a stock Windows host —
|
||||
* neither lsof nor ss nor net-tools `netstat -tlnp` exist there, so a Windows
|
||||
* service adopted by the supervisor reported `pid: null` while healthy.
|
||||
*/
|
||||
const WINDOWS_NETSTAT_ANO = [
|
||||
"Active Connections",
|
||||
"",
|
||||
" Proto Local Address Foreign Address State PID",
|
||||
" TCP 0.0.0.0:135 0.0.0.0:0 LISTENING 1244",
|
||||
" TCP 0.0.0.0:20128 0.0.0.0:0 LISTENING 12345",
|
||||
" TCP 127.0.0.1:8317 0.0.0.0:0 LISTENING 5678",
|
||||
" TCP 192.168.1.10:52413 140.82.121.4:443 ESTABLISHED 9012",
|
||||
" TCP [::]:20128 [::]:0 LISTENING 12345",
|
||||
" UDP 0.0.0.0:5353 *:* 3460",
|
||||
"",
|
||||
].join("\r\n");
|
||||
|
||||
test("parseWindowsNetstatPid reads the pid from a LISTENING row (#11236)", () => {
|
||||
assert.equal(parseWindowsNetstatPid(WINDOWS_NETSTAT_ANO, 20128), 12345);
|
||||
assert.equal(parseWindowsNetstatPid(WINDOWS_NETSTAT_ANO, 8317), 5678);
|
||||
});
|
||||
|
||||
test("parseWindowsNetstatPid matches the local address, not the foreign one", () => {
|
||||
// 443 appears only as a foreign address on an ESTABLISHED row.
|
||||
assert.equal(parseWindowsNetstatPid(WINDOWS_NETSTAT_ANO, 443), null);
|
||||
// 5353 appears only on a UDP row, which has no LISTENING state.
|
||||
assert.equal(parseWindowsNetstatPid(WINDOWS_NETSTAT_ANO, 5353), null);
|
||||
// A port that shares a suffix with a listening one must not match: 0128 vs
|
||||
// 20128 — the `:` anchor on the local address prevents the partial hit.
|
||||
assert.equal(parseWindowsNetstatPid(WINDOWS_NETSTAT_ANO, 128), null);
|
||||
assert.equal(parseWindowsNetstatPid("", 20128), null);
|
||||
});
|
||||
|
||||
test("resolvePortPid finds the pid holding a port", async () => {
|
||||
const server = createServer();
|
||||
await new Promise<void>((resolve) => server.listen(29994, "127.0.0.1", resolve));
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* effort_tiers loop — learned set overrides synced metadata in catalog
|
||||
* capabilities (design 2026-08-23, decisions: appris > sync, in-memory).
|
||||
* Records go through the REAL record path (executor-style connection keys)
|
||||
* then read back through the catalog builders — proves the key-space bridge,
|
||||
* unlike a unit injection of the same string on both sides.
|
||||
*/
|
||||
import { test, after, beforeEach } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
recordLearnedReasoningEffort,
|
||||
__test_resetLearnedReasoningEffortCaps,
|
||||
} from "../../open-sse/services/learnedReasoningEffortCaps.ts";
|
||||
import {
|
||||
buildSyncedCapabilities,
|
||||
mergeSyncedCapabilities,
|
||||
} from "../../src/app/api/v1/models/syncedCapabilities.ts";
|
||||
|
||||
beforeEach(() => __test_resetLearnedReasoningEffortCaps());
|
||||
after(() => __test_resetLearnedReasoningEffortCaps());
|
||||
|
||||
const SYNC_TIERS = ["none", "low", "medium", "high", "xhigh"];
|
||||
|
||||
test("learned set replaces synced effort_tiers", () => {
|
||||
recordLearnedReasoningEffort("openai-compatible-chat-eaff6869", "x-preview-f-free", [
|
||||
"low",
|
||||
"high",
|
||||
"max",
|
||||
]);
|
||||
const caps = buildSyncedCapabilities(
|
||||
{ id: "x-preview-f-free", supportedThinkingEfforts: SYNC_TIERS },
|
||||
"huggingface"
|
||||
);
|
||||
assert.deepEqual(caps?.effort_tiers, ["low", "high", "max"]);
|
||||
});
|
||||
|
||||
test("nothing learned keeps synced metadata untouched", () => {
|
||||
const caps = buildSyncedCapabilities(
|
||||
{ id: "some-synced-model", supportedThinkingEfforts: SYNC_TIERS },
|
||||
"huggingface"
|
||||
);
|
||||
assert.deepEqual(caps?.effort_tiers, SYNC_TIERS);
|
||||
});
|
||||
|
||||
test("neither learned nor synced yields undefined", () => {
|
||||
const caps = buildSyncedCapabilities({ id: "plain-model" }, "huggingface");
|
||||
assert.equal(caps, undefined);
|
||||
});
|
||||
|
||||
test("merge path keeps vision AND applies the learned override", () => {
|
||||
recordLearnedReasoningEffort("conn-a", "vision-model", ["low", "max"]);
|
||||
const merged = mergeSyncedCapabilities(
|
||||
{ tool_calling: true },
|
||||
{ id: "vision-model", supportsVision: true, supportedThinkingEfforts: SYNC_TIERS },
|
||||
"huggingface"
|
||||
);
|
||||
assert.equal(merged?.vision, true);
|
||||
assert.equal(merged?.tool_calling, true);
|
||||
assert.deepEqual(merged?.effort_tiers, ["low", "max"]);
|
||||
});
|
||||
|
||||
// Exclusion gate (#7694): codex/glm/kimi already own a conflicting
|
||||
// `-{effort}` suffix mechanism — the blind opencode-plugin mapping must never
|
||||
// see effort_tiers for them, learned or synced, or it double-handles the suffix.
|
||||
for (const ownedBy of ["codex", "glm", "glm-cn", "glmt", "kimi", "kimi-coding-apikey"]) {
|
||||
test(`build: excluded provider "${ownedBy}" never gets effort_tiers (synced)`, () => {
|
||||
const caps = buildSyncedCapabilities(
|
||||
{ id: "excluded-model", supportedThinkingEfforts: SYNC_TIERS },
|
||||
ownedBy
|
||||
);
|
||||
assert.equal(caps?.effort_tiers, undefined);
|
||||
});
|
||||
|
||||
test(`build: excluded provider "${ownedBy}" never gets effort_tiers (learned)`, () => {
|
||||
recordLearnedReasoningEffort(`conn-${ownedBy}`, "excluded-model", ["low", "max"]);
|
||||
const caps = buildSyncedCapabilities(
|
||||
{ id: "excluded-model", supportedThinkingEfforts: SYNC_TIERS },
|
||||
ownedBy
|
||||
);
|
||||
assert.equal(caps?.effort_tiers, undefined);
|
||||
});
|
||||
}
|
||||
|
||||
test("excluded provider still gets vision through buildSyncedCapabilities", () => {
|
||||
const caps = buildSyncedCapabilities({ id: "codex-vision-model", supportsVision: true }, "codex");
|
||||
assert.deepEqual(caps, { vision: true });
|
||||
});
|
||||
|
||||
test("merge path also excludes codex/glm/kimi from effort_tiers", () => {
|
||||
recordLearnedReasoningEffort("conn-glm", "glm-model", ["low", "max"]);
|
||||
const merged = mergeSyncedCapabilities(
|
||||
{ tool_calling: true },
|
||||
{ id: "glm-model", supportsVision: true, supportedThinkingEfforts: SYNC_TIERS },
|
||||
"glm"
|
||||
);
|
||||
assert.equal(merged?.vision, true);
|
||||
assert.equal(merged?.effort_tiers, undefined);
|
||||
});
|
||||
80
tests/unit/synced-effort-suffix-learned-validation.test.ts
Normal file
80
tests/unit/synced-effort-suffix-learned-validation.test.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* C1 — the `-<tier>` suffix resolver validates against the EFFECTIVE tier set
|
||||
* (learned ?? sync), not raw synced metadata. Without this, the catalog
|
||||
* advertises <alias>/<model>-max (learned set) but dispatch refuses to strip
|
||||
* `-max` because sync metadata lacks the tier — dead-on-arrival variant.
|
||||
* Harness mirrors deepseek-thinking-efforts.test.ts (custom provider +
|
||||
* persistDiscoveredModels + async getModelInfo).
|
||||
*/
|
||||
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-c1-effort-dispatch-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "c1-test-secret";
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const providersDb = await import("../../src/lib/db/providers.ts");
|
||||
const modelDiscovery = await import("../../src/lib/providerModels/modelDiscovery.ts");
|
||||
const { getModelInfo } = await import("../../src/sse/services/model.ts");
|
||||
const { recordLearnedReasoningEffort, __test_resetLearnedReasoningEffortCaps } =
|
||||
await import("@omniroute/open-sse/services/learnedReasoningEffortCaps.ts");
|
||||
|
||||
async function resetStorage() {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
const PROVIDER = "c1prov";
|
||||
const MODEL_ID = "c1-model";
|
||||
|
||||
async function seed() {
|
||||
const connection = await providersDb.createProviderConnection({
|
||||
provider: PROVIDER,
|
||||
authType: "apikey",
|
||||
name: "c1-runtime-efforts",
|
||||
apiKey: `${PROVIDER}-key`,
|
||||
isActive: true,
|
||||
testStatus: "active",
|
||||
});
|
||||
// Sync tiers deliberately EXCLUDE max — only the learned set will vouch for it.
|
||||
await modelDiscovery.persistDiscoveredModels(PROVIDER, connection.id, [
|
||||
{ id: MODEL_ID, reasoning: { supported_efforts: ["none", "low", "medium", "high"] } },
|
||||
]);
|
||||
}
|
||||
|
||||
test.beforeEach(async () => {
|
||||
__test_resetLearnedReasoningEffortCaps();
|
||||
await resetStorage();
|
||||
await seed();
|
||||
});
|
||||
|
||||
test.after(async () => {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("-max resolves once the learned set advertises it (sync metadata does not)", async () => {
|
||||
// Real record path, executor-style CONNECTION key — NOT the provider alias.
|
||||
recordLearnedReasoningEffort("openai-compatible-chat-eaff6869", MODEL_ID, ["low", "high", "max"]);
|
||||
const info = await getModelInfo(`${PROVIDER}/${MODEL_ID}-max`);
|
||||
assert.equal(info.provider, PROVIDER);
|
||||
assert.equal(info.model, MODEL_ID);
|
||||
assert.equal(info.resolvedThinkingEffort, "max");
|
||||
});
|
||||
|
||||
test("-medium still resolves via sync tiers even before anything is learned", async () => {
|
||||
const info = await getModelInfo(`${PROVIDER}/${MODEL_ID}-medium`);
|
||||
assert.equal(info.model, MODEL_ID);
|
||||
assert.equal(info.resolvedThinkingEffort, "medium");
|
||||
});
|
||||
|
||||
test("a tier neither learned nor synced is left untouched (literal id)", async () => {
|
||||
recordLearnedReasoningEffort("conn-a", MODEL_ID, ["low"]);
|
||||
const info = await getModelInfo(`${PROVIDER}/${MODEL_ID}-ultra`);
|
||||
assert.equal(info.resolvedThinkingEffort, undefined);
|
||||
});
|
||||
168
tests/unit/windows-platform-fold-guard-11236.test.ts
Normal file
168
tests/unit/windows-platform-fold-guard-11236.test.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
/**
|
||||
* Structural regression guard for #11236 (Windows cliproxy residuals, bugs 2+3).
|
||||
*
|
||||
* Why this guard exists: the published npm artifact is bundled on Linux, and
|
||||
* the bundler constant-folds every literal `process.platform` read to the
|
||||
* BUILD machine's platform ("linux"), pruning the win32 branch from the
|
||||
* shipped artifact. Precedent: b43a212680 (#10244/#10293), which converted
|
||||
* detectPlatform/detectArch to runtime `os.platform()`/`os.arch()` reads for
|
||||
* exactly this reason. #10371 later fixed the Windows `.exe` binary name in
|
||||
* the source but left literal `process.platform` reads behind in the same
|
||||
* runtime paths, so the shipped artifact still:
|
||||
* - named the managed binary `cliproxyapi` (no `.exe`) at install time
|
||||
* (binaryManager.managedBinaryName), and
|
||||
* - spawned that extension-less path at start time
|
||||
* (installers/cliproxy.resolveSpawnArgs) -> ENOENT on Windows even with a
|
||||
* valid `.exe` in place (issue #11236 bugs 2 and 3).
|
||||
*
|
||||
* The runtime-safe pattern is a call-time `os.platform()` read. This guard
|
||||
* fails if `process.platform` reappears outside a comment in any file whose
|
||||
* platform branch feeds the published artifact's runtime behavior (binary
|
||||
* name, spawn path, per-OS probe selection).
|
||||
*/
|
||||
|
||||
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";
|
||||
|
||||
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
||||
|
||||
const GUARDED_FILES = [
|
||||
"src/lib/versionManager/binaryManager.ts",
|
||||
"src/lib/versionManager/processManager.ts",
|
||||
"src/lib/services/installers/cliproxy.ts",
|
||||
"src/lib/services/portProbe.ts",
|
||||
];
|
||||
|
||||
interface Offender {
|
||||
line: number;
|
||||
text: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the source with every `//` and `/* ... *\/` comment blanked out
|
||||
* (replaced by spaces, newlines preserved so line numbers are stable). String
|
||||
* literals are kept verbatim — a `process.platform` inside one is still
|
||||
* flagged, which is acceptable: none of the guarded files carry the pattern
|
||||
* in a string, and a false positive there is safer than a false negative in
|
||||
* code.
|
||||
*/
|
||||
function stripComments(source: string): string {
|
||||
let out = "";
|
||||
let i = 0;
|
||||
let inBlock = false;
|
||||
let inLine = false;
|
||||
let inString: string | null = null;
|
||||
while (i < source.length) {
|
||||
const ch = source[i];
|
||||
const next = source[i + 1];
|
||||
if (inLine) {
|
||||
if (ch === "\n") {
|
||||
inLine = false;
|
||||
out += ch;
|
||||
} else {
|
||||
out += " ";
|
||||
}
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (inBlock) {
|
||||
if (ch === "*" && next === "/") {
|
||||
inBlock = false;
|
||||
out += " ";
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
out += ch === "\n" ? "\n" : " ";
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (inString) {
|
||||
out += ch;
|
||||
if (ch === "\\") {
|
||||
out += next ?? "";
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if (ch === inString) inString = null;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (ch === "/" && next === "/") {
|
||||
inLine = true;
|
||||
out += " ";
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if (ch === "/" && next === "*") {
|
||||
inBlock = true;
|
||||
out += " ";
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if (ch === '"' || ch === "'" || ch === "`") inString = ch;
|
||||
out += ch;
|
||||
i++;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Every remaining `process.platform` occurrence after comment stripping is an
|
||||
* offender — the fold-explanation comments reference the pattern by name and
|
||||
* must remain free to do so.
|
||||
*/
|
||||
function findFoldableReads(source: string): Offender[] {
|
||||
const stripped = stripComments(source);
|
||||
const offenders: Offender[] = [];
|
||||
stripped.split("\n").forEach((line, index) => {
|
||||
if (line.includes("process.platform")) {
|
||||
offenders.push({ line: index + 1, text: source.split("\n")[index].trim() });
|
||||
}
|
||||
});
|
||||
return offenders;
|
||||
}
|
||||
|
||||
for (const relPath of GUARDED_FILES) {
|
||||
test(`${relPath} has no build-foldable process.platform reads (#11236)`, () => {
|
||||
const source = fs.readFileSync(path.join(REPO_ROOT, relPath), "utf8");
|
||||
const offenders = findFoldableReads(source);
|
||||
assert.deepEqual(
|
||||
offenders,
|
||||
[],
|
||||
`${relPath} must read os.platform() at call time instead of the ` +
|
||||
`build-foldable process.platform literal (Turbopack folds it to the ` +
|
||||
`Linux build machine — b43a212680 / #10244 / #10371). Offenders: ` +
|
||||
offenders.map((o) => `L${o.line}: ${o.text}`).join("; ")
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Guard-the-guard (mutation check on synthetic input, so the real sources
|
||||
// never need to be touched): a code occurrence MUST be caught, comment-only
|
||||
// occurrences MUST be let through.
|
||||
test("findFoldableReads catches a code occurrence (mutation self-check)", () => {
|
||||
const snippet = [
|
||||
'const name = process.platform === "win32" ? "a.exe" : "a";',
|
||||
"// process.platform in a line comment is allowed",
|
||||
"/**",
|
||||
" * process.platform in a block comment is allowed",
|
||||
" */",
|
||||
"/* process.platform single-line block is allowed */",
|
||||
"const ok = os.platform();",
|
||||
].join("\n");
|
||||
const offenders = findFoldableReads(snippet);
|
||||
assert.equal(offenders.length, 1);
|
||||
assert.equal(offenders[0].line, 1);
|
||||
});
|
||||
|
||||
test("findFoldableReads reports nothing when only comments mention the pattern", () => {
|
||||
const snippet = [
|
||||
"// process.platform",
|
||||
"/* process.platform */",
|
||||
"const p = os.platform();",
|
||||
].join("\n");
|
||||
assert.deepEqual(findFoldableReads(snippet), []);
|
||||
});
|
||||
Reference in New Issue
Block a user