mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
Release v3.8.29 (#4126)
OmniRoute v3.8.29 — 115 commits since v3.8.28. Full CHANGELOG + 41 i18n mirrors. All content quality gates green (build, unit 8/8, vitest 188/188, PR test policy, quality gates extended, docs sync, quality ratchet). Remaining red CI checks are pre-existing release flakes (coverage-shard/integration/node-compat teardown), a new transitive undici advisory in electron devDeps, and a workflow-level CodeQL fail (0 open alerts). VPS-validated by the operator.
This commit is contained in:
committed by
GitHub
parent
dd5a3db55e
commit
3c9883bb73
31
tests/fixtures/translation/openai-to-gemini.json
vendored
31
tests/fixtures/translation/openai-to-gemini.json
vendored
@@ -5,7 +5,12 @@
|
||||
"targetFormat": "gemini",
|
||||
"input": {
|
||||
"model": "gemini-2.5-flash",
|
||||
"messages": [{ "role": "user", "content": "Hello Gemini!" }]
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Hello Gemini!"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -15,10 +20,30 @@
|
||||
"input": {
|
||||
"model": "gemini-2.5-pro",
|
||||
"messages": [
|
||||
{ "role": "system", "content": "You are a helpful assistant." },
|
||||
{ "role": "user", "content": "Tell me a joke." }
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a helpful assistant."
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Tell me a joke."
|
||||
}
|
||||
],
|
||||
"temperature": 0.7
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "openai-to-gemini/modern-gemini-default-thinking",
|
||||
"sourceFormat": "openai",
|
||||
"targetFormat": "gemini",
|
||||
"input": {
|
||||
"model": "gemini-2.5-flash",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Hello Gemini!"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -99,14 +99,29 @@ test("round-robin combo cycles through three providers", async () => {
|
||||
return buildGeminiResponse("Gemini round-robin");
|
||||
};
|
||||
|
||||
const first = await handleChat(buildRequest({ body: buildOpenAIChatBody("router-rr") }));
|
||||
const second = await handleChat(buildRequest({ body: buildOpenAIChatBody("router-rr") }));
|
||||
const third = await handleChat(buildRequest({ body: buildOpenAIChatBody("router-rr") }));
|
||||
// `stickyRoundRobinLimit` defaults to 3 (src/lib/db/settings.ts), so a combo using the
|
||||
// round-robin strategy serves 3 consecutive requests per target before advancing to the
|
||||
// next one — batched rotation, not one-request-per-target. Nine requests therefore cover
|
||||
// a full cycle: openai x3 -> claude x3 -> gemini x3.
|
||||
const responses = [];
|
||||
for (let i = 0; i < 9; i++) {
|
||||
responses.push(await handleChat(buildRequest({ body: buildOpenAIChatBody("router-rr") })));
|
||||
}
|
||||
|
||||
assert.equal(first.status, 200);
|
||||
assert.equal(second.status, 200);
|
||||
assert.equal(third.status, 200);
|
||||
assert.deepEqual(seenProviders, ["openai", "claude", "gemini"]);
|
||||
for (const response of responses) {
|
||||
assert.equal(response.status, 200);
|
||||
}
|
||||
assert.deepEqual(seenProviders, [
|
||||
"openai",
|
||||
"openai",
|
||||
"openai",
|
||||
"claude",
|
||||
"claude",
|
||||
"claude",
|
||||
"gemini",
|
||||
"gemini",
|
||||
"gemini",
|
||||
]);
|
||||
});
|
||||
|
||||
test("priority combo sticks to the primary model while healthy", async () => {
|
||||
@@ -382,19 +397,23 @@ test("strategy updates take effect for later requests on the same combo name", a
|
||||
strategy: "round-robin",
|
||||
config: { maxRetries: 0, retryDelayMs: 0 },
|
||||
});
|
||||
const second = await handleChat(
|
||||
buildRequest({
|
||||
body: buildOpenAIChatBody("router-dynamic", "Route dynamic second"),
|
||||
})
|
||||
);
|
||||
const third = await handleChat(
|
||||
buildRequest({
|
||||
body: buildOpenAIChatBody("router-dynamic", "Route dynamic third"),
|
||||
})
|
||||
);
|
||||
// After the strategy flips to round-robin the new behavior must take effect. With the
|
||||
// default stickyRoundRobinLimit of 3 the round-robin batch serves openai three times
|
||||
// before advancing, so the 4th post-update request rotates to claude — a provider the
|
||||
// previous "priority" strategy would never have reached. That rotation is the proof the
|
||||
// updated strategy is live.
|
||||
const postUpdate = [];
|
||||
for (let i = 0; i < 4; i++) {
|
||||
postUpdate.push(
|
||||
await handleChat(
|
||||
buildRequest({ body: buildOpenAIChatBody("router-dynamic", `Route dynamic ${i}`) })
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
assert.equal(initial.status, 200);
|
||||
assert.equal(second.status, 200);
|
||||
assert.equal(third.status, 200);
|
||||
assert.deepEqual(seenProviders, ["openai", "openai", "claude"]);
|
||||
for (const response of postUpdate) {
|
||||
assert.equal(response.status, 200);
|
||||
}
|
||||
assert.deepEqual(seenProviders, ["openai", "openai", "openai", "openai", "claude"]);
|
||||
});
|
||||
|
||||
115
tests/integration/stream-recovery-wiring.test.ts
Normal file
115
tests/integration/stream-recovery-wiring.test.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { test, before, after, beforeEach } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { createChatPipelineHarness } from "./_chatPipelineHarness.ts";
|
||||
|
||||
let h: Awaited<ReturnType<typeof createChatPipelineHarness>>;
|
||||
|
||||
before(async () => {
|
||||
h = await createChatPipelineHarness("stream-recovery");
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await h.cleanup();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await h.resetStorage();
|
||||
delete process.env.STREAM_RECOVERY_ENABLED;
|
||||
});
|
||||
|
||||
// A 200 SSE stream that ends WITHOUT a terminal `[DONE]` marker = silent truncation.
|
||||
function truncatedOpenAIStream(): Response {
|
||||
const chunk = JSON.stringify({
|
||||
id: "chatcmpl_trunc",
|
||||
object: "chat.completion.chunk",
|
||||
choices: [{ index: 0, delta: { role: "assistant", content: "PARTIAL" } }],
|
||||
});
|
||||
return new Response(`data: ${chunk}\n\n`, {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "text/event-stream" },
|
||||
});
|
||||
}
|
||||
|
||||
function completeOpenAIStream(): Response {
|
||||
const chunk = JSON.stringify({
|
||||
id: "chatcmpl_full",
|
||||
object: "chat.completion.chunk",
|
||||
choices: [{ index: 0, delta: { role: "assistant", content: "RECOVERED" } }],
|
||||
});
|
||||
return new Response([`data: ${chunk}`, "", "data: [DONE]", ""].join("\n"), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "text/event-stream" },
|
||||
});
|
||||
}
|
||||
|
||||
async function readSSE(response: Response): Promise<string> {
|
||||
const reader = (response.body as ReadableStream<Uint8Array>).getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let out = "";
|
||||
for (;;) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
if (value) out += decoder.decode(value, { stream: true });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
test("stream recovery ON: a truncated opening stream is retried transparently", async () => {
|
||||
await h.seedConnection("openai", { apiKey: "sk-openai-primary" });
|
||||
const apiKey = await h.seedApiKey();
|
||||
await h.settingsDb.updateSettings({
|
||||
resilienceSettings: { streamRecovery: { enabled: true } },
|
||||
});
|
||||
|
||||
let calls = 0;
|
||||
globalThis.fetch = (async () => {
|
||||
calls++;
|
||||
return calls === 1 ? truncatedOpenAIStream() : completeOpenAIStream();
|
||||
}) as typeof fetch;
|
||||
|
||||
const response = await h.handleChat(
|
||||
h.buildRequest({
|
||||
authKey: apiKey.key,
|
||||
body: {
|
||||
model: "openai/gpt-4o-mini",
|
||||
stream: true,
|
||||
messages: [{ role: "user", content: "hi" }],
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
const sse = await readSSE(response);
|
||||
assert.equal(calls, 2, "should have re-opened the upstream exactly once");
|
||||
assert.match(sse, /RECOVERED/, "client receives the recovered attempt");
|
||||
assert.match(sse, /\[DONE\]/, "recovered stream carries its terminal marker");
|
||||
assert.doesNotMatch(sse, /PARTIAL/, "the discarded first attempt must not leak to the client");
|
||||
});
|
||||
|
||||
test("stream recovery OFF (default): a truncated stream is NOT retried", async () => {
|
||||
await h.seedConnection("openai", { apiKey: "sk-openai-primary" });
|
||||
const apiKey = await h.seedApiKey();
|
||||
// No setting, no env → default OFF.
|
||||
|
||||
let calls = 0;
|
||||
globalThis.fetch = (async () => {
|
||||
calls++;
|
||||
return truncatedOpenAIStream();
|
||||
}) as typeof fetch;
|
||||
|
||||
const response = await h.handleChat(
|
||||
h.buildRequest({
|
||||
authKey: apiKey.key,
|
||||
body: {
|
||||
model: "openai/gpt-4o-mini",
|
||||
stream: true,
|
||||
messages: [{ role: "user", content: "hi" }],
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
await readSSE(response);
|
||||
assert.equal(calls, 1, "default path calls upstream exactly once — zero behavior change");
|
||||
});
|
||||
@@ -169,5 +169,8 @@ test("contract: /api/v1/messages/count_tokens computes token estimate from text
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
const body = (await response.json()) as any;
|
||||
assert.equal(body.input_tokens, 3);
|
||||
// Real tiktoken count (countTextTokens): "abcd" => 1, "12345678" => 3 (digits split).
|
||||
// The previous expectation (3) was the old ceil(chars/4) heuristic, replaced by the
|
||||
// tiktoken-based estimator; the accurate total for this payload is 4.
|
||||
assert.equal(body.input_tokens, 4);
|
||||
});
|
||||
|
||||
@@ -10,7 +10,11 @@
|
||||
}
|
||||
],
|
||||
"generationConfig": {
|
||||
"maxOutputTokens": 65536
|
||||
"maxOutputTokens": 65536,
|
||||
"thinkingConfig": {
|
||||
"includeThoughts": true,
|
||||
"thinkingBudget": 24576
|
||||
}
|
||||
},
|
||||
"model": "gemini-2.5-flash",
|
||||
"safetySettings": [
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"contents": [
|
||||
{
|
||||
"parts": [
|
||||
{
|
||||
"text": "Hello Gemini!"
|
||||
}
|
||||
],
|
||||
"role": "user"
|
||||
}
|
||||
],
|
||||
"generationConfig": {
|
||||
"maxOutputTokens": 65536,
|
||||
"thinkingConfig": {
|
||||
"includeThoughts": true,
|
||||
"thinkingBudget": 24576
|
||||
}
|
||||
},
|
||||
"model": "gemini-2.5-flash",
|
||||
"safetySettings": [
|
||||
{
|
||||
"category": "HARM_CATEGORY_HATE_SPEECH",
|
||||
"threshold": "OFF"
|
||||
},
|
||||
{
|
||||
"category": "HARM_CATEGORY_DANGEROUS_CONTENT",
|
||||
"threshold": "OFF"
|
||||
},
|
||||
{
|
||||
"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
|
||||
"threshold": "OFF"
|
||||
},
|
||||
{
|
||||
"category": "HARM_CATEGORY_HARASSMENT",
|
||||
"threshold": "OFF"
|
||||
},
|
||||
{
|
||||
"category": "HARM_CATEGORY_CIVIC_INTEGRITY",
|
||||
"threshold": "OFF"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -11,7 +11,11 @@
|
||||
],
|
||||
"generationConfig": {
|
||||
"maxOutputTokens": 8192,
|
||||
"temperature": 0.7
|
||||
"temperature": 0.7,
|
||||
"thinkingConfig": {
|
||||
"includeThoughts": true,
|
||||
"thinkingBudget": 24576
|
||||
}
|
||||
},
|
||||
"model": "gemini-2.5-pro",
|
||||
"safetySettings": [
|
||||
|
||||
48
tests/unit/access-scopes-infer.test.ts
Normal file
48
tests/unit/access-scopes-infer.test.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { inferRequiredScope } from "../../src/server/authz/accessScopes.ts";
|
||||
|
||||
test("read methods default to read", () => {
|
||||
assert.equal(inferRequiredScope("GET", "/api/v1/models"), "read");
|
||||
assert.equal(inferRequiredScope("HEAD", "/api/health"), "read");
|
||||
assert.equal(inferRequiredScope("OPTIONS", "/api/anything"), "read");
|
||||
});
|
||||
|
||||
test("mutating methods default to write", () => {
|
||||
assert.equal(inferRequiredScope("POST", "/api/keys"), "write");
|
||||
assert.equal(inferRequiredScope("PUT", "/api/config"), "write");
|
||||
assert.equal(inferRequiredScope("PATCH", "/api/combo/x"), "write");
|
||||
assert.equal(inferRequiredScope("DELETE", "/api/keys/abc"), "write");
|
||||
});
|
||||
|
||||
test("admin-prefix routes require admin for ANY method", () => {
|
||||
assert.equal(inferRequiredScope("GET", "/api/cli/tokens"), "admin");
|
||||
assert.equal(inferRequiredScope("POST", "/api/cli/tokens"), "admin");
|
||||
assert.equal(inferRequiredScope("DELETE", "/api/cli/tokens/tok_1"), "admin");
|
||||
assert.equal(inferRequiredScope("GET", "/api/oauth/start"), "admin");
|
||||
assert.equal(inferRequiredScope("POST", "/api/auth/login"), "admin");
|
||||
assert.equal(inferRequiredScope("POST", "/api/policy"), "admin");
|
||||
assert.equal(inferRequiredScope("POST", "/api/services/foo/start"), "admin");
|
||||
});
|
||||
|
||||
test("admin-mutation prefixes: GET stays read, mutations become admin", () => {
|
||||
// providers: status is read, but creating/rotating is admin
|
||||
assert.equal(inferRequiredScope("GET", "/api/providers/status"), "read");
|
||||
assert.equal(inferRequiredScope("GET", "/api/providers"), "read");
|
||||
assert.equal(inferRequiredScope("POST", "/api/providers"), "admin");
|
||||
assert.equal(inferRequiredScope("DELETE", "/api/providers/openai"), "admin");
|
||||
// cli-tools/apply writes to the host fs
|
||||
assert.equal(inferRequiredScope("POST", "/api/cli-tools/apply"), "admin");
|
||||
});
|
||||
|
||||
test("a brand-new mutating route is write by default (not admin)", () => {
|
||||
assert.equal(inferRequiredScope("POST", "/api/some-future-route"), "write");
|
||||
assert.equal(inferRequiredScope("GET", "/api/some-future-route"), "read");
|
||||
});
|
||||
|
||||
test("prefix matching does not over-match unrelated paths", () => {
|
||||
// "/api/authz-inventory" must NOT be caught by the "/api/auth" admin prefix
|
||||
assert.equal(inferRequiredScope("GET", "/api/authz-inventory"), "read");
|
||||
// "/api/services" itself and its children are admin, but a lookalike is not
|
||||
assert.equal(inferRequiredScope("GET", "/api/services-catalog"), "read");
|
||||
});
|
||||
58
tests/unit/access-token-scopes.test.ts
Normal file
58
tests/unit/access-token-scopes.test.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
ACCESS_SCOPES,
|
||||
isAccessScope,
|
||||
scopeSatisfies,
|
||||
normalizeScope,
|
||||
} from "../../src/lib/accessTokens/scopes.ts";
|
||||
|
||||
test("ACCESS_SCOPES are exactly read/write/admin", () => {
|
||||
assert.deepEqual([...ACCESS_SCOPES], ["read", "write", "admin"]);
|
||||
});
|
||||
|
||||
test("isAccessScope accepts valid scopes and rejects anything else", () => {
|
||||
assert.equal(isAccessScope("read"), true);
|
||||
assert.equal(isAccessScope("write"), true);
|
||||
assert.equal(isAccessScope("admin"), true);
|
||||
assert.equal(isAccessScope("superuser"), false);
|
||||
assert.equal(isAccessScope(""), false);
|
||||
assert.equal(isAccessScope(null), false);
|
||||
assert.equal(isAccessScope(undefined), false);
|
||||
assert.equal(isAccessScope(3), false);
|
||||
});
|
||||
|
||||
test("scopeSatisfies enforces the admin ⊃ write ⊃ read hierarchy", () => {
|
||||
// admin satisfies everything
|
||||
assert.equal(scopeSatisfies("admin", "read"), true);
|
||||
assert.equal(scopeSatisfies("admin", "write"), true);
|
||||
assert.equal(scopeSatisfies("admin", "admin"), true);
|
||||
// write satisfies read+write, not admin
|
||||
assert.equal(scopeSatisfies("write", "read"), true);
|
||||
assert.equal(scopeSatisfies("write", "write"), true);
|
||||
assert.equal(scopeSatisfies("write", "admin"), false);
|
||||
// read satisfies only read
|
||||
assert.equal(scopeSatisfies("read", "read"), true);
|
||||
assert.equal(scopeSatisfies("read", "write"), false);
|
||||
assert.equal(scopeSatisfies("read", "admin"), false);
|
||||
});
|
||||
|
||||
test("scopeSatisfies returns false for invalid `have` scopes (fail closed)", () => {
|
||||
assert.equal(scopeSatisfies("bogus", "read"), false);
|
||||
assert.equal(scopeSatisfies(null, "read"), false);
|
||||
assert.equal(scopeSatisfies(undefined, "read"), false);
|
||||
assert.equal(scopeSatisfies("", "read"), false);
|
||||
});
|
||||
|
||||
test("normalizeScope falls back to read by default for invalid input", () => {
|
||||
assert.equal(normalizeScope("write"), "write");
|
||||
assert.equal(normalizeScope("admin"), "admin");
|
||||
assert.equal(normalizeScope("bogus"), "read");
|
||||
assert.equal(normalizeScope(undefined), "read");
|
||||
assert.equal(normalizeScope(null), "read");
|
||||
});
|
||||
|
||||
test("normalizeScope honors a custom fallback", () => {
|
||||
assert.equal(normalizeScope("bogus", "write"), "write");
|
||||
assert.equal(normalizeScope(undefined, "admin"), "admin");
|
||||
});
|
||||
104
tests/unit/access-tokens-db.test.ts
Normal file
104
tests/unit/access-tokens-db.test.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
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";
|
||||
|
||||
// DB-backed access-token store. Uses an isolated DATA_DIR + closes the handle in
|
||||
// test.after (CLAUDE.md "Database Handles in Tests" — otherwise Node's runner hangs).
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-access-tokens-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
process.env.DISABLE_SQLITE_AUTO_BACKUP = "true";
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const at = await import("../../src/lib/db/accessTokens.ts");
|
||||
|
||||
test.after(() => {
|
||||
try {
|
||||
core.resetDbInstance();
|
||||
} catch {}
|
||||
try {
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
} catch {}
|
||||
});
|
||||
|
||||
test("createAccessToken returns a secret prefixed oma_live_ and a masked record", () => {
|
||||
const { record, secret } = at.createAccessToken({ name: "laptop", scope: "write" });
|
||||
assert.match(secret, /^oma_live_/);
|
||||
assert.equal(record.name, "laptop");
|
||||
assert.equal(record.scope, "write");
|
||||
assert.ok(record.id.startsWith("tok_"));
|
||||
assert.ok(secret.startsWith(record.tokenPrefix), "prefix must be a prefix of the secret");
|
||||
assert.equal(record.revokedAt, null);
|
||||
});
|
||||
|
||||
test("createAccessToken defaults to the safest scope (read) for invalid input", () => {
|
||||
const { record } = at.createAccessToken({ name: "x", scope: "bogus" });
|
||||
assert.equal(record.scope, "read");
|
||||
});
|
||||
|
||||
test("createAccessToken rejects an empty name", () => {
|
||||
assert.throws(() => at.createAccessToken({ name: " ", scope: "read" }), /name is required/);
|
||||
});
|
||||
|
||||
test("verifyAccessToken returns identity+scope for a valid secret, null for wrong", () => {
|
||||
const { secret } = at.createAccessToken({ name: "verify-me", scope: "admin" });
|
||||
const v = at.verifyAccessToken(secret);
|
||||
assert.ok(v);
|
||||
assert.equal(v?.scope, "admin");
|
||||
assert.equal(v?.name, "verify-me");
|
||||
assert.equal(at.verifyAccessToken("oma_live_wrong"), null);
|
||||
assert.equal(at.verifyAccessToken(""), null);
|
||||
assert.equal(at.verifyAccessToken(null), null);
|
||||
});
|
||||
|
||||
test("only the hash is stored — the plaintext secret never lands in the DB", () => {
|
||||
const { secret, record } = at.createAccessToken({ name: "secrecy", scope: "read" });
|
||||
const db = core.getDbInstance();
|
||||
const row = db
|
||||
.prepare("SELECT token_hash, token_prefix FROM cli_access_tokens WHERE id = ?")
|
||||
.get(record.id) as { token_hash: string; token_prefix: string };
|
||||
assert.notEqual(row.token_hash, secret, "must store hash, not plaintext");
|
||||
assert.equal(row.token_hash, at.hashAccessToken(secret));
|
||||
assert.equal(row.token_hash.length, 64, "sha-256 hex");
|
||||
});
|
||||
|
||||
test("verifyAccessToken stamps last_used_at", () => {
|
||||
const { secret, record } = at.createAccessToken({ name: "touch", scope: "read" });
|
||||
assert.equal(at.getAccessToken(record.id)?.lastUsedAt, null);
|
||||
at.verifyAccessToken(secret);
|
||||
assert.notEqual(at.getAccessToken(record.id)?.lastUsedAt, null);
|
||||
});
|
||||
|
||||
test("revoked tokens fail verification", () => {
|
||||
const { secret, record } = at.createAccessToken({ name: "to-revoke", scope: "write" });
|
||||
assert.ok(at.verifyAccessToken(secret));
|
||||
assert.equal(at.revokeAccessToken(record.id), true);
|
||||
assert.equal(at.verifyAccessToken(secret), null);
|
||||
// idempotent: revoking again is a no-op
|
||||
assert.equal(at.revokeAccessToken(record.id), false);
|
||||
});
|
||||
|
||||
test("revokeAccessToken works by display prefix too", () => {
|
||||
const { secret, record } = at.createAccessToken({ name: "by-prefix", scope: "read" });
|
||||
assert.equal(at.revokeAccessToken(record.tokenPrefix), true);
|
||||
assert.equal(at.verifyAccessToken(secret), null);
|
||||
});
|
||||
|
||||
test("expired tokens fail verification", () => {
|
||||
const past = new Date(Date.now() - 60_000).toISOString();
|
||||
const { secret } = at.createAccessToken({ name: "expired", scope: "admin", expiresAt: past });
|
||||
assert.equal(at.verifyAccessToken(secret), null);
|
||||
});
|
||||
|
||||
test("listAccessTokens returns masked records (no secret/hash field)", () => {
|
||||
at.createAccessToken({ name: "listed", scope: "read" });
|
||||
const list = at.listAccessTokens();
|
||||
assert.ok(list.length >= 1);
|
||||
for (const rec of list) {
|
||||
assert.ok("tokenPrefix" in rec);
|
||||
assert.ok(!("secret" in rec));
|
||||
assert.ok(!("tokenHash" in rec));
|
||||
assert.ok(!("token_hash" in rec));
|
||||
}
|
||||
});
|
||||
123
tests/unit/agent-bridge-maintenance-api.test.ts
Normal file
123
tests/unit/agent-bridge-maintenance-api.test.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* Client-side fetch helpers that the AgentBridge maintenance card uses to drive
|
||||
* the already-shipped backend routes (#4084 repair + DELETE cert, #4093
|
||||
* diagnose, #4094 config import/export). Pure integration logic — no DOM — so
|
||||
* it is unit-testable by stubbing global.fetch: each helper must parse the
|
||||
* success body and surface the sanitized server error message on !res.ok.
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const {
|
||||
runDiagnose,
|
||||
removeCaCert,
|
||||
repairMitmState,
|
||||
fetchAgentBridgeConfig,
|
||||
importAgentBridgeConfig,
|
||||
} = await import("../../src/lib/inspector/agentBridgeMaintenanceApi.ts");
|
||||
|
||||
type FetchCall = { url: string; init?: RequestInit };
|
||||
|
||||
function stubFetch(handler: (call: FetchCall) => { ok: boolean; status?: number; body: unknown }) {
|
||||
const calls: FetchCall[] = [];
|
||||
const original = global.fetch;
|
||||
global.fetch = (async (url: string, init?: RequestInit) => {
|
||||
const call = { url: String(url), init };
|
||||
calls.push(call);
|
||||
const { ok, status = ok ? 200 : 500, body } = handler(call);
|
||||
return {
|
||||
ok,
|
||||
status,
|
||||
json: async () => body,
|
||||
} as unknown as Response;
|
||||
}) as typeof fetch;
|
||||
return {
|
||||
calls,
|
||||
restore() {
|
||||
global.fetch = original;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test("runDiagnose returns the report and hits GET /diagnose", async () => {
|
||||
const report = { healthy: false, checks: [{ name: "cert-trusted", ok: false, hint: "trust it" }], port: 443 };
|
||||
const f = stubFetch(() => ({ ok: true, body: report }));
|
||||
try {
|
||||
const result = await runDiagnose();
|
||||
assert.equal(result.healthy, false);
|
||||
assert.equal(result.port, 443);
|
||||
assert.equal(result.checks[0].name, "cert-trusted");
|
||||
assert.equal(f.calls[0].url, "/api/tools/agent-bridge/diagnose");
|
||||
} finally {
|
||||
f.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test("removeCaCert DELETEs /cert and returns trusted flag", async () => {
|
||||
const f = stubFetch(() => ({ ok: true, body: { ok: true, trusted: false } }));
|
||||
try {
|
||||
const result = await removeCaCert();
|
||||
assert.equal(result.trusted, false);
|
||||
assert.equal(f.calls[0].url, "/api/tools/agent-bridge/cert");
|
||||
assert.equal(f.calls[0].init?.method, "DELETE");
|
||||
} finally {
|
||||
f.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test("repairMitmState POSTs /repair and returns the repaired list", async () => {
|
||||
const f = stubFetch(() => ({ ok: true, body: { ok: true, repaired: ["dns", "system-proxy"] } }));
|
||||
try {
|
||||
const result = await repairMitmState();
|
||||
assert.deepEqual(result.repaired, ["dns", "system-proxy"]);
|
||||
assert.equal(f.calls[0].url, "/api/tools/agent-bridge/repair");
|
||||
assert.equal(f.calls[0].init?.method, "POST");
|
||||
} finally {
|
||||
f.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test("fetchAgentBridgeConfig GETs /config and returns the portable blob", async () => {
|
||||
const cfg = { version: 1, bypassPatterns: ["*.corp"], customHosts: [], agentMappings: {} };
|
||||
const f = stubFetch(() => ({ ok: true, body: cfg }));
|
||||
try {
|
||||
const result = await fetchAgentBridgeConfig();
|
||||
assert.equal(result.version, 1);
|
||||
assert.deepEqual(result.bypassPatterns, ["*.corp"]);
|
||||
assert.equal(f.calls[0].url, "/api/tools/agent-bridge/config");
|
||||
} finally {
|
||||
f.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test("importAgentBridgeConfig POSTs the config and returns the counts", async () => {
|
||||
const cfg = { version: 1 as const, bypassPatterns: ["*.corp"], customHosts: [], agentMappings: {} };
|
||||
const f = stubFetch(() => ({ ok: true, body: { ok: true, bypassPatterns: 1, customHosts: 0, agents: 0 } }));
|
||||
try {
|
||||
const result = await importAgentBridgeConfig(cfg);
|
||||
assert.equal(result.bypassPatterns, 1);
|
||||
assert.equal(f.calls[0].url, "/api/tools/agent-bridge/config");
|
||||
assert.equal(f.calls[0].init?.method, "POST");
|
||||
assert.equal(JSON.parse(String(f.calls[0].init?.body)).version, 1);
|
||||
} finally {
|
||||
f.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test("each helper surfaces the sanitized server error message on !res.ok", async () => {
|
||||
const f = stubFetch(() => ({ ok: false, status: 400, body: { error: { message: "Invalid AgentBridge config" } } }));
|
||||
try {
|
||||
await assert.rejects(() => importAgentBridgeConfig({ version: 1, bypassPatterns: [], customHosts: [], agentMappings: {} }), /Invalid AgentBridge config/);
|
||||
} finally {
|
||||
f.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test("helpers fall back to HTTP status when the error body has no message", async () => {
|
||||
const f = stubFetch(() => ({ ok: false, status: 503, body: null }));
|
||||
try {
|
||||
await assert.rejects(() => repairMitmState(), /HTTP 503/);
|
||||
} finally {
|
||||
f.restore();
|
||||
}
|
||||
});
|
||||
@@ -48,6 +48,26 @@ test("normalizeAgentBridgeState falls back to safe defaults for empty/garbage in
|
||||
}
|
||||
});
|
||||
|
||||
test("normalizeAgentBridgeState maps orphanedStateDetected + dnsConfigured from getMitmStatus (Gap 7 repair banner)", () => {
|
||||
// getMitmStatus() returns these two flags; the maintenance card needs them in
|
||||
// serverState to decide whether to surface the "orphaned state — repair" banner.
|
||||
const routeShape = {
|
||||
server: { running: false, dnsConfigured: true, certExists: true, orphanedStateDetected: true },
|
||||
agents: [],
|
||||
};
|
||||
const result = normalizeAgentBridgeState(routeShape);
|
||||
assert.equal(result.serverState.orphanedStateDetected, true, "orphanedStateDetected maps through");
|
||||
assert.equal(result.serverState.dnsConfigured, true, "dnsConfigured maps through");
|
||||
});
|
||||
|
||||
test("normalizeAgentBridgeState defaults orphanedStateDetected + dnsConfigured to false", () => {
|
||||
for (const bad of [null, undefined, {}, { server: {} }]) {
|
||||
const result = normalizeAgentBridgeState(bad);
|
||||
assert.equal(result.serverState.orphanedStateDetected, false);
|
||||
assert.equal(result.serverState.dnsConfigured, false);
|
||||
}
|
||||
});
|
||||
|
||||
test("normalizeAgentBridgeState passes a correctly-shaped payload through intact", () => {
|
||||
const correct = {
|
||||
...DEFAULT_AGENT_BRIDGE_STATE,
|
||||
|
||||
63
tests/unit/auto-combos-enhanced-4235.test.ts
Normal file
63
tests/unit/auto-combos-enhanced-4235.test.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* #4235 — Enhanced `auto/*` combos.
|
||||
*
|
||||
* Phase A: the README advertises `auto/cheap`, `auto/offline`, `auto/smart`
|
||||
* (and they already resolve via parseAutoPrefix → createVirtualAutoCombo), but
|
||||
* they were missing from AUTO_TEMPLATE_VARIANTS, so `/v1/models` and the
|
||||
* dashboard never listed them — the catalog/UI drifted from the README.
|
||||
*
|
||||
* Later phases (suffix parser `auto/<category>:<tier>`, new categories, `:pro`)
|
||||
* extend the assertions below.
|
||||
*/
|
||||
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-4235-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "auto-4235-test-secret";
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const builtinCatalog = await import("../../open-sse/services/autoCombo/builtinCatalog.ts");
|
||||
|
||||
function resetStorage() {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
test.beforeEach(() => {
|
||||
resetStorage();
|
||||
});
|
||||
|
||||
test.after(() => {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("#4235 Phase A: README-advertised cheap/offline/smart are in the built-in catalog", () => {
|
||||
const ids = Object.keys(builtinCatalog.AUTO_TEMPLATE_VARIANTS);
|
||||
for (const id of ["auto/cheap", "auto/offline", "auto/smart"]) {
|
||||
assert.ok(
|
||||
ids.includes(id),
|
||||
`expected ${id} in AUTO_TEMPLATE_VARIANTS (advertised in /v1/models)`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("#4235 Phase A: cheap/offline/smart map to their scoring variant", () => {
|
||||
assert.equal(builtinCatalog.AUTO_TEMPLATE_VARIANTS["auto/cheap"], "cheap");
|
||||
assert.equal(builtinCatalog.AUTO_TEMPLATE_VARIANTS["auto/offline"], "offline");
|
||||
assert.equal(builtinCatalog.AUTO_TEMPLATE_VARIANTS["auto/smart"], "smart");
|
||||
});
|
||||
|
||||
test("#4235 Phase A: each new entry materializes into a virtual auto-combo", async () => {
|
||||
for (const id of ["auto/cheap", "auto/offline", "auto/smart"]) {
|
||||
const suffix = id.slice("auto/".length);
|
||||
const combo = await builtinCatalog.createBuiltinAutoCombo(id, suffix);
|
||||
assert.equal(combo.id, id, `${id} combo id`);
|
||||
assert.equal(combo.strategy, "auto", `${id} uses the auto strategy`);
|
||||
}
|
||||
});
|
||||
120
tests/unit/auto-combos-suffixes-4235.test.ts
Normal file
120
tests/unit/auto-combos-suffixes-4235.test.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* #4235 Phase B — OpenRouter-style `auto/<category>:<tier>` suffix combos.
|
||||
*
|
||||
* Covers the pure composition layer (parse + tier→weights + candidate filter) and
|
||||
* the end-to-end advertisement of the curated suffix combos in `/v1/models`.
|
||||
*/
|
||||
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-4235b-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "auto-4235b-test-secret";
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const suffix = await import("../../open-sse/services/autoCombo/suffixComposition.ts");
|
||||
const modePacks = await import("../../open-sse/services/autoCombo/modePacks.ts");
|
||||
const builtinCatalog = await import("../../open-sse/services/autoCombo/builtinCatalog.ts");
|
||||
const v1ModelsCatalog = await import("../../src/app/api/v1/models/catalog.ts");
|
||||
|
||||
function resetStorage() {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
test.beforeEach(() => resetStorage());
|
||||
test.after(() => {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("#4235 parseAutoSuffix parses category and category:tier", () => {
|
||||
assert.deepEqual(suffix.parseAutoSuffix("coding:fast"), {
|
||||
valid: true,
|
||||
category: "coding",
|
||||
tier: "fast",
|
||||
});
|
||||
assert.deepEqual(suffix.parseAutoSuffix("vision"), { valid: true, category: "vision" });
|
||||
assert.deepEqual(suffix.parseAutoSuffix("reasoning:pro"), {
|
||||
valid: true,
|
||||
category: "reasoning",
|
||||
tier: "pro",
|
||||
});
|
||||
// floor is an accepted tier alias of cheap
|
||||
assert.equal(suffix.parseAutoSuffix("coding:floor").valid, true);
|
||||
});
|
||||
|
||||
test("#4235 parseAutoSuffix rejects unknown categories/tiers and malformed input", () => {
|
||||
assert.equal(suffix.parseAutoSuffix("coding:bogus").valid, false);
|
||||
assert.equal(suffix.parseAutoSuffix("bogus:fast").valid, false);
|
||||
assert.equal(suffix.parseAutoSuffix("coding:fast:extra").valid, false);
|
||||
// a bare flat-variant token (fast/cheap/smart) is NOT a category — left to parseAutoPrefix
|
||||
assert.equal(suffix.parseAutoSuffix("fast").valid, false);
|
||||
assert.equal(suffix.parseAutoSuffix("").valid, false);
|
||||
});
|
||||
|
||||
test("#4235 tierToWeightVariant maps tiers to scoring profiles", () => {
|
||||
assert.equal(suffix.tierToWeightVariant("fast"), "fast");
|
||||
assert.equal(suffix.tierToWeightVariant("cheap"), "cheap");
|
||||
assert.equal(suffix.tierToWeightVariant("floor"), "cheap");
|
||||
assert.equal(suffix.tierToWeightVariant("reliable"), "reliability");
|
||||
// free/pro carry no weight bias — the candidate filter does the work
|
||||
assert.equal(suffix.tierToWeightVariant("free"), undefined);
|
||||
assert.equal(suffix.tierToWeightVariant("pro"), undefined);
|
||||
assert.equal(suffix.tierToWeightVariant(undefined), undefined);
|
||||
});
|
||||
|
||||
test("#4235 buildAutoCandidateFilter only filters when a constraint applies", () => {
|
||||
// coding/chat with no tier → no narrowing
|
||||
assert.equal(suffix.buildAutoCandidateFilter("coding", undefined), null);
|
||||
assert.equal(suffix.buildAutoCandidateFilter("chat", undefined), null);
|
||||
// category constraints (vision/reasoning/multimodal) and tier constraints (free/pro) → a filter fn
|
||||
assert.equal(typeof suffix.buildAutoCandidateFilter("vision", undefined), "function");
|
||||
assert.equal(typeof suffix.buildAutoCandidateFilter("reasoning", undefined), "function");
|
||||
assert.equal(typeof suffix.buildAutoCandidateFilter(undefined, "free"), "function");
|
||||
assert.equal(typeof suffix.buildAutoCandidateFilter("coding", "pro"), "function");
|
||||
});
|
||||
|
||||
test("#4235 reliability-first mode pack exists and is normalized", () => {
|
||||
const pack = modePacks.MODE_PACKS["reliability-first"];
|
||||
assert.ok(pack, "reliability-first pack registered");
|
||||
const sum = Object.values(pack).reduce((a, b) => a + b, 0);
|
||||
assert.ok(Math.abs(sum - 1.0) < 0.01, `reliability-first weights sum to ~1.0 (got ${sum})`);
|
||||
// health + stability should dominate
|
||||
assert.ok(pack.health >= 0.3, "reliability-first leans on circuit-breaker health");
|
||||
});
|
||||
|
||||
test("#4235 createBuiltinAutoCombo composes tier weights for auto/coding:fast", async () => {
|
||||
const combo = await builtinCatalog.createBuiltinAutoCombo("auto/coding:fast", "coding:fast");
|
||||
assert.equal(combo.id, "auto/coding:fast");
|
||||
assert.equal(combo.strategy, "auto");
|
||||
// :fast → ship-fast weights (latency-dominant)
|
||||
assert.deepEqual(combo.weights, modePacks.MODE_PACKS["ship-fast"]);
|
||||
});
|
||||
|
||||
test("#4235 createBuiltinAutoCombo composes reliability weights for auto/coding:reliable", async () => {
|
||||
const combo = await builtinCatalog.createBuiltinAutoCombo(
|
||||
"auto/coding:reliable",
|
||||
"coding:reliable"
|
||||
);
|
||||
assert.deepEqual(combo.weights, modePacks.MODE_PACKS["reliability-first"]);
|
||||
});
|
||||
|
||||
test("#4235 /v1/models advertises the curated auto/<category>:<tier> combos", async () => {
|
||||
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; owned_by?: string }> };
|
||||
const ids = new Set(body.data.map((m) => m.id));
|
||||
|
||||
for (const autoId of builtinCatalog.AUTO_SUFFIX_VARIANTS) {
|
||||
assert.ok(ids.has(autoId), `expected /v1/models to advertise ${autoId}`);
|
||||
}
|
||||
const codingFast = body.data.find((m) => m.id === "auto/coding:fast");
|
||||
assert.equal(codingFast?.owned_by, "combo");
|
||||
});
|
||||
@@ -248,12 +248,17 @@ test("ensureGitTagExists verifies refs/tags paths and throws a clear error when
|
||||
test("auto update script builders generate npm, source, and docker-compose scripts with quoting and patch commits", () => {
|
||||
const npmScript = autoUpdate.buildNpmUpdateScript("3.6.0");
|
||||
assert.match(npmScript, /npm install -g omniroute@3.6.0/);
|
||||
// Optional deps (better-sqlite3, keytar, tls-client, and the llmlingua SLM stack)
|
||||
// must survive an update — install them explicitly so an `omit=optional` config
|
||||
// cannot silently drop them.
|
||||
assert.match(npmScript, /--include=optional/);
|
||||
assert.match(npmScript, /pm2 restart omniroute \|\| true/);
|
||||
assert.match(npmScript, /Successfully updated to v3.6.0/);
|
||||
|
||||
const sourceScript = autoUpdate.buildSourceUpdateScript("3.6.0", "upstream");
|
||||
assert.match(sourceScript, /git fetch --tags 'upstream'/);
|
||||
assert.match(sourceScript, /git stash --include-untracked/);
|
||||
assert.match(sourceScript, /npm install --include=optional --legacy-peer-deps/);
|
||||
assert.match(sourceScript, /node scripts\/dev\/sync-env\.mjs 2>\/dev\/null \|\| true/);
|
||||
assert.match(sourceScript, /Successfully updated to v3\.6\.0/);
|
||||
|
||||
|
||||
@@ -337,3 +337,102 @@ test("sanitizeReasoningEffortForProvider: non-object body returns unchanged", ()
|
||||
const arr: unknown[] = [];
|
||||
assert.equal(sanitizeReasoningEffortForProvider(arr, "xiaomi-mimo", "x", null), arr);
|
||||
});
|
||||
|
||||
// ── Native DeepSeek (api.deepseek.com) ───────────────────────────────────────
|
||||
// DeepSeek V4 thinking mode accepts reasoning_effort ONLY as {high, max}. The
|
||||
// internal OmniRoute scale (low|medium|high|xhigh, xhigh = top) must be mapped
|
||||
// onto DeepSeek's native vocabulary so the client's requested effort is honored
|
||||
// instead of silently dropped to the default. This is the INVERSE of the
|
||||
// OpenRouter-DeepSeek path, whose normalized API expects xhigh, not max.
|
||||
|
||||
test("sanitizeReasoningEffortForProvider: native deepseek maps xhigh → max", () => {
|
||||
const log = makeLog();
|
||||
const body = {
|
||||
model: "deepseek-v4-pro",
|
||||
reasoning_effort: "xhigh",
|
||||
messages: [{ role: "user", content: "hi" }],
|
||||
};
|
||||
const result = sanitizeReasoningEffortForProvider(body, "deepseek", "deepseek-v4-pro", log);
|
||||
assert.notEqual(result, body, "must return a new object when mutating");
|
||||
assert.equal((result as any).reasoning_effort, "max");
|
||||
assert.equal((result as any).model, "deepseek-v4-pro", "other fields preserved");
|
||||
assert.ok(
|
||||
log.messages.some(([tag, m]) => tag === "REASONING_SANITIZE" && /xhigh → max/.test(m)),
|
||||
"logs the xhigh → max mapping"
|
||||
);
|
||||
});
|
||||
|
||||
test("sanitizeReasoningEffortForProvider: native deepseek preserves max", () => {
|
||||
const log = makeLog();
|
||||
const body = {
|
||||
model: "deepseek-v4-flash",
|
||||
reasoning_effort: "max",
|
||||
messages: [{ role: "user", content: "hi" }],
|
||||
};
|
||||
const result = sanitizeReasoningEffortForProvider(body, "deepseek", "deepseek-v4-flash", log);
|
||||
assert.equal(result, body, "max is DeepSeek's native top tier — passes through unchanged");
|
||||
assert.equal((result as any).reasoning_effort, "max");
|
||||
assert.equal(log.messages.length, 0);
|
||||
});
|
||||
|
||||
test("sanitizeReasoningEffortForProvider: native deepseek clamps low → high", () => {
|
||||
const body = {
|
||||
model: "deepseek-v4-pro",
|
||||
reasoning_effort: "low",
|
||||
messages: [{ role: "user", content: "hi" }],
|
||||
};
|
||||
const result = sanitizeReasoningEffortForProvider(body, "deepseek", "deepseek-v4-pro", null);
|
||||
assert.notEqual(result, body, "must return a new object when mutating");
|
||||
assert.equal((result as any).reasoning_effort, "high", "below the {high, max} floor → high");
|
||||
});
|
||||
|
||||
test("sanitizeReasoningEffortForProvider: native deepseek clamps medium → high", () => {
|
||||
const body = {
|
||||
model: "deepseek-v4-pro",
|
||||
reasoning_effort: "medium",
|
||||
messages: [{ role: "user", content: "hi" }],
|
||||
};
|
||||
const result = sanitizeReasoningEffortForProvider(body, "deepseek", "deepseek-v4-pro", null);
|
||||
assert.equal((result as any).reasoning_effort, "high");
|
||||
});
|
||||
|
||||
test("sanitizeReasoningEffortForProvider: native deepseek preserves high unchanged", () => {
|
||||
const body = {
|
||||
model: "deepseek-v4-pro",
|
||||
reasoning_effort: "high",
|
||||
messages: [{ role: "user", content: "hi" }],
|
||||
};
|
||||
const result = sanitizeReasoningEffortForProvider(body, "deepseek", "deepseek-v4-pro", null);
|
||||
assert.equal(result, body, "high is already valid — passes through unchanged");
|
||||
assert.equal((result as any).reasoning_effort, "high");
|
||||
});
|
||||
|
||||
test("sanitizeReasoningEffortForProvider: native deepseek maps nested reasoning.effort xhigh → max", () => {
|
||||
const body = {
|
||||
model: "deepseek-v4-pro",
|
||||
reasoning: { effort: "xhigh", summary: "auto" },
|
||||
input: [],
|
||||
};
|
||||
const result = sanitizeReasoningEffortForProvider(body, "deepseek", "deepseek-v4-pro", null);
|
||||
assert.equal((result as any).reasoning.effort, "max");
|
||||
assert.equal((result as any).reasoning.summary, "auto", "other reasoning fields preserved");
|
||||
assert.equal((result as any).reasoning_effort, undefined);
|
||||
});
|
||||
|
||||
test("sanitizeReasoningEffortForProvider: OpenRouter DeepSeek still preserves xhigh (not native)", () => {
|
||||
// Regression guard: the native-deepseek mapping must NOT touch openrouter,
|
||||
// whose normalized API expects xhigh (issue earendil-works/pi#4055).
|
||||
const body = {
|
||||
model: "deepseek/deepseek-v4-pro",
|
||||
reasoning_effort: "xhigh",
|
||||
messages: [{ role: "user", content: "hi" }],
|
||||
};
|
||||
const result = sanitizeReasoningEffortForProvider(
|
||||
body,
|
||||
"openrouter",
|
||||
"deepseek/deepseek-v4-pro",
|
||||
null
|
||||
);
|
||||
assert.equal(result, body);
|
||||
assert.equal((result as any).reasoning_effort, "xhigh");
|
||||
});
|
||||
|
||||
@@ -107,6 +107,36 @@ test("resolveNextBuildEnv forces stable build worker mode unless already provide
|
||||
assert.equal(preservedEnv.NODE_ENV, "production");
|
||||
});
|
||||
|
||||
// Escalated bug (WhatsApp BR, cmqiuhd7600): a local `npm run build` stalls/OOMs
|
||||
// during the webpack production pass ("Compiling instrumentation" bundles the whole
|
||||
// server graph). #4076/#4104 raised the heap only in the Docker builder stage; the
|
||||
// local/native path (build-next-isolated.mjs → resolveNextBuildEnv) was left on V8's
|
||||
// default ~2 GB ceiling, so memory-constrained npm-global installs hit the same OOM.
|
||||
test("resolveNextBuildEnv raises the Node heap for memory-constrained local builds", () => {
|
||||
const env = resolveNextBuildEnv({ NODE_ENV: "production" });
|
||||
const match = (env.NODE_OPTIONS ?? "").match(/--max-old-space-size=(\d+)/);
|
||||
assert.ok(
|
||||
match,
|
||||
"local build must set NODE_OPTIONS --max-old-space-size to avoid the webpack-pass OOM"
|
||||
);
|
||||
assert.ok(
|
||||
Number(match[1]) >= 4096,
|
||||
`build heap default must be >= 4096 MB (the V8 default ~2 GB OOMed); got ${match[1]}`
|
||||
);
|
||||
});
|
||||
|
||||
test("resolveNextBuildEnv does not clobber an existing --max-old-space-size (Docker)", () => {
|
||||
const env = resolveNextBuildEnv({ NODE_OPTIONS: "--max-old-space-size=8192" });
|
||||
const occurrences = (env.NODE_OPTIONS.match(/--max-old-space-size=/g) || []).length;
|
||||
assert.equal(occurrences, 1, "must not duplicate the heap flag when one is already set");
|
||||
assert.match(env.NODE_OPTIONS, /--max-old-space-size=8192/);
|
||||
});
|
||||
|
||||
test("resolveNextBuildEnv honors the OMNIROUTE_BUILD_MEMORY_MB override", () => {
|
||||
const env = resolveNextBuildEnv({ OMNIROUTE_BUILD_MEMORY_MB: "6144" });
|
||||
assert.match(env.NODE_OPTIONS, /--max-old-space-size=6144/);
|
||||
});
|
||||
|
||||
test("getTransientBuildPaths leaves _tasks in place by default", () => {
|
||||
const paths = getTransientBuildPaths("/repo", {});
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ function seedSidecarSources(root: string) {
|
||||
const files = [
|
||||
"node_modules/wreq-js/rust/lib.so",
|
||||
"node_modules/better-sqlite3/build/Release/better_sqlite3.node",
|
||||
"src/mitm/tproxy/native/build/Release/transparent.node",
|
||||
"node_modules/@swc/helpers/package.json",
|
||||
"node_modules/pino-abstract-transport/index.js",
|
||||
"node_modules/pino-pretty/index.js",
|
||||
@@ -70,7 +71,13 @@ test("assembleStandalone copies standalone + static + public + sidecars into out
|
||||
fs.mkdirSync(path.join(tmp, "public"), { recursive: true });
|
||||
fs.writeFileSync(path.join(tmp, "public", "logo.svg"), "<svg/>");
|
||||
|
||||
assembleStandalone({ distDir, outDir, projectRoot: tmp, sanitizePaths: false, copyNatives: false });
|
||||
assembleStandalone({
|
||||
distDir,
|
||||
outDir,
|
||||
projectRoot: tmp,
|
||||
sanitizePaths: false,
|
||||
copyNatives: false,
|
||||
});
|
||||
|
||||
assert.ok(fs.existsSync(path.join(outDir, "server.js")), "server.js copied");
|
||||
// Static lands under the distDir path (.build/next/static), where the standalone
|
||||
@@ -128,5 +135,27 @@ test("async and sync sidecar copy paths produce identical bundle trees", async (
|
||||
asyncTree,
|
||||
"sync (assembleStandalone) and async (sync*ToDir) must copy the same sidecar tree"
|
||||
);
|
||||
// The TPROXY native addon must land at the cwd-relative path the runtime loader
|
||||
// (transparentSocket.ts) resolves in the standalone bundle.
|
||||
assert.ok(
|
||||
asyncTree.includes("src/mitm/tproxy/native/build/Release/transparent.node"),
|
||||
"TPROXY transparent.node copied into the standalone bundle"
|
||||
);
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("the TPROXY addon source is skipped gracefully when it was not built (non-Linux)", async () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "assemble-skip-"));
|
||||
const projectRoot = path.join(tmp, "src-root");
|
||||
// Seed everything EXCEPT the tproxy addon (simulating a non-Linux build).
|
||||
fs.mkdirSync(projectRoot, { recursive: true });
|
||||
const out = path.join(tmp, "out");
|
||||
fs.mkdirSync(out, { recursive: true });
|
||||
// No throw even though src/mitm/tproxy/native/... is absent.
|
||||
await syncStandaloneNativeAssets(projectRoot, fs.promises, { log() {} }, out);
|
||||
assert.ok(
|
||||
!fs.existsSync(path.join(out, "src/mitm/tproxy/native/build/Release/transparent.node")),
|
||||
"absent addon is simply not copied (graceful skip)"
|
||||
);
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
91
tests/unit/build/check-mutation-ratchet.test.ts
Normal file
91
tests/unit/build/check-mutation-ratchet.test.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import os from "node:os";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import {
|
||||
evaluateMutationRatchet,
|
||||
mutationScoreForFile,
|
||||
measureMutationScores,
|
||||
readBaselineMutationScores,
|
||||
} from "../../../scripts/check/check-mutation-ratchet.mjs";
|
||||
|
||||
// ── evaluateMutationRatchet: direction UP (score can only improve) ───────────
|
||||
test("mutation ratchet flags a drop (direction up)", () => {
|
||||
assert.equal(evaluateMutationRatchet(72.0, 75.0).regressed, true);
|
||||
assert.equal(evaluateMutationRatchet(76.0, 75.0).regressed, false);
|
||||
assert.equal(evaluateMutationRatchet(76.0, 75.0).improved, true);
|
||||
assert.equal(evaluateMutationRatchet(75.0, 75.0).regressed, false); // equal holds
|
||||
assert.equal(evaluateMutationRatchet(75.0, 75.0).improved, false);
|
||||
});
|
||||
|
||||
// ── mutationScoreForFile: covered score = detected/(detected+survived),
|
||||
// NoCoverage EXCLUDED (it is a coverage gap, not a test-quality signal). ─────
|
||||
test("mutationScoreForFile computes the covered score and excludes NoCoverage", () => {
|
||||
const fileData = {
|
||||
mutants: [
|
||||
{ status: "Killed" },
|
||||
{ status: "Killed" },
|
||||
{ status: "Killed" },
|
||||
{ status: "Timeout" }, // Timeout counts as detected
|
||||
{ status: "Survived" },
|
||||
{ status: "Survived" },
|
||||
{ status: "NoCoverage" }, // excluded from denominator
|
||||
{ status: "NoCoverage" },
|
||||
{ status: "Ignored" }, // excluded (not a valid mutant)
|
||||
],
|
||||
};
|
||||
// detected = 4 (3 Killed + 1 Timeout); denom = 6 (+2 Survived); NoCoverage/Ignored out.
|
||||
assert.ok(Math.abs(mutationScoreForFile(fileData) - (4 / 6) * 100) < 1e-9);
|
||||
});
|
||||
|
||||
test("mutationScoreForFile returns null when there are no covered mutants", () => {
|
||||
assert.equal(mutationScoreForFile({ mutants: [{ status: "NoCoverage" }] }), null);
|
||||
assert.equal(mutationScoreForFile({ mutants: [] }), null);
|
||||
});
|
||||
|
||||
// ── measureMutationScores: per-file scores from a report (and merges batches) ─
|
||||
test("measureMutationScores maps each mutated file to its score", () => {
|
||||
const report = {
|
||||
files: {
|
||||
"src/a.ts": { mutants: [{ status: "Killed" }, { status: "Survived" }] }, // 50
|
||||
"src/b.ts": { mutants: [{ status: "Killed" }, { status: "Killed" }] }, // 100
|
||||
"src/empty.ts": { mutants: [{ status: "NoCoverage" }] }, // null -> omitted
|
||||
},
|
||||
};
|
||||
const scores = measureMutationScores(report);
|
||||
assert.equal(scores["src/a.ts"], 50);
|
||||
assert.equal(scores["src/b.ts"], 100);
|
||||
assert.equal("src/empty.ts" in scores, false);
|
||||
});
|
||||
|
||||
test("measureMutationScores accepts several reports (per-batch) and unions them", () => {
|
||||
const c = { files: { "src/a.ts": { mutants: [{ status: "Killed" }, { status: "Survived" }] } } };
|
||||
const g = { files: { "src/b.ts": { mutants: [{ status: "Killed" }] } } };
|
||||
const scores = measureMutationScores([c, g]);
|
||||
assert.equal(scores["src/a.ts"], 50);
|
||||
assert.equal(scores["src/b.ts"], 100);
|
||||
});
|
||||
|
||||
// ── readBaselineMutationScores: graceful skip when the file/keys are absent ──
|
||||
test("readBaselineMutationScores returns {} when the baseline file is missing", () => {
|
||||
assert.deepEqual(readBaselineMutationScores("/no/such/baseline.json"), {});
|
||||
});
|
||||
|
||||
test("readBaselineMutationScores extracts mutationScore.<path> metric values", () => {
|
||||
// Write a tiny baseline to a temp file and read it back.
|
||||
const tmp = path.join(os.tmpdir(), `mut-baseline-${process.pid}.json`);
|
||||
fs.writeFileSync(
|
||||
tmp,
|
||||
JSON.stringify({
|
||||
metrics: {
|
||||
eslintWarnings: { value: 10, direction: "down" },
|
||||
"mutationScore.src/a.ts": { value: 70, direction: "up", dedicatedGate: true },
|
||||
"mutationScore.src/b.ts": { value: 80, direction: "up", dedicatedGate: true },
|
||||
},
|
||||
})
|
||||
);
|
||||
const base = readBaselineMutationScores(tmp);
|
||||
assert.deepEqual(base, { "src/a.ts": 70, "src/b.ts": 80 });
|
||||
fs.rmSync(tmp, { force: true });
|
||||
});
|
||||
119
tests/unit/build/mutation-radiography.test.ts
Normal file
119
tests/unit/build/mutation-radiography.test.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import {
|
||||
classifyTestFiles,
|
||||
aggregateRadiography,
|
||||
classifyFromCounts,
|
||||
} from "../../../scripts/quality/mutation-radiography.mjs";
|
||||
|
||||
// ── classifyTestFiles: the plan's canonical fixture ──────────────────────────
|
||||
// 2 testFiles that kill mutants + 1 that kills nothing.
|
||||
// m1 killed ONLY by A -> A gets a unique kill
|
||||
// m2 killed by A AND B -> both get a shared kill
|
||||
// m3 survived -> nobody
|
||||
// Universe = [A, B, C]; C never appears in any killedBy -> empty.
|
||||
test("classifies unique / redundant / empty test files from killedBy (file-name killedBy)", () => {
|
||||
const report = {
|
||||
files: {
|
||||
"open-sse/utils/error.ts": {
|
||||
mutants: [
|
||||
{ id: "m1", status: "Killed", killedBy: ["A.test.ts"] },
|
||||
{ id: "m2", status: "Killed", killedBy: ["A.test.ts", "B.test.ts"] },
|
||||
{ id: "m3", status: "Survived", killedBy: [] },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
const allTestFiles = ["A.test.ts", "B.test.ts", "C.test.ts"];
|
||||
const r = classifyTestFiles(report, allTestFiles);
|
||||
assert.equal(r["A.test.ts"].class, "unique"); // mata m1 sozinho
|
||||
assert.equal(r["A.test.ts"].uniqueKills, 1);
|
||||
assert.equal(r["A.test.ts"].sharedKills, 1);
|
||||
assert.equal(r["B.test.ts"].class, "redundant"); // só m2 (compartilhado)
|
||||
assert.equal(r["B.test.ts"].uniqueKills, 0);
|
||||
assert.equal(r["C.test.ts"].class, "empty"); // não mata nada
|
||||
});
|
||||
|
||||
// ── id resolution: real Stryker tap-runner reports use numeric test ids in
|
||||
// killedBy and a testFiles{} section mapping id -> file name. ────────────────
|
||||
test("resolves numeric killedBy ids to file names via the testFiles section", () => {
|
||||
const report = {
|
||||
testFiles: {
|
||||
"tests/unit/x.test.ts": { tests: [{ id: "0", name: "tests/unit/x.test.ts" }] },
|
||||
"tests/unit/y.test.ts": { tests: [{ id: "1", name: "tests/unit/y.test.ts" }] },
|
||||
"tests/unit/z.test.ts": { tests: [{ id: "2", name: "tests/unit/z.test.ts" }] },
|
||||
},
|
||||
files: {
|
||||
"src/m.ts": {
|
||||
mutants: [
|
||||
{ id: "m1", status: "Killed", killedBy: ["0"] }, // x alone -> unique
|
||||
{ id: "m2", status: "Killed", killedBy: ["0", "1"] }, // x + y shared
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
// allTestFiles defaults to the testFiles keys when omitted.
|
||||
const r = classifyTestFiles(report);
|
||||
assert.equal(r["tests/unit/x.test.ts"].class, "unique");
|
||||
assert.equal(r["tests/unit/y.test.ts"].class, "redundant");
|
||||
assert.equal(r["tests/unit/z.test.ts"].class, "empty");
|
||||
});
|
||||
|
||||
// ── overlapping (🟡): kills ≥1 unique but the majority of its kills are shared.
|
||||
test("classifies overlapping when shared kills outnumber unique kills", () => {
|
||||
const report = {
|
||||
files: {
|
||||
"src/m.ts": {
|
||||
mutants: [
|
||||
{ id: "m1", status: "Killed", killedBy: ["D"] }, // D unique
|
||||
{ id: "m2", status: "Killed", killedBy: ["D", "E"] }, // D shared
|
||||
{ id: "m3", status: "Killed", killedBy: ["D", "E", "F"] }, // D shared
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
const r = classifyTestFiles(report, ["D", "E", "F"]);
|
||||
assert.equal(r["D"].uniqueKills, 1);
|
||||
assert.equal(r["D"].sharedKills, 2);
|
||||
assert.equal(r["D"].class, "overlapping"); // 2 shared > 1 unique
|
||||
assert.equal(r["E"].class, "redundant");
|
||||
assert.equal(r["F"].class, "redundant");
|
||||
});
|
||||
|
||||
// ── classifyFromCounts: the pure threshold helper. ───────────────────────────
|
||||
test("classifyFromCounts applies the threshold rules", () => {
|
||||
assert.equal(classifyFromCounts(0, 0), "empty");
|
||||
assert.equal(classifyFromCounts(0, 3), "redundant");
|
||||
assert.equal(classifyFromCounts(2, 1), "unique"); // shared not > unique
|
||||
assert.equal(classifyFromCounts(1, 1), "unique"); // tie -> unique
|
||||
assert.equal(classifyFromCounts(1, 5), "overlapping"); // shared > unique
|
||||
});
|
||||
|
||||
// ── aggregateRadiography: merge per-batch reports at the FILE level (ids are
|
||||
// per-run, so each report is classified independently then summed). A file can
|
||||
// be empty in one batch but unique in another -> unique overall. ─────────────
|
||||
test("aggregateRadiography sums per-file kills across batches and reclassifies", () => {
|
||||
const batchC = {
|
||||
files: {
|
||||
"src/routeGuard.ts": {
|
||||
mutants: [{ id: "c1", status: "Killed", killedBy: ["A"] }], // A unique here
|
||||
},
|
||||
},
|
||||
};
|
||||
const batchG = {
|
||||
files: {
|
||||
"src/chatCore/x.ts": {
|
||||
mutants: [
|
||||
{ id: "g1", status: "Killed", killedBy: ["A", "B"] }, // A shared, B shared
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
// B only ever shares; A is unique in C and shared in G -> A unique overall.
|
||||
const agg = aggregateRadiography([batchC, batchG], ["A", "B", "C"]);
|
||||
assert.equal(agg["A"].uniqueKills, 1);
|
||||
assert.equal(agg["A"].sharedKills, 1);
|
||||
assert.equal(agg["A"].class, "unique");
|
||||
assert.equal(agg["B"].class, "redundant");
|
||||
assert.equal(agg["C"].class, "empty");
|
||||
});
|
||||
42
tests/unit/card-hover-visible.test.ts
Normal file
42
tests/unit/card-hover-visible.test.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
// Regression guard: provider cards must have a *visible* hover state.
|
||||
//
|
||||
// Reported bug: hovering a provider card made it go "transparent / not
|
||||
// highlighted". Root cause was a 1%-opacity hover (`hover:bg-black/[0.01]` /
|
||||
// `hover:bg-white/[0.01]`) — effectively invisible. The fix switches both card
|
||||
// surfaces to the dashboard's dominant, visible hover (`hover:bg-*/5`) plus a
|
||||
// `hover:border-primary/40` highlight. This pins it so the near-invisible hover
|
||||
// can't silently come back.
|
||||
|
||||
const ROOT = fileURLToPath(new URL("../../", import.meta.url));
|
||||
|
||||
const CARD_FILES = [
|
||||
"src/app/(dashboard)/dashboard/providers/components/ProviderCard.tsx",
|
||||
"src/app/(dashboard)/dashboard/media-providers/[kind]/page.tsx",
|
||||
];
|
||||
|
||||
for (const rel of CARD_FILES) {
|
||||
test(`${rel}: card hover is visible, not near-transparent`, () => {
|
||||
const src = readFileSync(ROOT + rel, "utf8");
|
||||
assert.ok(
|
||||
!src.includes("hover:bg-black/[0.01]"),
|
||||
`${rel} still uses the near-invisible 1% hover (light)`
|
||||
);
|
||||
assert.ok(
|
||||
!src.includes("hover:bg-white/[0.01]"),
|
||||
`${rel} still uses the near-invisible 1% hover (dark)`
|
||||
);
|
||||
assert.ok(
|
||||
src.includes("hover:bg-black/5") && src.includes("hover:bg-white/5"),
|
||||
`${rel} should use a visible hover background (hover:bg-*/5)`
|
||||
);
|
||||
assert.ok(
|
||||
src.includes("hover:border-primary/40"),
|
||||
`${rel} should add a visible hover border highlight`
|
||||
);
|
||||
});
|
||||
}
|
||||
116
tests/unit/catalog-updates-v3829-kimi-qwen.test.ts
Normal file
116
tests/unit/catalog-updates-v3829-kimi-qwen.test.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
// Regression guard for two catalog fixes shipped in v3.8.29:
|
||||
//
|
||||
// 1. Task 1 — moonshotai/kimi-k2.7-code added to the kimi-coding-apikey (kmca)
|
||||
// provider catalog (KIMI_CODING_SHARED.models in open-sse/config/providers/shared.ts).
|
||||
// Requested by @hana189 in discussion #3737.
|
||||
//
|
||||
// 2. Bug #3 (issue #3931) — qwen-web missing from PROVIDER_MODELS_CONFIG in
|
||||
// src/app/api/providers/[id]/models/route.ts, so the model discovery page for
|
||||
// the web-cookie provider returned nothing. Identified by @thezukiru in #3895.
|
||||
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import { getModelsByProviderId } from "../../open-sse/config/providerModels.ts";
|
||||
|
||||
// ── Task 1: kimi-k2.7-code in kmca (kimi-coding-apikey) ───────────────────────
|
||||
|
||||
test("kmca (kimi-coding-apikey) catalog includes moonshotai/kimi-k2.7-code", () => {
|
||||
const models = getModelsByProviderId("kmca");
|
||||
const ids = new Set(models.map((m) => m.id));
|
||||
assert.ok(
|
||||
ids.has("moonshotai/kimi-k2.7-code"),
|
||||
"moonshotai/kimi-k2.7-code missing from kmca — add to KIMI_CODING_SHARED.models in shared.ts"
|
||||
);
|
||||
});
|
||||
|
||||
test("kmca kimi-k2.7-code has 262144 context length and maxOutputTokens", () => {
|
||||
const models = getModelsByProviderId("kmca");
|
||||
const model = models.find((m) => m.id === "moonshotai/kimi-k2.7-code");
|
||||
assert.ok(model, "moonshotai/kimi-k2.7-code not found in kmca catalog");
|
||||
assert.equal(model.contextLength, 262144, "kimi-k2.7-code contextLength must be 262144");
|
||||
assert.equal(model.maxOutputTokens, 262144, "kimi-k2.7-code maxOutputTokens must be 262144");
|
||||
});
|
||||
|
||||
test("kmca still exposes kimi-k2.6 and kimi-k2.6-thinking alongside the new model", () => {
|
||||
const ids = new Set(getModelsByProviderId("kmca").map((m) => m.id));
|
||||
assert.ok(ids.has("kimi-k2.6"), "kimi-k2.6 must still be present");
|
||||
assert.ok(ids.has("kimi-k2.6-thinking"), "kimi-k2.6-thinking must still be present");
|
||||
assert.ok(ids.has("moonshotai/kimi-k2.7-code"), "moonshotai/kimi-k2.7-code must be present");
|
||||
});
|
||||
|
||||
// ── Bug #3 / issue #3931: qwen-web in PROVIDER_MODELS_CONFIG ──────────────────
|
||||
|
||||
const ROUTE_FILE = path.join("src", "app", "api", "providers", "[id]", "models", "route.ts");
|
||||
|
||||
test("PROVIDER_MODELS_CONFIG contains a qwen-web entry (issue #3931 bug #3)", () => {
|
||||
const src = fs.readFileSync(ROUTE_FILE, "utf-8");
|
||||
assert.match(
|
||||
src,
|
||||
/"qwen-web"\s*:/,
|
||||
'"qwen-web" key missing from PROVIDER_MODELS_CONFIG in models/route.ts'
|
||||
);
|
||||
});
|
||||
|
||||
test("qwen-web PROVIDER_MODELS_CONFIG entry targets chat.qwen.ai/api/v2/models", () => {
|
||||
const src = fs.readFileSync(ROUTE_FILE, "utf-8");
|
||||
assert.match(
|
||||
src,
|
||||
/chat\.qwen\.ai\/api\/v2\/models/,
|
||||
"qwen-web discovery URL must be https://chat.qwen.ai/api/v2/models"
|
||||
);
|
||||
});
|
||||
|
||||
test("qwen-web parseResponse handles Qwen nested data.data structure", () => {
|
||||
const mockResponse = {
|
||||
data: {
|
||||
data: [
|
||||
{ id: "qwen3.7-plus", name: "Qwen3.7-Plus", owned_by: "qwen" },
|
||||
{ id: "qwen3-235b-a22b", name: "Qwen3-235B-A22B", owned_by: "qwen" },
|
||||
{ id: "qwen3-coder-480b", name: "Qwen3-Coder-480B" },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// parseResponse logic matches PROVIDER_MODELS_CONFIG["qwen-web"].parseResponse
|
||||
const innerData: Array<Record<string, unknown>> =
|
||||
(mockResponse?.data?.data as Array<Record<string, unknown>>) ||
|
||||
(mockResponse?.data as unknown as Array<Record<string, unknown>>) ||
|
||||
[];
|
||||
const models = innerData
|
||||
.map((item) => ({
|
||||
id: (item.id || item.name) as string,
|
||||
name: (item.name || item.id) as string,
|
||||
owned_by: (item.owned_by || "qwen") as string,
|
||||
}))
|
||||
.filter((m) => m.id);
|
||||
|
||||
assert.equal(models.length, 3);
|
||||
assert.equal(models[0].id, "qwen3.7-plus");
|
||||
assert.equal(models[0].name, "Qwen3.7-Plus");
|
||||
assert.equal(models[0].owned_by, "qwen");
|
||||
assert.equal(models[2].owned_by, "qwen", "owned_by defaults to 'qwen' when absent");
|
||||
});
|
||||
|
||||
test("qwen-web parseResponse handles flat data array fallback", () => {
|
||||
const mockResponse = {
|
||||
data: [{ id: "qwen3.7-plus", name: "Qwen3.7-Plus" }],
|
||||
};
|
||||
|
||||
const innerData: Array<Record<string, unknown>> =
|
||||
(mockResponse?.data as unknown as { data?: Array<Record<string, unknown>> })?.data ||
|
||||
(mockResponse?.data as unknown as Array<Record<string, unknown>>) ||
|
||||
[];
|
||||
const models = innerData
|
||||
.map((item) => ({
|
||||
id: (item.id || item.name) as string,
|
||||
name: (item.name || item.id) as string,
|
||||
owned_by: (item.owned_by || "qwen") as string,
|
||||
}))
|
||||
.filter((m) => m.id);
|
||||
|
||||
assert.equal(models.length, 1);
|
||||
assert.equal(models[0].id, "qwen3.7-plus");
|
||||
});
|
||||
171
tests/unit/chatcore-executor-helpers.test.ts
Normal file
171
tests/unit/chatcore-executor-helpers.test.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
resolveAccountSemaphoreKey,
|
||||
resolveAccountSemaphoreAccountKey,
|
||||
resolveAccountSemaphoreMaxConcurrency,
|
||||
buildClaudePromptCacheLogMeta,
|
||||
} from "../../open-sse/handlers/chatCore/executorHelpers.ts";
|
||||
import { FORMATS } from "../../open-sse/translator/formats.ts";
|
||||
|
||||
test("resolveAccountSemaphoreAccountKey prefers an explicit non-blank connectionId", () => {
|
||||
assert.equal(resolveAccountSemaphoreAccountKey("conn-1", { id: "ignored" }), "conn-1");
|
||||
});
|
||||
|
||||
test("resolveAccountSemaphoreAccountKey falls back through credential candidates in order", () => {
|
||||
// blank connectionId -> first non-blank candidate (connectionId field) wins
|
||||
assert.equal(
|
||||
resolveAccountSemaphoreAccountKey(" ", { connectionId: "cred-conn", id: "id-x" }),
|
||||
"cred-conn"
|
||||
);
|
||||
// candidate order: connectionId, id, email, name, displayName
|
||||
assert.equal(resolveAccountSemaphoreAccountKey(null, { id: " id-x " }), "id-x");
|
||||
assert.equal(resolveAccountSemaphoreAccountKey(null, { email: "a@b.co" }), "a@b.co");
|
||||
assert.equal(resolveAccountSemaphoreAccountKey(null, { name: "the-name" }), "the-name");
|
||||
assert.equal(resolveAccountSemaphoreAccountKey(null, { displayName: "Disp" }), "Disp");
|
||||
});
|
||||
|
||||
test("resolveAccountSemaphoreAccountKey returns null when nothing usable is present", () => {
|
||||
assert.equal(resolveAccountSemaphoreAccountKey(null, null), null);
|
||||
assert.equal(resolveAccountSemaphoreAccountKey(undefined, undefined), null);
|
||||
assert.equal(resolveAccountSemaphoreAccountKey("", {}), null);
|
||||
// non-string / blank candidates are all rejected
|
||||
assert.equal(resolveAccountSemaphoreAccountKey("", { id: 123, email: " " } as unknown as Record<string, unknown>), null);
|
||||
});
|
||||
|
||||
test("resolveAccountSemaphoreMaxConcurrency parses finite numbers and numeric strings", () => {
|
||||
// number passthrough
|
||||
assert.equal(resolveAccountSemaphoreMaxConcurrency({ maxConcurrent: 4 }), 4);
|
||||
assert.equal(resolveAccountSemaphoreMaxConcurrency({ maxConcurrent: 0 }), 0);
|
||||
assert.equal(resolveAccountSemaphoreMaxConcurrency({ maxConcurrent: -2 }), -2);
|
||||
// numeric string is coerced
|
||||
assert.equal(resolveAccountSemaphoreMaxConcurrency({ maxConcurrent: "8" }), 8);
|
||||
assert.equal(resolveAccountSemaphoreMaxConcurrency({ maxConcurrent: " 3.5 " }), 3.5);
|
||||
});
|
||||
|
||||
test("resolveAccountSemaphoreMaxConcurrency rejects non-finite / non-numeric / missing values", () => {
|
||||
// exercises the private toFiniteNumberOrNull null branches indirectly
|
||||
assert.equal(resolveAccountSemaphoreMaxConcurrency({ maxConcurrent: Infinity }), null);
|
||||
assert.equal(resolveAccountSemaphoreMaxConcurrency({ maxConcurrent: NaN }), null);
|
||||
assert.equal(resolveAccountSemaphoreMaxConcurrency({ maxConcurrent: "abc" }), null);
|
||||
assert.equal(resolveAccountSemaphoreMaxConcurrency({ maxConcurrent: "" }), null);
|
||||
assert.equal(resolveAccountSemaphoreMaxConcurrency({ maxConcurrent: " " }), null);
|
||||
assert.equal(resolveAccountSemaphoreMaxConcurrency({ maxConcurrent: true } as unknown as Record<string, unknown>), null);
|
||||
assert.equal(resolveAccountSemaphoreMaxConcurrency({}), null);
|
||||
assert.equal(resolveAccountSemaphoreMaxConcurrency(null), null);
|
||||
});
|
||||
|
||||
test("resolveAccountSemaphoreKey builds provider:accountKey when both resolve", () => {
|
||||
assert.equal(
|
||||
resolveAccountSemaphoreKey({
|
||||
provider: "openai",
|
||||
model: "gpt-4o",
|
||||
connectionId: "conn-9",
|
||||
credentials: null,
|
||||
}),
|
||||
"openai:conn-9"
|
||||
);
|
||||
// accountKey can come from credentials when connectionId is blank
|
||||
assert.equal(
|
||||
resolveAccountSemaphoreKey({
|
||||
provider: "anthropic",
|
||||
model: "claude",
|
||||
connectionId: null,
|
||||
credentials: { email: "user@x.io" },
|
||||
}),
|
||||
"anthropic:user@x.io"
|
||||
);
|
||||
});
|
||||
|
||||
test("resolveAccountSemaphoreKey returns null without a provider or account key", () => {
|
||||
// no account key resolvable
|
||||
assert.equal(
|
||||
resolveAccountSemaphoreKey({ provider: "openai", model: "m", connectionId: null, credentials: null }),
|
||||
null
|
||||
);
|
||||
// account key resolves but provider missing
|
||||
assert.equal(
|
||||
resolveAccountSemaphoreKey({ provider: null, model: "m", connectionId: "conn", credentials: null }),
|
||||
null
|
||||
);
|
||||
assert.equal(
|
||||
resolveAccountSemaphoreKey({ provider: "", model: "m", connectionId: "conn", credentials: null }),
|
||||
null
|
||||
);
|
||||
});
|
||||
|
||||
test("buildClaudePromptCacheLogMeta returns null for non-Claude format or non-object body", () => {
|
||||
assert.equal(buildClaudePromptCacheLogMeta(FORMATS.OPENAI, { system: [] }, null), null);
|
||||
assert.equal(buildClaudePromptCacheLogMeta(FORMATS.CLAUDE, null, null), null);
|
||||
assert.equal(
|
||||
buildClaudePromptCacheLogMeta(FORMATS.CLAUDE, "x" as unknown as Record<string, unknown>, null),
|
||||
null
|
||||
);
|
||||
});
|
||||
|
||||
test("buildClaudePromptCacheLogMeta returns null when there are no breakpoints and no beta header", () => {
|
||||
const body = {
|
||||
system: [{ text: "plain system, no cache_control" }],
|
||||
tools: [{ name: "t1" }],
|
||||
messages: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
||||
};
|
||||
assert.equal(buildClaudePromptCacheLogMeta(FORMATS.CLAUDE, body, null), null);
|
||||
});
|
||||
|
||||
test("buildClaudePromptCacheLogMeta counts cache_control breakpoints across system/tools/messages", () => {
|
||||
const body = {
|
||||
system: [
|
||||
{ text: "billing", cache_control: { type: "ephemeral", ttl: "5m" } },
|
||||
// billing header lines are skipped entirely
|
||||
{ text: "x-anthropic-billing-header: abc", cache_control: { type: "ephemeral" } },
|
||||
],
|
||||
tools: [{ name: "lookup", cache_control: { type: "ephemeral" } }],
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: "q", cache_control: { type: "ephemeral" } },
|
||||
{ type: "text", text: "no cache here" },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const meta = buildClaudePromptCacheLogMeta(FORMATS.CLAUDE, body, null);
|
||||
assert.ok(meta);
|
||||
// 1 system (the billing-header one is skipped) + 1 tool + 1 message = 3
|
||||
assert.equal(meta.totalBreakpoints, 3);
|
||||
assert.equal(meta.applied, true);
|
||||
assert.equal(meta.systemBreakpoints.length, 1);
|
||||
assert.equal(meta.systemBreakpoints[0].ttl, "5m");
|
||||
assert.equal(meta.systemBreakpoints[0].index, 0);
|
||||
assert.equal(meta.toolBreakpoints.length, 1);
|
||||
assert.equal(meta.toolBreakpoints[0].name, "lookup");
|
||||
assert.equal(meta.messageBreakpoints.length, 1);
|
||||
assert.equal(meta.messageBreakpoints[0].role, "user");
|
||||
assert.equal(meta.messageBreakpoints[0].blockType, "text");
|
||||
// default type when cache_control.type missing/blank is "ephemeral"
|
||||
assert.equal(meta.messageBreakpoints[0].type, "ephemeral");
|
||||
});
|
||||
|
||||
test("buildClaudePromptCacheLogMeta surfaces the Anthropic-Beta header even with zero breakpoints", () => {
|
||||
const body = { system: [{ text: "plain" }] };
|
||||
// provider header present -> meta returned, applied=false (no breakpoints)
|
||||
const fromProvider = buildClaudePromptCacheLogMeta(FORMATS.CLAUDE, body, {
|
||||
"anthropic-beta": "prompt-caching-2024-07-31",
|
||||
});
|
||||
assert.ok(fromProvider);
|
||||
assert.equal(fromProvider.applied, false);
|
||||
assert.equal(fromProvider.totalBreakpoints, 0);
|
||||
assert.equal(fromProvider.anthropicBeta, "prompt-caching-2024-07-31");
|
||||
|
||||
// falls back to client headers when provider header absent
|
||||
const fromClient = buildClaudePromptCacheLogMeta(
|
||||
FORMATS.CLAUDE,
|
||||
body,
|
||||
null,
|
||||
new Headers({ "Anthropic-Beta": "client-beta" })
|
||||
);
|
||||
assert.ok(fromClient);
|
||||
assert.equal(fromClient.anthropicBeta, "client-beta");
|
||||
});
|
||||
48
tests/unit/chatcore-headers.test.ts
Normal file
48
tests/unit/chatcore-headers.test.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { getHeaderValueCaseInsensitive } from "../../open-sse/handlers/chatCore/headers.ts";
|
||||
|
||||
test("getHeaderValueCaseInsensitive reads a Headers instance via .get()", () => {
|
||||
const h = new Headers({ "Content-Type": "text/event-stream" });
|
||||
// Headers.get is itself case-insensitive, so a lowercase lookup hits the value.
|
||||
assert.equal(getHeaderValueCaseInsensitive(h, "content-type"), "text/event-stream");
|
||||
assert.equal(getHeaderValueCaseInsensitive(h, "Content-Type"), "text/event-stream");
|
||||
// Missing header on a Headers instance returns null (Headers.get contract).
|
||||
assert.equal(getHeaderValueCaseInsensitive(h, "x-missing"), null);
|
||||
});
|
||||
|
||||
test("getHeaderValueCaseInsensitive matches plain-object keys case-insensitively", () => {
|
||||
const obj = { Accept: "text/event-stream", "X-Foo": "bar" };
|
||||
assert.equal(getHeaderValueCaseInsensitive(obj, "accept"), "text/event-stream");
|
||||
assert.equal(getHeaderValueCaseInsensitive(obj, "ACCEPT"), "text/event-stream");
|
||||
assert.equal(getHeaderValueCaseInsensitive(obj, "x-foo"), "bar");
|
||||
});
|
||||
|
||||
test("getHeaderValueCaseInsensitive trims plain-object string values", () => {
|
||||
assert.equal(getHeaderValueCaseInsensitive({ Accept: " v " }, "accept"), "v");
|
||||
});
|
||||
|
||||
test("getHeaderValueCaseInsensitive ignores blank and non-string plain-object values", () => {
|
||||
// whitespace-only value: value.trim() is falsy -> skipped -> null
|
||||
assert.equal(getHeaderValueCaseInsensitive({ Accept: " " }, "accept"), null);
|
||||
// empty string -> skipped -> null
|
||||
assert.equal(getHeaderValueCaseInsensitive({ Accept: "" }, "accept"), null);
|
||||
// non-string values are not strings -> skipped -> null
|
||||
assert.equal(getHeaderValueCaseInsensitive({ "Content-Length": 42 }, "content-length"), null);
|
||||
assert.equal(getHeaderValueCaseInsensitive({ Flag: true }, "flag"), null);
|
||||
});
|
||||
|
||||
test("getHeaderValueCaseInsensitive returns null for missing key on plain object", () => {
|
||||
assert.equal(getHeaderValueCaseInsensitive({ Accept: "x" }, "missing"), null);
|
||||
});
|
||||
|
||||
test("getHeaderValueCaseInsensitive returns null for null/undefined/non-object inputs", () => {
|
||||
assert.equal(getHeaderValueCaseInsensitive(null, "accept"), null);
|
||||
assert.equal(getHeaderValueCaseInsensitive(undefined, "accept"), null);
|
||||
// a non-object (typeof !== "object") short-circuits to null
|
||||
assert.equal(
|
||||
getHeaderValueCaseInsensitive("text/event-stream" as unknown as Record<string, unknown>, "accept"),
|
||||
null
|
||||
);
|
||||
});
|
||||
151
tests/unit/chatcore-log-truncation.test.ts
Normal file
151
tests/unit/chatcore-log-truncation.test.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
capMemoryExtractionText,
|
||||
truncateChatLogText,
|
||||
cloneBoundedChatLogPayload,
|
||||
truncateForLog,
|
||||
MEMORY_EXTRACTION_TEXT_LIMIT,
|
||||
} from "../../open-sse/handlers/chatCore/logTruncation.ts";
|
||||
import {
|
||||
getChatLogTextLimit,
|
||||
getChatLogArrayTailItems,
|
||||
getChatLogMaxDepth,
|
||||
getChatLogMaxObjectKeys,
|
||||
} from "../../src/lib/logEnv.ts";
|
||||
|
||||
test("MEMORY_EXTRACTION_TEXT_LIMIT is the documented 64KB constant", () => {
|
||||
assert.equal(MEMORY_EXTRACTION_TEXT_LIMIT, 64 * 1024);
|
||||
});
|
||||
|
||||
test("capMemoryExtractionText returns short strings unchanged", () => {
|
||||
assert.equal(capMemoryExtractionText("hello"), "hello");
|
||||
// exactly at the limit is not truncated (<= limit)
|
||||
const exact = "x".repeat(MEMORY_EXTRACTION_TEXT_LIMIT);
|
||||
assert.equal(capMemoryExtractionText(exact), exact);
|
||||
});
|
||||
|
||||
test("capMemoryExtractionText keeps the trailing window of over-limit strings", () => {
|
||||
const long = "a".repeat(MEMORY_EXTRACTION_TEXT_LIMIT) + "TAIL";
|
||||
const capped = capMemoryExtractionText(long);
|
||||
assert.equal(capped.length, MEMORY_EXTRACTION_TEXT_LIMIT);
|
||||
// slice(-LIMIT) keeps the end, so the appended TAIL survives.
|
||||
assert.ok(capped.endsWith("TAIL"));
|
||||
});
|
||||
|
||||
test("truncateChatLogText passes through strings up to the configured limit", () => {
|
||||
const limit = getChatLogTextLimit();
|
||||
const under = "y".repeat(limit);
|
||||
assert.equal(truncateChatLogText(under), under);
|
||||
});
|
||||
|
||||
test("truncateChatLogText builds head + marker + tail for over-limit strings", () => {
|
||||
const limit = getChatLogTextLimit();
|
||||
const head = "H".repeat(Math.floor(limit / 2));
|
||||
const tail = "T".repeat(Math.ceil(limit / 2));
|
||||
// make the total strictly larger than the limit by inserting a middle chunk
|
||||
const middle = "M".repeat(500);
|
||||
const value = head + middle + tail;
|
||||
const out = truncateChatLogText(value);
|
||||
const expectedHead = value.slice(0, Math.floor(limit / 2));
|
||||
const expectedTail = value.slice(-Math.ceil(limit / 2));
|
||||
assert.equal(
|
||||
out,
|
||||
`${expectedHead}\n[...truncated ${value.length - limit} chars...]\n${expectedTail}`
|
||||
);
|
||||
// sanity: the marker reports exactly the number of dropped chars
|
||||
assert.ok(out.includes(`[...truncated ${value.length - limit} chars...]`));
|
||||
});
|
||||
|
||||
test("cloneBoundedChatLogPayload returns null/undefined/primitives as-is", () => {
|
||||
assert.equal(cloneBoundedChatLogPayload(null), null);
|
||||
assert.equal(cloneBoundedChatLogPayload(undefined), undefined);
|
||||
assert.equal(cloneBoundedChatLogPayload(42), 42);
|
||||
assert.equal(cloneBoundedChatLogPayload(true), true);
|
||||
});
|
||||
|
||||
test("cloneBoundedChatLogPayload truncates long string leaves via truncateChatLogText", () => {
|
||||
const limit = getChatLogTextLimit();
|
||||
const big = "z".repeat(limit + 1000);
|
||||
const out = cloneBoundedChatLogPayload(big) as string;
|
||||
assert.equal(out, truncateChatLogText(big));
|
||||
assert.ok(out.includes("[...truncated"));
|
||||
});
|
||||
|
||||
test("cloneBoundedChatLogPayload tail-truncates long arrays with a marker entry", () => {
|
||||
const maxTail = getChatLogArrayTailItems();
|
||||
const n = maxTail + 100;
|
||||
const cloned = cloneBoundedChatLogPayload(new Array(n).fill("a")) as unknown[];
|
||||
// marker prepended + the retained tail items
|
||||
assert.equal(cloned.length, maxTail + 1);
|
||||
const marker = cloned[0] as Record<string, unknown>;
|
||||
assert.equal(marker._omniroute_truncated_array, true);
|
||||
assert.equal(marker.originalLength, n);
|
||||
assert.equal(marker.retainedTailItems, maxTail);
|
||||
});
|
||||
|
||||
test("cloneBoundedChatLogPayload leaves short arrays unmarked", () => {
|
||||
const cloned = cloneBoundedChatLogPayload(["a", "b", "c"]) as unknown[];
|
||||
assert.deepEqual(cloned, ["a", "b", "c"]);
|
||||
});
|
||||
|
||||
test("cloneBoundedChatLogPayload caps object keys and records the dropped count", () => {
|
||||
const maxKeys = getChatLogMaxObjectKeys();
|
||||
const obj: Record<string, number> = {};
|
||||
for (let i = 0; i < maxKeys + 5; i += 1) obj[`k${i}`] = i;
|
||||
const cloned = cloneBoundedChatLogPayload(obj) as Record<string, unknown>;
|
||||
// first maxKeys keys retained + the synthetic _omniroute_truncated_keys field
|
||||
assert.equal(cloned._omniroute_truncated_keys, 5);
|
||||
assert.equal(cloned.k0, 0);
|
||||
assert.equal(cloned[`k${maxKeys - 1}`], maxKeys - 1);
|
||||
// a key beyond the cap is dropped
|
||||
assert.equal(cloned[`k${maxKeys}`], undefined);
|
||||
});
|
||||
|
||||
test("cloneBoundedChatLogPayload stops at max depth with a [MaxDepth] sentinel", () => {
|
||||
const depth = getChatLogMaxDepth();
|
||||
// build nesting that goes one level past the cap
|
||||
let leaf: Record<string, unknown> = { deep: "value" };
|
||||
for (let i = 0; i < depth + 1; i += 1) leaf = { nested: leaf };
|
||||
const cloned = cloneBoundedChatLogPayload(leaf) as Record<string, unknown>;
|
||||
// walk down `depth` levels; the level at >= maxDepth becomes the sentinel string
|
||||
let cursor: unknown = cloned;
|
||||
for (let i = 0; i < depth; i += 1) {
|
||||
cursor = (cursor as Record<string, unknown>).nested;
|
||||
}
|
||||
assert.equal(cursor, "[MaxDepth]");
|
||||
});
|
||||
|
||||
test("truncateForLog returns null/undefined and non-object primitives unchanged", () => {
|
||||
assert.equal(truncateForLog(null), null);
|
||||
assert.equal(truncateForLog(undefined), undefined);
|
||||
assert.equal(truncateForLog("hi" as unknown), "hi" as unknown);
|
||||
});
|
||||
|
||||
test("truncateForLog passes small objects through untouched", () => {
|
||||
const small = { model: "gpt-4o", messages: [{ role: "user", content: "hi" }] };
|
||||
assert.equal(truncateForLog(small), small);
|
||||
});
|
||||
|
||||
test("truncateForLog summarizes oversized payloads instead of cloning", () => {
|
||||
const huge = {
|
||||
model: "gpt-4o",
|
||||
provider: "openai",
|
||||
stream: true,
|
||||
// distinct object references so estimateSizeFast (WeakSet-dedup) counts each one
|
||||
messages: Array.from({ length: 50000 }, () => ({ role: "user", content: "x".repeat(64) })),
|
||||
contents: [{ a: 1 }],
|
||||
};
|
||||
const summary = truncateForLog(huge) as Record<string, unknown>;
|
||||
assert.equal(summary._truncated, true);
|
||||
assert.equal(typeof summary._originalBytes, "number");
|
||||
assert.ok((summary._originalBytes as number) > 8 * 1024);
|
||||
assert.equal(summary.model, "gpt-4o");
|
||||
assert.equal(summary.provider, "openai");
|
||||
assert.equal(summary.messageCount, 50000);
|
||||
assert.equal(summary.contentCount, 1);
|
||||
assert.equal(summary.stream, true);
|
||||
// the original (huge) is NOT returned — it is a fresh summary object
|
||||
assert.notEqual(summary, huge);
|
||||
});
|
||||
136
tests/unit/chatcore-memory-extraction.test.ts
Normal file
136
tests/unit/chatcore-memory-extraction.test.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
extractMemoryTextFromResponse,
|
||||
extractMemoryTextFromRequestBody,
|
||||
resolveMemoryOwnerId,
|
||||
} from "../../open-sse/handlers/chatCore/memoryExtraction.ts";
|
||||
|
||||
test("extractMemoryTextFromResponse reads OpenAI choices[0].message.content (trimmed)", () => {
|
||||
assert.equal(
|
||||
extractMemoryTextFromResponse({ choices: [{ message: { content: " hi " } }] }),
|
||||
"hi"
|
||||
);
|
||||
});
|
||||
|
||||
test("extractMemoryTextFromResponse joins Claude content text blocks and skips non-text", () => {
|
||||
assert.equal(
|
||||
extractMemoryTextFromResponse({
|
||||
content: [
|
||||
{ type: "text", text: " a " },
|
||||
{ type: "image" },
|
||||
{ type: "text", text: "b" },
|
||||
],
|
||||
}),
|
||||
"a\nb"
|
||||
);
|
||||
});
|
||||
|
||||
test("extractMemoryTextFromResponse falls back to Responses output_text", () => {
|
||||
assert.equal(extractMemoryTextFromResponse({ output_text: " out " }), "out");
|
||||
});
|
||||
|
||||
test("extractMemoryTextFromResponse returns empty string for null/empty/no-text shapes", () => {
|
||||
assert.equal(extractMemoryTextFromResponse(null), "");
|
||||
assert.equal(extractMemoryTextFromResponse(undefined), "");
|
||||
assert.equal(extractMemoryTextFromResponse({}), "");
|
||||
// content array with no text parts -> contentText is "" -> falls through to ""
|
||||
assert.equal(extractMemoryTextFromResponse({ content: [{ type: "image" }] }), "");
|
||||
});
|
||||
|
||||
test("extractMemoryTextFromResponse prefers OpenAI content over output_text", () => {
|
||||
assert.equal(
|
||||
extractMemoryTextFromResponse({
|
||||
choices: [{ message: { content: "openai" } }],
|
||||
output_text: "responses",
|
||||
}),
|
||||
"openai"
|
||||
);
|
||||
});
|
||||
|
||||
test("extractMemoryTextFromRequestBody returns the LAST user message (string content)", () => {
|
||||
const body = {
|
||||
messages: [
|
||||
{ role: "user", content: "first" },
|
||||
{ role: "assistant", content: "ignored" },
|
||||
{ role: "user", content: "second" },
|
||||
],
|
||||
};
|
||||
assert.equal(extractMemoryTextFromRequestBody(body), "second");
|
||||
});
|
||||
|
||||
test("extractMemoryTextFromRequestBody joins array content parts of the last user message", () => {
|
||||
const body = {
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "input_text", text: " a " },
|
||||
{ text: "b" },
|
||||
{ type: "image_url" },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
assert.equal(extractMemoryTextFromRequestBody(body), "a\nb");
|
||||
});
|
||||
|
||||
test("extractMemoryTextFromRequestBody reads Responses-style input items", () => {
|
||||
const inputBody = {
|
||||
input: [{ role: "user", type: "message", content: [{ type: "input_text", text: "hey" }] }],
|
||||
};
|
||||
assert.equal(extractMemoryTextFromRequestBody(inputBody), "hey");
|
||||
});
|
||||
|
||||
test("extractMemoryTextFromRequestBody reads a string-content input item", () => {
|
||||
const inputBody = {
|
||||
input: [{ role: "user", type: "message", content: " plain " }],
|
||||
};
|
||||
assert.equal(extractMemoryTextFromRequestBody(inputBody), "plain");
|
||||
});
|
||||
|
||||
test("extractMemoryTextFromRequestBody scans input items from the end and returns the last user text", () => {
|
||||
// The primary input scan walks from the end and returns immediately on the
|
||||
// last item whose role is user (or unset) and type is message (or unset).
|
||||
const inputBody = {
|
||||
input: [
|
||||
{ type: "reasoning", content: "thinking" }, // itemType !== "message" -> skipped
|
||||
{ role: "user", content: "alpha" }, // matches, but an earlier item from the end wins
|
||||
{ role: "user", content: "beta" }, // last matching item -> returned
|
||||
],
|
||||
};
|
||||
assert.equal(extractMemoryTextFromRequestBody(inputBody), "beta");
|
||||
});
|
||||
|
||||
test("extractMemoryTextFromRequestBody skips assistant input items but accepts the prior user item", () => {
|
||||
const inputBody = {
|
||||
input: [
|
||||
{ role: "user", type: "message", content: "the question" },
|
||||
{ role: "assistant", type: "message", content: "the answer" }, // role !== user -> skipped
|
||||
],
|
||||
};
|
||||
assert.equal(extractMemoryTextFromRequestBody(inputBody), "the question");
|
||||
});
|
||||
|
||||
test("extractMemoryTextFromRequestBody returns empty for null/empty/no-user bodies", () => {
|
||||
assert.equal(extractMemoryTextFromRequestBody(null), "");
|
||||
assert.equal(extractMemoryTextFromRequestBody(undefined), "");
|
||||
assert.equal(extractMemoryTextFromRequestBody({}), "");
|
||||
// only an assistant message -> no user text
|
||||
assert.equal(
|
||||
extractMemoryTextFromRequestBody({ messages: [{ role: "assistant", content: "x" }] }),
|
||||
""
|
||||
);
|
||||
});
|
||||
|
||||
test("resolveMemoryOwnerId returns the id when present, null otherwise", () => {
|
||||
assert.equal(resolveMemoryOwnerId({ id: "key_123" }), "key_123");
|
||||
// whitespace-only id is rejected
|
||||
assert.equal(resolveMemoryOwnerId({ id: " " }), null);
|
||||
// non-string id is rejected
|
||||
assert.equal(resolveMemoryOwnerId({ id: 7 } as unknown as Record<string, unknown>), null);
|
||||
// missing id / null info
|
||||
assert.equal(resolveMemoryOwnerId({}), null);
|
||||
assert.equal(resolveMemoryOwnerId(null), null);
|
||||
});
|
||||
128
tests/unit/chatcore-memory-skills-injection.test.ts
Normal file
128
tests/unit/chatcore-memory-skills-injection.test.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
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";
|
||||
|
||||
// Isolated DATA_DIR set BEFORE importing anything that touches the DB
|
||||
// (injectMemoryAndSkills -> getMemorySettings / retrieveMemories / injectSkills).
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-mem-skills-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
|
||||
const { getSkillsProviderForFormat, injectMemoryAndSkills } = await import(
|
||||
"../../open-sse/handlers/chatCore/memorySkillsInjection.ts"
|
||||
);
|
||||
const { FORMATS } = await import("../../open-sse/translator/formats.ts");
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
|
||||
test.after(() => {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
// ─── getSkillsProviderForFormat (pure switch) ────────────────────────────────
|
||||
|
||||
test("getSkillsProviderForFormat maps CLAUDE -> anthropic", () => {
|
||||
assert.equal(getSkillsProviderForFormat(FORMATS.CLAUDE), "anthropic");
|
||||
});
|
||||
|
||||
test("getSkillsProviderForFormat maps GEMINI -> google", () => {
|
||||
assert.equal(getSkillsProviderForFormat(FORMATS.GEMINI), "google");
|
||||
});
|
||||
|
||||
test("getSkillsProviderForFormat maps OPENAI and any unknown format -> openai (default)", () => {
|
||||
assert.equal(getSkillsProviderForFormat(FORMATS.OPENAI), "openai");
|
||||
// any other / unknown format falls through to the default branch
|
||||
assert.equal(getSkillsProviderForFormat("gemini-cli"), "openai");
|
||||
assert.equal(getSkillsProviderForFormat("codex"), "openai");
|
||||
assert.equal(getSkillsProviderForFormat("totally-unknown"), "openai");
|
||||
assert.equal(getSkillsProviderForFormat(""), "openai");
|
||||
});
|
||||
|
||||
// ─── injectMemoryAndSkills ───────────────────────────────────────────────────
|
||||
|
||||
test("injectMemoryAndSkills with memoryOwnerId=null skips both branches and returns the body unchanged", async () => {
|
||||
// memoryOwnerId is null -> memorySettings stays null -> the memory guard is false
|
||||
// (no getMemorySettings/retrieveMemories) AND the skills guard (memorySettings?.skillsEnabled)
|
||||
// is false. The body is returned verbatim with memorySettings=null.
|
||||
const body: Record<string, unknown> = {
|
||||
model: "gpt-4o",
|
||||
messages: [{ role: "user", content: "hello world" }],
|
||||
};
|
||||
|
||||
const result = await injectMemoryAndSkills({
|
||||
body,
|
||||
memoryOwnerId: null,
|
||||
provider: "openai",
|
||||
effectiveModel: "gpt-4o",
|
||||
sourceFormat: FORMATS.OPENAI,
|
||||
targetFormat: FORMATS.OPENAI,
|
||||
backgroundReason: null,
|
||||
log: null,
|
||||
});
|
||||
|
||||
assert.equal(result.memorySettings, null, "memorySettings is null when no owner is provided");
|
||||
// body is returned as-is (same reference, no injection happened)
|
||||
assert.equal(result.body, body);
|
||||
assert.deepEqual(result.body.messages, [{ role: "user", content: "hello world" }]);
|
||||
assert.equal("tools" in result.body, false, "no skills were injected");
|
||||
});
|
||||
|
||||
test("injectMemoryAndSkills with an empty DB resolves settings, finds nothing to inject, returns body unchanged", async () => {
|
||||
// memoryOwnerId is set -> getMemorySettings() resolves DB defaults (enabled, skillsEnabled).
|
||||
// The body has NO `messages` array (only `input`), so shouldInjectMemory() returns false and
|
||||
// the memory-retrieval branch is skipped. The skills branch runs injectSkills(), but the
|
||||
// empty DB registry has no skills, so mergedTools.length == existingTools.length and the body
|
||||
// is NOT cloned/mutated. This exercises the realistic "nothing to inject" path end-to-end.
|
||||
const log = {
|
||||
debug: (..._args: unknown[]) => {
|
||||
/* swallow */
|
||||
},
|
||||
};
|
||||
const body: Record<string, unknown> = {
|
||||
model: "gpt-4o",
|
||||
input: [{ role: "user", content: "no messages array here" }],
|
||||
};
|
||||
|
||||
const result = await injectMemoryAndSkills({
|
||||
body,
|
||||
memoryOwnerId: "owner-empty-db",
|
||||
provider: "openai",
|
||||
effectiveModel: "gpt-4o",
|
||||
sourceFormat: FORMATS.OPENAI,
|
||||
targetFormat: FORMATS.OPENAI,
|
||||
backgroundReason: null,
|
||||
log,
|
||||
});
|
||||
|
||||
// memorySettings was resolved (defaults) — it is a real object, not null.
|
||||
assert.ok(result.memorySettings, "memorySettings resolved from DB defaults");
|
||||
assert.equal(result.memorySettings.enabled, true);
|
||||
assert.equal(result.memorySettings.skillsEnabled, true);
|
||||
// No skills in the empty registry -> body returned unchanged (same reference).
|
||||
assert.equal(result.body, body);
|
||||
assert.equal("tools" in result.body, false, "no skills injected from an empty registry");
|
||||
});
|
||||
|
||||
test("injectMemoryAndSkills resolves cleanly for a CLAUDE-format body with no owner (provider-mapping path)", async () => {
|
||||
// Characterizes the no-owner short-circuit for a non-OpenAI source format. Nothing is
|
||||
// injected; the function just returns the body untouched and memorySettings=null.
|
||||
const body: Record<string, unknown> = {
|
||||
model: "claude-3-5-sonnet",
|
||||
messages: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
|
||||
};
|
||||
|
||||
const result = await injectMemoryAndSkills({
|
||||
body,
|
||||
memoryOwnerId: null,
|
||||
provider: "claude",
|
||||
effectiveModel: "claude-3-5-sonnet",
|
||||
sourceFormat: FORMATS.CLAUDE,
|
||||
targetFormat: FORMATS.CLAUDE,
|
||||
backgroundReason: "background-task",
|
||||
log: null,
|
||||
});
|
||||
|
||||
assert.equal(result.memorySettings, null);
|
||||
assert.equal(result.body, body);
|
||||
});
|
||||
72
tests/unit/chatcore-non-streaming-sse.test.ts
Normal file
72
tests/unit/chatcore-non-streaming-sse.test.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
convertNDJSONToSSE,
|
||||
normalizeNonStreamingEventPayload,
|
||||
isTruthyStreamBody,
|
||||
isEventStreamAccepted,
|
||||
shouldTreatBufferedEventResponseAsExpected,
|
||||
parseNonStreamingSSEPayload,
|
||||
appendNonStreamingSseTerminalSignal,
|
||||
type NonStreamingSseTerminalState,
|
||||
} from "../../open-sse/handlers/chatCore/nonStreamingSse.ts";
|
||||
import { FORMATS } from "../../open-sse/translator/formats.ts";
|
||||
|
||||
test("convertNDJSONToSSE wraps each non-empty line as a data: frame", () => {
|
||||
const out = convertNDJSONToSSE('{"a":1}\n{"b":2}\n');
|
||||
assert.ok(out.includes('data: {"a":1}\n'));
|
||||
assert.ok(out.includes('data: {"b":2}\n'));
|
||||
assert.equal(convertNDJSONToSSE(""), "");
|
||||
});
|
||||
|
||||
test("normalizeNonStreamingEventPayload only converts x-ndjson content", () => {
|
||||
const raw = '{"a":1}';
|
||||
assert.equal(normalizeNonStreamingEventPayload(raw, "application/json"), raw);
|
||||
assert.notEqual(normalizeNonStreamingEventPayload(raw, "application/x-ndjson"), raw);
|
||||
});
|
||||
|
||||
test("stream-body and event-stream acceptance predicates", () => {
|
||||
assert.equal(isTruthyStreamBody({ stream: true }), true);
|
||||
assert.equal(isTruthyStreamBody({ stream: false }), false);
|
||||
assert.equal(isTruthyStreamBody(null), false);
|
||||
|
||||
assert.equal(isEventStreamAccepted({ accept: "text/event-stream" }), true);
|
||||
assert.equal(isEventStreamAccepted({ accept: "application/json" }), false);
|
||||
|
||||
assert.equal(
|
||||
shouldTreatBufferedEventResponseAsExpected(
|
||||
false,
|
||||
{ accept: "application/json" },
|
||||
{ stream: true }
|
||||
),
|
||||
true
|
||||
);
|
||||
assert.equal(
|
||||
shouldTreatBufferedEventResponseAsExpected(false, { accept: "application/json" }, {}),
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
test("appendNonStreamingSseTerminalSignal detects [DONE] and terminal event types", () => {
|
||||
const done: NonStreamingSseTerminalState = { currentEvent: "", pendingLine: "" };
|
||||
assert.equal(appendNonStreamingSseTerminalSignal(done, "data: [DONE]\n"), true);
|
||||
|
||||
const stop: NonStreamingSseTerminalState = { currentEvent: "", pendingLine: "" };
|
||||
assert.equal(appendNonStreamingSseTerminalSignal(stop, "event: message_stop\ndata: {}\n"), true);
|
||||
|
||||
const delta: NonStreamingSseTerminalState = { currentEvent: "", pendingLine: "" };
|
||||
assert.equal(
|
||||
appendNonStreamingSseTerminalSignal(delta, 'data: {"type":"content_block_delta"}\n'),
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
test("parseNonStreamingSSEPayload parses an OpenAI-format SSE buffer", () => {
|
||||
const raw =
|
||||
'data: {"id":"x","choices":[{"delta":{"content":"hi"},"finish_reason":"stop"}]}\n\ndata: [DONE]\n';
|
||||
const result = parseNonStreamingSSEPayload(raw, FORMATS.OPENAI, "gpt-4o");
|
||||
assert.ok(result !== null);
|
||||
assert.equal(result?.format, FORMATS.OPENAI);
|
||||
assert.equal(typeof result?.body, "object");
|
||||
});
|
||||
41
tests/unit/chatcore-passthrough-tool-names.test.ts
Normal file
41
tests/unit/chatcore-passthrough-tool-names.test.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
buildClaudePassthroughToolNameMap,
|
||||
restoreClaudePassthroughToolNames,
|
||||
mergeResponseToolNameMap,
|
||||
} from "../../open-sse/handlers/chatCore/passthroughToolNames.ts";
|
||||
import { CLAUDE_OAUTH_TOOL_PREFIX } from "../../open-sse/translator/request/openai-to-claude.ts";
|
||||
|
||||
test("buildClaudePassthroughToolNameMap maps prefixed -> original names", () => {
|
||||
const map = buildClaudePassthroughToolNameMap({
|
||||
tools: [{ type: "function", function: { name: "get_weather" } }],
|
||||
});
|
||||
assert.ok(map);
|
||||
assert.equal(map?.get(`${CLAUDE_OAUTH_TOOL_PREFIX}get_weather`), "get_weather");
|
||||
assert.equal(buildClaudePassthroughToolNameMap({ tools: [] }), null);
|
||||
assert.equal(buildClaudePassthroughToolNameMap(null), null);
|
||||
});
|
||||
|
||||
test("restoreClaudePassthroughToolNames rewrites tool_use block names", () => {
|
||||
const map = new Map([[`${CLAUDE_OAUTH_TOOL_PREFIX}x`, "x"]]);
|
||||
const restored = restoreClaudePassthroughToolNames(
|
||||
{ content: [{ type: "tool_use", name: `${CLAUDE_OAUTH_TOOL_PREFIX}x` }] },
|
||||
map
|
||||
) as { content: { name: string }[] };
|
||||
assert.equal(restored.content[0].name, "x");
|
||||
const body = { content: [{ type: "tool_use", name: "y" }] };
|
||||
assert.equal(restoreClaudePassthroughToolNames(body, null), body);
|
||||
});
|
||||
|
||||
test("mergeResponseToolNameMap unions base with executor _toolNameMap", () => {
|
||||
const base = new Map([["a", "1"]]);
|
||||
const merged = mergeResponseToolNameMap(base, { _toolNameMap: new Map([["b", "2"]]) }) as Map<
|
||||
string,
|
||||
string
|
||||
>;
|
||||
assert.equal(merged.get("a"), "1");
|
||||
assert.equal(merged.get("b"), "2");
|
||||
assert.equal(mergeResponseToolNameMap(base, {}), base);
|
||||
});
|
||||
307
tests/unit/chatcore-semantic-cache.test.ts
Normal file
307
tests/unit/chatcore-semantic-cache.test.ts
Normal file
@@ -0,0 +1,307 @@
|
||||
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";
|
||||
|
||||
// Isolated DATA_DIR set BEFORE importing anything that touches the DB
|
||||
// (checkSemanticCache -> getCachedResponse reads the semantic_cache SQLite table).
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-sem-cache-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
|
||||
const { checkSemanticCache } = await import("../../open-sse/handlers/chatCore/semanticCache.ts");
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
// Seeding the real cache (no mock.module under the Stryker tap-runner) lets us drive the
|
||||
// HIT branch deterministically: setCachedResponse populates the in-memory cache that
|
||||
// getCachedResponse checks first, so the signature checkSemanticCache rebuilds resolves.
|
||||
const { generateSignature, setCachedResponse, clearCache } = await import(
|
||||
"../../src/lib/semanticCache.ts"
|
||||
);
|
||||
const { OMNIROUTE_RESPONSE_HEADERS } = await import("../../src/shared/constants/headers.ts");
|
||||
|
||||
test.after(() => {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
// A reusable persistAttemptLogs spy + base args. The functions below should NEVER be
|
||||
// invoked on the guard-false / cache-miss paths (those only run on a HIT).
|
||||
function makeBaseArgs(overrides: Record<string, unknown> = {}) {
|
||||
const persistCalls: unknown[] = [];
|
||||
const args = {
|
||||
semanticCacheEnabled: false,
|
||||
body: { model: "gpt-4o", messages: [{ role: "user", content: "hi" }], temperature: 0 },
|
||||
clientRawRequest: { headers: {} },
|
||||
model: "gpt-4o",
|
||||
provider: "openai",
|
||||
stream: false,
|
||||
reqLogger: {
|
||||
logConvertedResponse: () => {
|
||||
throw new Error("logConvertedResponse should not run on guard-false / miss paths");
|
||||
},
|
||||
},
|
||||
effectiveServiceTier: undefined,
|
||||
connectionId: null as string | null,
|
||||
startTime: Date.now(),
|
||||
log: {
|
||||
debug: () => {
|
||||
throw new Error("log.debug should only fire on a cache HIT");
|
||||
},
|
||||
},
|
||||
persistAttemptLogs: (a: unknown) => {
|
||||
persistCalls.push(a);
|
||||
},
|
||||
apiKeyId: null as string | null,
|
||||
...overrides,
|
||||
};
|
||||
return { args, persistCalls };
|
||||
}
|
||||
|
||||
// ─── checkSemanticCache ──────────────────────────────────────────────────────
|
||||
|
||||
test("checkSemanticCache returns null when semanticCacheEnabled is false (outer guard short-circuit)", async () => {
|
||||
const { args, persistCalls } = makeBaseArgs({ semanticCacheEnabled: false });
|
||||
|
||||
const result = await checkSemanticCache(args as Parameters<typeof checkSemanticCache>[0]);
|
||||
|
||||
assert.equal(result, null, "disabled cache -> no-op null result");
|
||||
assert.equal(persistCalls.length, 0, "no logging side effects when the guard is false");
|
||||
});
|
||||
|
||||
test("checkSemanticCache returns null when enabled but the body is NOT cacheable (temperature != 0)", async () => {
|
||||
// isCacheableForRead requires temperature === 0. A non-zero temperature makes the guard
|
||||
// false even with semanticCacheEnabled=true, so it short-circuits to the null no-op.
|
||||
const { args, persistCalls } = makeBaseArgs({
|
||||
semanticCacheEnabled: true,
|
||||
body: { model: "gpt-4o", messages: [{ role: "user", content: "hi" }], temperature: 0.7 },
|
||||
});
|
||||
|
||||
const result = await checkSemanticCache(args as Parameters<typeof checkSemanticCache>[0]);
|
||||
|
||||
assert.equal(result, null, "non-cacheable body -> guard false -> null");
|
||||
assert.equal(persistCalls.length, 0);
|
||||
});
|
||||
|
||||
test("checkSemanticCache returns null when the x-omniroute-no-cache header forces a bypass", async () => {
|
||||
// The no-cache header makes isCacheableForRead return false even with temperature:0.
|
||||
const { args } = makeBaseArgs({
|
||||
semanticCacheEnabled: true,
|
||||
body: { model: "gpt-4o", messages: [{ role: "user", content: "hi" }], temperature: 0 },
|
||||
clientRawRequest: { headers: { "x-omniroute-no-cache": "true" } },
|
||||
});
|
||||
|
||||
const result = await checkSemanticCache(args as Parameters<typeof checkSemanticCache>[0]);
|
||||
|
||||
assert.equal(result, null, "explicit no-cache header bypasses the cache read");
|
||||
});
|
||||
|
||||
test("checkSemanticCache returns null on a cache MISS (enabled + cacheable body + empty cache)", async () => {
|
||||
// semanticCacheEnabled=true AND temperature:0 (cacheable) -> enters the guard -> builds a
|
||||
// signature -> getCachedResponse() finds nothing in the empty (fresh DATA_DIR) cache ->
|
||||
// the `if (cached)` block is skipped -> the function falls through to `return null`.
|
||||
const { args, persistCalls } = makeBaseArgs({
|
||||
semanticCacheEnabled: true,
|
||||
body: {
|
||||
model: "gpt-4o",
|
||||
messages: [{ role: "user", content: "a unique miss query " + Date.now() }],
|
||||
temperature: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await checkSemanticCache(args as Parameters<typeof checkSemanticCache>[0]);
|
||||
|
||||
assert.equal(result, null, "empty cache -> MISS -> null (no HIT-path side effects)");
|
||||
assert.equal(persistCalls.length, 0, "persistAttemptLogs only runs on a HIT");
|
||||
});
|
||||
|
||||
test("checkSemanticCache MISS also works for the Responses-API `input` body shape", async () => {
|
||||
// generateSignature falls back to body.input when body.messages is absent; the empty cache
|
||||
// still yields a MISS, so the result is null.
|
||||
const { args, persistCalls } = makeBaseArgs({
|
||||
semanticCacheEnabled: true,
|
||||
body: {
|
||||
model: "gpt-4o",
|
||||
input: [{ role: "user", content: "responses api miss " + Date.now() }],
|
||||
temperature: 0,
|
||||
},
|
||||
stream: true,
|
||||
});
|
||||
|
||||
const result = await checkSemanticCache(args as Parameters<typeof checkSemanticCache>[0]);
|
||||
|
||||
assert.equal(result, null);
|
||||
assert.equal(persistCalls.length, 0);
|
||||
});
|
||||
|
||||
// ─── HIT path ────────────────────────────────────────────────────────────────
|
||||
// The guard-false / miss tests above only exercise the `return null` branches. These
|
||||
// seed the real cache so the `if (cached)` block actually runs end-to-end, killing
|
||||
// mutants in the HIT body (status 200 / "semantic" / "HIT" / the stream + content-type
|
||||
// ternaries / the cost fallback / the side-effect calls).
|
||||
|
||||
// Recording (non-throwing) spies — the HIT path DOES invoke log.debug + logConvertedResponse.
|
||||
function makeHitArgs(overrides: Record<string, unknown> = {}) {
|
||||
const persistCalls: Record<string, unknown>[] = [];
|
||||
const convertedCalls: unknown[] = [];
|
||||
const debugCalls: unknown[][] = [];
|
||||
const args = {
|
||||
semanticCacheEnabled: true,
|
||||
body: { model: "gpt-4o", messages: [{ role: "user", content: "cached query" }], temperature: 0 },
|
||||
clientRawRequest: { headers: {} },
|
||||
model: "gpt-4o",
|
||||
provider: "openai",
|
||||
stream: false,
|
||||
reqLogger: {
|
||||
logConvertedResponse: (r: unknown) => {
|
||||
convertedCalls.push(r);
|
||||
},
|
||||
},
|
||||
effectiveServiceTier: undefined,
|
||||
connectionId: null as string | null,
|
||||
startTime: Date.now() - 5,
|
||||
log: {
|
||||
debug: (...a: unknown[]) => {
|
||||
debugCalls.push(a);
|
||||
},
|
||||
},
|
||||
persistAttemptLogs: (a: unknown) => {
|
||||
persistCalls.push(a as Record<string, unknown>);
|
||||
},
|
||||
apiKeyId: null as string | null,
|
||||
...overrides,
|
||||
};
|
||||
return { args, persistCalls, convertedCalls, debugCalls };
|
||||
}
|
||||
|
||||
// Seed the cache under the EXACT signature checkSemanticCache rebuilds for `args`.
|
||||
function seedHit(args: ReturnType<typeof makeHitArgs>["args"], response: unknown) {
|
||||
const signature = generateSignature(
|
||||
args.model,
|
||||
args.body.messages ?? (args.body as Record<string, unknown>).input,
|
||||
args.body.temperature,
|
||||
(args.body as Record<string, unknown>).top_p,
|
||||
args.apiKeyId ?? undefined
|
||||
);
|
||||
setCachedResponse(signature, args.model, response);
|
||||
return signature;
|
||||
}
|
||||
|
||||
test("checkSemanticCache returns a non-streaming JSON HIT with cache headers + logging side effects", async () => {
|
||||
clearCache();
|
||||
const cached = {
|
||||
id: "chatcmpl-cached-1",
|
||||
choices: [
|
||||
{ index: 0, message: { role: "assistant", content: "cached answer" }, finish_reason: "stop" },
|
||||
],
|
||||
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
|
||||
};
|
||||
const { args, persistCalls, convertedCalls, debugCalls } = makeHitArgs({
|
||||
body: { model: "gpt-4o", messages: [{ role: "user", content: "hit query one" }], temperature: 0 },
|
||||
stream: false,
|
||||
});
|
||||
seedHit(args, cached);
|
||||
|
||||
const result = await checkSemanticCache(args as Parameters<typeof checkSemanticCache>[0]);
|
||||
|
||||
assert.ok(result, "HIT -> non-null result");
|
||||
assert.equal(result.success, true, "HIT result.success is true");
|
||||
const res = result.response as Response;
|
||||
assert.equal(res.headers.get(OMNIROUTE_RESPONSE_HEADERS.cache), "HIT", "X-OmniRoute-Cache: HIT");
|
||||
assert.equal(
|
||||
res.headers.get(OMNIROUTE_RESPONSE_HEADERS.cacheHit),
|
||||
"true",
|
||||
"cacheHit meta header is true"
|
||||
);
|
||||
assert.equal(
|
||||
res.headers.get("Content-Type"),
|
||||
"application/json",
|
||||
"non-streaming HIT -> application/json"
|
||||
);
|
||||
const bodyText = await res.text();
|
||||
assert.equal(bodyText, JSON.stringify(cached), "non-streaming HIT body is the cached JSON");
|
||||
|
||||
assert.equal(persistCalls.length, 1, "persistAttemptLogs runs exactly once on a HIT");
|
||||
const logged = persistCalls[0];
|
||||
assert.equal(logged.status, 200, "persisted status is 200");
|
||||
assert.equal(logged.cacheSource, "semantic", "persisted cacheSource is 'semantic'");
|
||||
assert.deepEqual(logged.responseBody, cached, "persisted responseBody is the cached object");
|
||||
assert.deepEqual(logged.clientResponse, cached, "persisted clientResponse is the cached object");
|
||||
assert.deepEqual(logged.tokens, cached.usage, "persisted tokens come from cached.usage");
|
||||
assert.equal(logged.providerRequest, null);
|
||||
assert.equal(logged.providerResponse, null);
|
||||
|
||||
assert.equal(convertedCalls.length, 1, "logConvertedResponse called once with the cached body");
|
||||
assert.deepEqual(convertedCalls[0], cached);
|
||||
assert.equal(debugCalls.length, 1, "log.debug fires exactly once on a HIT");
|
||||
});
|
||||
|
||||
test("checkSemanticCache returns a streaming SSE HIT (text/event-stream) when stream=true", async () => {
|
||||
clearCache();
|
||||
const cached = {
|
||||
id: "chatcmpl-cached-stream",
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
message: { role: "assistant", content: "streamed cached answer" },
|
||||
finish_reason: "stop",
|
||||
},
|
||||
],
|
||||
usage: { prompt_tokens: 3, completion_tokens: 4, total_tokens: 7 },
|
||||
};
|
||||
const { args, persistCalls } = makeHitArgs({
|
||||
body: { model: "gpt-4o", messages: [{ role: "user", content: "hit query stream" }], temperature: 0 },
|
||||
stream: true,
|
||||
});
|
||||
seedHit(args, cached);
|
||||
|
||||
const result = await checkSemanticCache(args as Parameters<typeof checkSemanticCache>[0]);
|
||||
|
||||
assert.ok(result, "streaming HIT -> non-null result");
|
||||
assert.equal(result.success, true);
|
||||
const res = result.response as Response;
|
||||
assert.equal(
|
||||
res.headers.get("Content-Type"),
|
||||
"text/event-stream",
|
||||
"streaming HIT -> text/event-stream"
|
||||
);
|
||||
assert.equal(res.headers.get(OMNIROUTE_RESPONSE_HEADERS.cache), "HIT");
|
||||
const bodyText = await res.text();
|
||||
assert.ok(bodyText.includes("data: "), "SSE body contains data frames");
|
||||
assert.ok(bodyText.includes("streamed cached answer"), "SSE body carries the cached content");
|
||||
assert.ok(bodyText.trimEnd().endsWith("data: [DONE]"), "SSE body terminates with [DONE]");
|
||||
assert.equal(persistCalls.length, 1, "persistAttemptLogs runs once on a streaming HIT too");
|
||||
});
|
||||
|
||||
test("checkSemanticCache HITs even when the cached body has no usage (cost falls back to 0)", async () => {
|
||||
clearCache();
|
||||
const cached = {
|
||||
id: "chatcmpl-cached-no-usage",
|
||||
choices: [
|
||||
{ index: 0, message: { role: "assistant", content: "no-usage answer" }, finish_reason: "stop" },
|
||||
],
|
||||
};
|
||||
const { args, persistCalls } = makeHitArgs({
|
||||
body: {
|
||||
model: "gpt-4o",
|
||||
messages: [{ role: "user", content: "hit query no usage" }],
|
||||
temperature: 0,
|
||||
},
|
||||
stream: false,
|
||||
});
|
||||
seedHit(args, cached);
|
||||
|
||||
const result = await checkSemanticCache(args as Parameters<typeof checkSemanticCache>[0]);
|
||||
|
||||
assert.ok(result, "HIT with no usage -> non-null result");
|
||||
assert.equal(result.success, true);
|
||||
const res = result.response as Response;
|
||||
assert.equal(res.headers.get(OMNIROUTE_RESPONSE_HEADERS.cache), "HIT");
|
||||
// cachedUsage resolves to undefined -> cachedCost = 0 -> the zero-cost sentinel header.
|
||||
assert.equal(
|
||||
res.headers.get(OMNIROUTE_RESPONSE_HEADERS.responseCost),
|
||||
"0.0000000000",
|
||||
"no usage -> zero responseCost header"
|
||||
);
|
||||
assert.equal(persistCalls.length, 1);
|
||||
assert.equal(persistCalls[0].tokens, undefined, "no usage -> persisted tokens is undefined");
|
||||
});
|
||||
173
tests/unit/chatcore-telemetry-helpers.test.ts
Normal file
173
tests/unit/chatcore-telemetry-helpers.test.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
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";
|
||||
|
||||
// Isolated DATA_DIR set BEFORE importing anything that may touch the DB
|
||||
// (maybeSyncClaudeExtraUsageState -> fetchLiveProviderLimits -> getProviderConnectionById).
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-telemetry-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
|
||||
const { forwardDashboardEventToLiveWs, maybeSyncClaudeExtraUsageState } = await import(
|
||||
"../../open-sse/handlers/chatCore/telemetryHelpers.ts"
|
||||
);
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
const originalLiveWsPort = process.env.LIVE_WS_PORT;
|
||||
|
||||
test.afterEach(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
if (originalLiveWsPort === undefined) {
|
||||
delete process.env.LIVE_WS_PORT;
|
||||
} else {
|
||||
process.env.LIVE_WS_PORT = originalLiveWsPort;
|
||||
}
|
||||
});
|
||||
|
||||
test.after(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
// ─── forwardDashboardEventToLiveWs ───────────────────────────────────────────
|
||||
|
||||
test("forwardDashboardEventToLiveWs POSTs event+payload+timestamp as JSON to the default port", async () => {
|
||||
delete process.env.LIVE_WS_PORT;
|
||||
let capturedUrl: string | undefined;
|
||||
let capturedInit: RequestInit | undefined;
|
||||
globalThis.fetch = (async (url: string, init: RequestInit) => {
|
||||
capturedUrl = url;
|
||||
capturedInit = init;
|
||||
return new Response("ok", { status: 200 });
|
||||
}) as typeof fetch;
|
||||
|
||||
const before = Date.now();
|
||||
await forwardDashboardEventToLiveWs("my-event", { foo: "bar" });
|
||||
const after = Date.now();
|
||||
|
||||
// Default port is 20129 when LIVE_WS_PORT is unset.
|
||||
assert.equal(capturedUrl, "http://127.0.0.1:20129/__omniroute_event");
|
||||
assert.equal(capturedInit?.method, "POST");
|
||||
assert.equal(
|
||||
(capturedInit?.headers as Record<string, string>)["content-type"],
|
||||
"application/json"
|
||||
);
|
||||
assert.ok(capturedInit?.signal, "an AbortSignal is attached for the 1.5s timeout");
|
||||
|
||||
const parsed = JSON.parse(capturedInit?.body as string);
|
||||
assert.equal(parsed.event, "my-event");
|
||||
assert.deepEqual(parsed.payload, { foo: "bar" });
|
||||
assert.equal(typeof parsed.timestamp, "number");
|
||||
assert.ok(
|
||||
parsed.timestamp >= before && parsed.timestamp <= after,
|
||||
"timestamp is Date.now() captured at call time"
|
||||
);
|
||||
});
|
||||
|
||||
test("forwardDashboardEventToLiveWs honors LIVE_WS_PORT override", async () => {
|
||||
process.env.LIVE_WS_PORT = "31337";
|
||||
let capturedUrl: string | undefined;
|
||||
globalThis.fetch = (async (url: string) => {
|
||||
capturedUrl = url;
|
||||
return new Response("ok", { status: 200 });
|
||||
}) as typeof fetch;
|
||||
|
||||
await forwardDashboardEventToLiveWs("e", null);
|
||||
|
||||
assert.equal(capturedUrl, "http://127.0.0.1:31337/__omniroute_event");
|
||||
});
|
||||
|
||||
test("forwardDashboardEventToLiveWs swallows fetch rejection and still resolves", async () => {
|
||||
globalThis.fetch = (async () => {
|
||||
throw new Error("sidecar down");
|
||||
}) as typeof fetch;
|
||||
|
||||
// Must not throw — best-effort sidecar bridge; the catch swallows.
|
||||
await assert.doesNotReject(forwardDashboardEventToLiveWs("e", { a: 1 }));
|
||||
});
|
||||
|
||||
// ─── maybeSyncClaudeExtraUsageState ──────────────────────────────────────────
|
||||
|
||||
test("maybeSyncClaudeExtraUsageState returns early when connectionId is falsy (no fetch)", async () => {
|
||||
let fetchCalled = false;
|
||||
globalThis.fetch = (async () => {
|
||||
fetchCalled = true;
|
||||
return new Response("", { status: 200 });
|
||||
}) as typeof fetch;
|
||||
|
||||
await maybeSyncClaudeExtraUsageState({
|
||||
provider: "claude",
|
||||
connectionId: null,
|
||||
providerSpecificData: {},
|
||||
log: null,
|
||||
});
|
||||
|
||||
assert.equal(fetchCalled, false, "guard short-circuits before any network/DB work");
|
||||
});
|
||||
|
||||
test("maybeSyncClaudeExtraUsageState returns early for non-claude provider (block disabled)", async () => {
|
||||
let fetchCalled = false;
|
||||
globalThis.fetch = (async () => {
|
||||
fetchCalled = true;
|
||||
return new Response("", { status: 200 });
|
||||
}) as typeof fetch;
|
||||
|
||||
await maybeSyncClaudeExtraUsageState({
|
||||
provider: "openai",
|
||||
connectionId: "some-conn",
|
||||
providerSpecificData: {},
|
||||
log: null,
|
||||
});
|
||||
|
||||
assert.equal(fetchCalled, false, "isClaudeExtraUsageBlockEnabled is false for non-claude");
|
||||
});
|
||||
|
||||
test("maybeSyncClaudeExtraUsageState returns early for claude with blockExtraUsage:false", async () => {
|
||||
let fetchCalled = false;
|
||||
globalThis.fetch = (async () => {
|
||||
fetchCalled = true;
|
||||
return new Response("", { status: 200 });
|
||||
}) as typeof fetch;
|
||||
|
||||
await maybeSyncClaudeExtraUsageState({
|
||||
provider: "claude",
|
||||
connectionId: "some-conn",
|
||||
providerSpecificData: { blockExtraUsage: false },
|
||||
log: null,
|
||||
});
|
||||
|
||||
assert.equal(fetchCalled, false, "explicit blockExtraUsage:false disables the block");
|
||||
});
|
||||
|
||||
test("maybeSyncClaudeExtraUsageState enters the try for claude+enabled, swallows the error, and logs via log.debug", async () => {
|
||||
// provider=claude, providerSpecificData={} (blockExtraUsage !== false), connectionId set
|
||||
// -> passes the guard -> calls the REAL fetchLiveProviderLimits("bogus-conn") which
|
||||
// looks the connection up in the (empty) DB, finds nothing, throws "Connection not found",
|
||||
// and the function's internal try/catch swallows it while logging to log.debug.
|
||||
const calls: unknown[][] = [];
|
||||
const log = {
|
||||
debug: (...args: unknown[]) => {
|
||||
calls.push(args);
|
||||
},
|
||||
};
|
||||
|
||||
await assert.doesNotReject(
|
||||
maybeSyncClaudeExtraUsageState({
|
||||
provider: "claude",
|
||||
connectionId: "bogus-conn-id",
|
||||
providerSpecificData: {},
|
||||
log,
|
||||
})
|
||||
);
|
||||
|
||||
assert.equal(calls.length, 1, "the swallowed error path logs exactly once");
|
||||
assert.equal(calls[0][0], "CLAUDE_USAGE");
|
||||
assert.match(
|
||||
String(calls[0][1]),
|
||||
/Failed to sync Claude extra-usage state:/,
|
||||
"logs the sync-failure message with the underlying error text"
|
||||
);
|
||||
});
|
||||
@@ -433,14 +433,13 @@ test("chatCore times out upstream execution before provider response headers", a
|
||||
userAgent: "unit-test",
|
||||
} as any);
|
||||
|
||||
const pendingDetail = (await waitFor(
|
||||
() =>
|
||||
// details[connectionId] is Record<modelKey, PendingRequestDetail[]> —
|
||||
// the original predicate tested each ARRAY's .providerRequest (always
|
||||
// undefined), so the waitFor could never resolve. Flatten to the details.
|
||||
Object.values(getPendingRequests().details[connectionId] || {})
|
||||
.flat()
|
||||
.find((detail: any) => detail?.providerRequest?.model === "gpt-4o-mini")
|
||||
const pendingDetail = (await waitFor(() =>
|
||||
// details[connectionId] is Record<modelKey, PendingRequestDetail[]> —
|
||||
// the original predicate tested each ARRAY's .providerRequest (always
|
||||
// undefined), so the waitFor could never resolve. Flatten to the details.
|
||||
Object.values(getPendingRequests().details[connectionId] || {})
|
||||
.flat()
|
||||
.find((detail: any) => detail?.providerRequest?.model === "gpt-4o-mini")
|
||||
)) as any;
|
||||
assert.equal(pendingDetail?.providerRequest?.model, "gpt-4o-mini");
|
||||
assert.deepEqual(pendingDetail?.providerRequest?.messages, body.messages);
|
||||
@@ -2519,7 +2518,13 @@ test("chatCore returns streaming responses without waiting for upstream completi
|
||||
|
||||
const raceResult = await Promise.race([
|
||||
invocation.then(() => "returned"),
|
||||
new Promise((resolve) => setTimeout(() => resolve("blocked"), 1000)),
|
||||
// 10s ceiling: a non-buffering streaming impl resolves the invocation as soon
|
||||
// as the Response is returned (upstream still open), but on a starved CI event
|
||||
// loop that legitimate early return can exceed a 1s wall-clock budget (flake
|
||||
// repro: 5/8 runs returned at 1.3–2.2s under CPU contention → false "blocked").
|
||||
// The ceiling only bounds the buffered-failure case: a buffering impl never
|
||||
// resolves until closeUpstream() fires below, so it still trips "blocked".
|
||||
new Promise((resolve) => setTimeout(() => resolve("blocked"), 10000)),
|
||||
]);
|
||||
|
||||
if (raceResult !== "returned") {
|
||||
|
||||
51
tests/unit/chatcore-upstream-timeouts.test.ts
Normal file
51
tests/unit/chatcore-upstream-timeouts.test.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
createBodyTimeoutError,
|
||||
createUpstreamStartTimeoutError,
|
||||
createAbortError,
|
||||
computeBillableTokens,
|
||||
getExecutorTimeoutMs,
|
||||
normalizeExecutorResult,
|
||||
} from "../../open-sse/handlers/chatCore/upstreamTimeouts.ts";
|
||||
|
||||
test("error factories set name and message", () => {
|
||||
const body = createBodyTimeoutError(1234);
|
||||
assert.equal(body.name, "BodyTimeoutError");
|
||||
assert.match(body.message, /1234ms/);
|
||||
|
||||
const start = createUpstreamStartTimeoutError(500, "openai", "gpt-4o");
|
||||
assert.equal(start.name, "TimeoutError");
|
||||
assert.match(start.message, /openai\/gpt-4o/);
|
||||
|
||||
const ctrl = new AbortController();
|
||||
ctrl.abort("nope");
|
||||
const ab = createAbortError(ctrl.signal);
|
||||
assert.equal(ab.name, "AbortError");
|
||||
});
|
||||
|
||||
test("computeBillableTokens sums input+output+reasoning (no cache double-count)", () => {
|
||||
const total = computeBillableTokens({
|
||||
prompt_tokens: 10,
|
||||
completion_tokens: 5,
|
||||
reasoning_tokens: 2,
|
||||
});
|
||||
assert.equal(total, 17);
|
||||
});
|
||||
|
||||
test("getExecutorTimeoutMs floors valid values and falls back to default", () => {
|
||||
assert.equal(getExecutorTimeoutMs({ getTimeoutMs: () => 1234.9 }), 1234);
|
||||
assert.equal(getExecutorTimeoutMs({ getTimeoutMs: () => NaN }), getExecutorTimeoutMs(null));
|
||||
assert.ok(Number.isFinite(getExecutorTimeoutMs(null)));
|
||||
});
|
||||
|
||||
test("normalizeExecutorResult wraps bare Response and passes through rich result", () => {
|
||||
const r = new Response("x");
|
||||
const wrapped = normalizeExecutorResult(r);
|
||||
assert.equal(wrapped.response, r);
|
||||
assert.equal(wrapped.url, "");
|
||||
const rich = normalizeExecutorResult({ response: r, url: "u", headers: { a: "b" } });
|
||||
assert.equal(rich.url, "u");
|
||||
assert.equal(rich.headers.a, "b");
|
||||
});
|
||||
@@ -122,9 +122,10 @@ test("INTENTIONALLY_INTERNAL is exported from check-db-rules.mjs", () => {
|
||||
assert.ok(INTENTIONALLY_INTERNAL.size > 0, "INTENTIONALLY_INTERNAL must not be empty");
|
||||
});
|
||||
|
||||
test("INTENTIONALLY_INTERNAL contains the expected 25 audited modules", () => {
|
||||
test("INTENTIONALLY_INTERNAL contains the expected 26 audited modules", () => {
|
||||
const expected = [
|
||||
"_rowTypes",
|
||||
"accessTokens",
|
||||
"cleanup",
|
||||
"cliToolState",
|
||||
"comboForecast",
|
||||
|
||||
@@ -9,6 +9,45 @@ import {
|
||||
formatHumanReport,
|
||||
} from "../../scripts/check/check-fabricated-docs.mjs";
|
||||
|
||||
// ── Fixture helpers ─────────────────────────────────────────────────────────
|
||||
// The hardened checker accepts a `root` so we can run the full pipeline against a
|
||||
// throwaway fixture repo (its own docs/, src/, .env.example) instead of the live
|
||||
// tree. Each fixture is a minimal repo: a code surface to index + a single doc.
|
||||
|
||||
type Fixture = { docs?: Record<string, string>; files?: Record<string, string> };
|
||||
|
||||
function makeFixtureRoot(fx: Fixture): string {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "fab-docs-"));
|
||||
const write = (rel: string, content: string) => {
|
||||
const abs = path.join(root, rel);
|
||||
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
||||
fs.writeFileSync(abs, content);
|
||||
};
|
||||
// Always provide an AGENTS.md so allScanFiles() has at least one scan target.
|
||||
write("AGENTS.md", fx.docs?.["AGENTS.md"] ?? "# Fixture\n");
|
||||
for (const [rel, content] of Object.entries(fx.docs ?? {})) {
|
||||
if (rel === "AGENTS.md") write(rel, content);
|
||||
else write(path.join("docs", rel), content);
|
||||
}
|
||||
for (const [rel, content] of Object.entries(fx.files ?? {})) write(rel, content);
|
||||
return root;
|
||||
}
|
||||
|
||||
/** Returns the set of distinct `${kind}::${value}` findings for a fixture. */
|
||||
function findingsFor(fx: Fixture): Set<string> {
|
||||
const root = makeFixtureRoot(fx);
|
||||
try {
|
||||
const result = runFabricatedDocsCheck({ root });
|
||||
const out = new Set<string>();
|
||||
for (const f of result.files) {
|
||||
for (const finding of f.findings) out.add(`${finding.kind}::${finding.value}`);
|
||||
}
|
||||
return out;
|
||||
} finally {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
// We can't easily mock the buildCodebaseIndex() which walks src/app/api etc.
|
||||
// Instead, we test the report-formatting logic and the no-findings path on
|
||||
// the real repo, which acts as a smoke test that the script runs end-to-end.
|
||||
@@ -73,3 +112,236 @@ test("formatHumanReport: groups findings by kind", () => {
|
||||
assert.match(out, /FAKE_VAR_X/);
|
||||
assert.match(out, /onFake/);
|
||||
});
|
||||
|
||||
// ─── Hardening: precision fixes (QG v2 Fase 9 T9) ───────────────────────────
|
||||
//
|
||||
// Each test runs the FULL pipeline (index + scan) over a fixture repo so it
|
||||
// exercises the real heuristic, not a stubbed index.
|
||||
|
||||
test("env-var: an `export const` UPPER_SNAKE identifier in backticks is NOT flagged", () => {
|
||||
const found = findingsFor({
|
||||
files: {
|
||||
"src/server/authz/routeGuard.ts":
|
||||
"export const LOCAL_ONLY_API_PREFIXES: ReadonlyArray<string> = [];\n",
|
||||
},
|
||||
docs: {
|
||||
"guide.md": "We expose `LOCAL_ONLY_API_PREFIXES` to gate loopback routes.\n",
|
||||
},
|
||||
});
|
||||
assert.ok(
|
||||
!found.has("env-var::LOCAL_ONLY_API_PREFIXES"),
|
||||
"export const identifier must not be flagged as a fabricated env var"
|
||||
);
|
||||
});
|
||||
|
||||
test("env-var: an enum / object-literal member (HALF_OPEN) in backticks is NOT flagged", () => {
|
||||
const found = findingsFor({
|
||||
files: {
|
||||
"src/shared/utils/circuitBreaker.ts":
|
||||
'export const STATE = {\n CLOSED: "CLOSED",\n HALF_OPEN: "HALF_OPEN",\n} as const;\n',
|
||||
},
|
||||
docs: { "resilience.md": "After the reset window the breaker enters `HALF_OPEN`.\n" },
|
||||
});
|
||||
assert.ok(!found.has("env-var::HALF_OPEN"), "object-literal/enum key must not be flagged");
|
||||
});
|
||||
|
||||
test('env-var: a var read via bracket notation process.env["X"] is NOT flagged', () => {
|
||||
const found = findingsFor({
|
||||
files: {
|
||||
"src/lib/memory/vectorStore.ts": 'const k = Number(process.env["MEMORY_RRF_K"] ?? 60);\n',
|
||||
},
|
||||
docs: { "memory.md": "Tune ranking with `MEMORY_RRF_K`.\n" },
|
||||
});
|
||||
assert.ok(!found.has("env-var::MEMORY_RRF_K"), 'process.env["X"] read must be indexed');
|
||||
});
|
||||
|
||||
test('env-var: a var read via an env helper (envInt("X")) is NOT flagged', () => {
|
||||
const found = findingsFor({
|
||||
files: {
|
||||
"open-sse/config/constants.ts":
|
||||
'const t = envInt("OMNIROUTE_CIRCUIT_BREAKER_OAUTH_THRESHOLD", 8);\n',
|
||||
},
|
||||
docs: { "cfg.md": "Override with `OMNIROUTE_CIRCUIT_BREAKER_OAUTH_THRESHOLD`.\n" },
|
||||
});
|
||||
assert.ok(
|
||||
!found.has("env-var::OMNIROUTE_CIRCUIT_BREAKER_OAUTH_THRESHOLD"),
|
||||
'envInt("X", …) helper read must be indexed'
|
||||
);
|
||||
});
|
||||
|
||||
test("env-var: a var read ONLY in tests/ (RUN_CHAOS_INT) is NOT flagged", () => {
|
||||
const found = findingsFor({
|
||||
files: {
|
||||
"tests/integration/resilience-chaos.test.ts":
|
||||
'const RUN = process.env.RUN_CHAOS_INT === "1";\n',
|
||||
},
|
||||
docs: { "testing.md": "Set `RUN_CHAOS_INT` to enable the chaos suite.\n" },
|
||||
});
|
||||
assert.ok(
|
||||
!found.has("env-var::RUN_CHAOS_INT"),
|
||||
"env vars read only in tests/ must be indexed, not flagged"
|
||||
);
|
||||
});
|
||||
|
||||
test("env-var: a var present only in .env.example is NOT flagged", () => {
|
||||
const found = findingsFor({
|
||||
files: { ".env.example": "# Contract\nSOME_DOCUMENTED_CONTRACT_VAR=value\n" },
|
||||
docs: { "env.md": "Configure `SOME_DOCUMENTED_CONTRACT_VAR` in your environment.\n" },
|
||||
});
|
||||
assert.ok(
|
||||
!found.has("env-var::SOME_DOCUMENTED_CONTRACT_VAR"),
|
||||
".env.example is the env contract — its vars are documented, not fabricated"
|
||||
);
|
||||
});
|
||||
|
||||
test("api-path: a documented prefix with sub-routes (/api/cloud/) is NOT flagged", () => {
|
||||
const found = findingsFor({
|
||||
files: {
|
||||
"src/app/api/cloud/auth/route.ts": "export async function GET() {}\n",
|
||||
},
|
||||
docs: { "cloud.md": "All cloud endpoints live under `/api/cloud/`.\n" },
|
||||
});
|
||||
assert.ok(
|
||||
!found.has("api-path::/api/cloud/"),
|
||||
"a prefix that is an ancestor of a real route.ts must resolve"
|
||||
);
|
||||
});
|
||||
|
||||
test("api-path: a dynamic-segment prefix (/api/services/{name}/) is NOT flagged", () => {
|
||||
const found = findingsFor({
|
||||
files: {
|
||||
"src/app/api/services/[name]/status/route.ts": "export async function GET() {}\n",
|
||||
},
|
||||
docs: { "services.md": "Hit `/api/services/{name}/status` for health.\n" },
|
||||
});
|
||||
assert.ok(
|
||||
!found.has("api-path::/api/services/{name}/status"),
|
||||
"[name] dynamic segments must match the documented {name} convention"
|
||||
);
|
||||
});
|
||||
|
||||
test("hook: a real callback now in KNOWN_HOOKS (onChunk) is NOT flagged", () => {
|
||||
const found = findingsFor({
|
||||
docs: { "playground.md": "The stream invokes `onChunk` for each delta.\n" },
|
||||
});
|
||||
assert.ok(!found.has("hook::onChunk"), "onChunk is a real callback and must not be flagged");
|
||||
});
|
||||
|
||||
test("file-ref: a tutorial placeholder (src/app/api/your-route/route.ts) is NOT flagged", () => {
|
||||
const found = findingsFor({
|
||||
docs: {
|
||||
"scenario.md": "Create `src/app/api/your-route/route.ts` with GET/POST handlers.\n",
|
||||
},
|
||||
});
|
||||
assert.ok(
|
||||
!found.has("file-ref::src/app/api/your-route/route.ts"),
|
||||
"your-* / my* placeholders in how-to scenarios must not be flagged"
|
||||
);
|
||||
});
|
||||
|
||||
// ─── ANTI-OVER-SUPPRESSION GUARD (the most important test) ──────────────────
|
||||
//
|
||||
// The hardening eliminates false positives by adding PRECISION, never by blinding
|
||||
// the checker. A reference to something that genuinely does not exist MUST still be
|
||||
// flagged — otherwise the gate is worthless.
|
||||
|
||||
test("ANTI-OVER-SUPPRESSION: a reference to a genuinely missing file IS still flagged", () => {
|
||||
const found = findingsFor({
|
||||
docs: { "bad.md": "See the handler in `src/nao/existe.ts` for details.\n" },
|
||||
});
|
||||
assert.ok(
|
||||
found.has("file-ref::src/nao/existe.ts"),
|
||||
"a non-existent file reference must remain flagged — precision must not blind detection"
|
||||
);
|
||||
});
|
||||
|
||||
test("ANTI-OVER-SUPPRESSION: a truly fabricated env var IS still flagged", () => {
|
||||
const found = findingsFor({
|
||||
// No code reads it, not a code identifier, not in .env.example.
|
||||
docs: { "bad.md": "Set `TOTALLY_FABRICATED_ENV_VAR_XYZ` to enable nothing.\n" },
|
||||
});
|
||||
assert.ok(
|
||||
found.has("env-var::TOTALLY_FABRICATED_ENV_VAR_XYZ"),
|
||||
"a fabricated env var must remain flagged"
|
||||
);
|
||||
});
|
||||
|
||||
test("ANTI-OVER-SUPPRESSION: a fabricated API path with no backing route.ts IS still flagged", () => {
|
||||
const found = findingsFor({
|
||||
files: { "src/app/api/cloud/auth/route.ts": "export async function GET() {}\n" },
|
||||
// /api/imaginary/* has no route.ts anywhere and is not an ancestor of one.
|
||||
docs: { "bad.md": "Call `/api/imaginary/widget` to do magic.\n" },
|
||||
});
|
||||
assert.ok(
|
||||
found.has("api-path::/api/imaginary/widget"),
|
||||
"an API path with no backing route must remain flagged"
|
||||
);
|
||||
});
|
||||
|
||||
test("env-var: a doc explicitly stating a var does NOT exist is NOT flagged (documents absence)", () => {
|
||||
const found = findingsFor({
|
||||
docs: {
|
||||
"memory.md":
|
||||
"There are no env vars to tune weights (`MEMORY_RRF_VECTOR_WEIGHT` does not exist).\n",
|
||||
},
|
||||
});
|
||||
assert.ok(
|
||||
!found.has("env-var::MEMORY_RRF_VECTOR_WEIGHT"),
|
||||
"documenting a var's absence is not fabricating it"
|
||||
);
|
||||
});
|
||||
|
||||
test("env-var: a doc saying an override is 'not yet implemented' is NOT flagged", () => {
|
||||
const found = findingsFor({
|
||||
docs: {
|
||||
"zed.md": "A `ZED_CONFIG_PATH` environment variable override is not yet implemented.\n",
|
||||
},
|
||||
});
|
||||
assert.ok(!found.has("env-var::ZED_CONFIG_PATH"), "not-yet-implemented is an absence statement");
|
||||
});
|
||||
|
||||
test("ANTI-OVER-SUPPRESSION: a fabricated env var on a normal (non-negated) line IS still flagged", () => {
|
||||
// Same shape as the negation cases above, but WITHOUT an absence statement —
|
||||
// proves the negation skip is scoped to lines that disclaim the var, not a blanket
|
||||
// suppression of every memory/config-looking name.
|
||||
const found = findingsFor({
|
||||
docs: { "bad.md": "Set `MADE_UP_TUNING_KNOB_FOR_NOTHING` to change behavior.\n" },
|
||||
});
|
||||
assert.ok(
|
||||
found.has("env-var::MADE_UP_TUNING_KNOB_FOR_NOTHING"),
|
||||
"a fabricated env var on a plain line must still be flagged"
|
||||
);
|
||||
});
|
||||
|
||||
test("cli-cmd: an arg-bearing `.command('connect <host>')` registration is NOT flagged", () => {
|
||||
// The old extraction regex required the subcommand name to be immediately followed
|
||||
// by the closing quote, so commander's arg-bearing forms (`connect <host>`,
|
||||
// `chat [msg]`) were never indexed → a doc that referenced them was wrongly flagged.
|
||||
const found = findingsFor({
|
||||
files: {
|
||||
"bin/cli/commands/connect.mjs":
|
||||
'export function registerConnect(p) {\n p.command("connect <host>").action(() => {});\n}\n',
|
||||
},
|
||||
docs: { "guides/remote.md": "You log in once with `omniroute connect <host>`.\n" },
|
||||
});
|
||||
assert.ok(
|
||||
!found.has("cli-cmd::omniroute connect"),
|
||||
"a registered arg-bearing subcommand must be recognized and not flagged"
|
||||
);
|
||||
});
|
||||
|
||||
test("ANTI-OVER-SUPPRESSION: an unregistered subcommand IS still flagged", () => {
|
||||
// Broadening the regex must add precision, not blind detection: a doc that invokes
|
||||
// a subcommand with no `.command()` registration anywhere in bin/ must remain flagged.
|
||||
const found = findingsFor({
|
||||
files: {
|
||||
"bin/cli/commands/connect.mjs":
|
||||
'export function registerConnect(p) {\n p.command("connect <host>").action(() => {});\n}\n',
|
||||
},
|
||||
docs: { "guides/remote.md": "Then run `omniroute teleport <host>` to finish.\n" },
|
||||
});
|
||||
assert.ok(
|
||||
found.has("cli-cmd::omniroute teleport"),
|
||||
"an unregistered subcommand must remain flagged — precision must not blind detection"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -17,6 +17,7 @@ import { spawnSync } from "node:child_process";
|
||||
import {
|
||||
evaluateOpenapiRatchet,
|
||||
readBaselineOpenapiValue,
|
||||
releaseBranchForVersion,
|
||||
// @ts-expect-error — .mjs helper has no type declarations; runtime shape is known.
|
||||
} from "../../scripts/check/check-openapi-breaking.mjs";
|
||||
|
||||
@@ -26,11 +27,34 @@ const evaluate = evaluateOpenapiRatchet as (args: {
|
||||
baseline: number | null;
|
||||
}) => RatchetVerdict;
|
||||
const readBaseline = readBaselineOpenapiValue as (p?: string) => number | null;
|
||||
const releaseBranch = releaseBranchForVersion as (v: string | null | undefined) => string | null;
|
||||
|
||||
const SCRIPT_PATH = fileURLToPath(
|
||||
new URL("../../scripts/check/check-openapi-breaking.mjs", import.meta.url)
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// releaseBranchForVersion — the default base ref derives from the package
|
||||
// version so it never goes stale across release cycles (was a hard-coded
|
||||
// "origin/release/v3.8.27" that drifted into the v3.8.29 cycle).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test("releaseBranchForVersion: a clean semver derives the matching release branch", () => {
|
||||
assert.equal(releaseBranch("3.8.29"), "origin/release/v3.8.29");
|
||||
});
|
||||
|
||||
test("releaseBranchForVersion: a prerelease/build suffix is ignored", () => {
|
||||
assert.equal(releaseBranch("3.8.29-dev.2"), "origin/release/v3.8.29");
|
||||
assert.equal(releaseBranch("10.0.0+build.7"), "origin/release/v10.0.0");
|
||||
});
|
||||
|
||||
test("releaseBranchForVersion: a non-semver value yields null (caller falls back)", () => {
|
||||
assert.equal(releaseBranch(""), null);
|
||||
assert.equal(releaseBranch(null), null);
|
||||
assert.equal(releaseBranch(undefined), null);
|
||||
assert.equal(releaseBranch("not-a-version"), null);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// evaluateOpenapiRatchet — the three contract cases from the plan
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
72
tests/unit/claude-adaptive-sampling-params.test.ts
Normal file
72
tests/unit/claude-adaptive-sampling-params.test.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Claude Opus 4.7+/Fable 5 sampling-param strip + adaptive-only flag.
|
||||
*
|
||||
* Anthropic's Opus 4.7+ generation rejects non-default `temperature`/`top_p`/`top_k` with a
|
||||
* 400 (sampling is fixed; reasoning is steered by output_config.effort). These tests pin both
|
||||
* the registry `unsupportedParams` that drive the strip at the chatCore dispatch point and
|
||||
* the `isAdaptiveThinkingOnly` model flag — with regression guards that pre-4.7 models keep
|
||||
* accepting sampling params and manual thinking.
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { getUnsupportedParams } from "../../open-sse/config/providerRegistry.ts";
|
||||
import { isAdaptiveThinkingOnly } from "../../src/shared/constants/modelSpecs.ts";
|
||||
|
||||
const SAMPLING = ["temperature", "top_p", "top_k"];
|
||||
|
||||
test("claude registry strips temperature/top_p/top_k for Opus 4.7+/Fable 5", () => {
|
||||
for (const model of ["claude-opus-4-8", "claude-opus-4-7", "claude-fable-5"]) {
|
||||
const unsupported = getUnsupportedParams("claude", model);
|
||||
for (const param of SAMPLING) {
|
||||
assert.ok(
|
||||
unsupported.includes(param),
|
||||
`${model} must list ${param} as unsupported (400 on Anthropic otherwise)`
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("anthropic registry (dotted ids) strips sampling params for Opus 4.7", () => {
|
||||
const unsupported = getUnsupportedParams("anthropic", "claude-opus-4.7");
|
||||
for (const param of SAMPLING) {
|
||||
assert.ok(unsupported.includes(param), `claude-opus-4.7 must list ${param} as unsupported`);
|
||||
}
|
||||
});
|
||||
|
||||
test("pre-4.7 Claude models still accept sampling params (regression guard)", () => {
|
||||
for (const [provider, model] of [
|
||||
["claude", "claude-opus-4-6"],
|
||||
["claude", "claude-opus-4-5-20251101"],
|
||||
["claude", "claude-sonnet-4-5-20250929"],
|
||||
["claude", "claude-haiku-4-5-20251001"],
|
||||
["anthropic", "claude-opus-4.6"],
|
||||
] as const) {
|
||||
const unsupported = getUnsupportedParams(provider, model);
|
||||
for (const param of SAMPLING) {
|
||||
assert.ok(
|
||||
!unsupported.includes(param),
|
||||
`${provider}/${model} must NOT strip ${param} — it still accepts sampling`
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("isAdaptiveThinkingOnly is true only for Opus 4.7+/Fable 5", () => {
|
||||
for (const model of ["claude-opus-4-8", "claude-opus-4-7", "claude-fable-5"]) {
|
||||
assert.equal(isAdaptiveThinkingOnly(model), true, `${model} is adaptive-only`);
|
||||
}
|
||||
for (const model of [
|
||||
"claude-opus-4-6",
|
||||
"claude-opus-4-5-20251101",
|
||||
"claude-sonnet-4-6",
|
||||
"claude-haiku-4-5-20251001",
|
||||
]) {
|
||||
assert.equal(isAdaptiveThinkingOnly(model), false, `${model} still supports manual thinking`);
|
||||
}
|
||||
assert.equal(isAdaptiveThinkingOnly(null), false);
|
||||
assert.equal(isAdaptiveThinkingOnly(""), false);
|
||||
});
|
||||
|
||||
test("isAdaptiveThinkingOnly resolves Bedrock/dated aliases", () => {
|
||||
assert.equal(isAdaptiveThinkingOnly("anthropic.claude-opus-4-8"), true);
|
||||
});
|
||||
108
tests/unit/claude-adaptive-thinking-normalize.test.ts
Normal file
108
tests/unit/claude-adaptive-thinking-normalize.test.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* Claude adaptive-thinking normalization — `normalizeClaudeAdaptiveThinking`.
|
||||
*
|
||||
* Claude Opus 4.7+/Fable 5 removed manual extended thinking: `thinking.type:"enabled"` and
|
||||
* any `thinking.budget_tokens` return HTTP 400 (Anthropic migration guide, 2026-05-19).
|
||||
* These tests pin the final guard that collapses any manual thinking that reached the
|
||||
* dispatch point to `{type:"adaptive"}`, while leaving non-adaptive-only models and
|
||||
* already-adaptive bodies untouched.
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { normalizeClaudeAdaptiveThinking } from "../../open-sse/services/claudeAdaptiveThinking.ts";
|
||||
|
||||
test("manual thinking:{type:'enabled', budget_tokens} → adaptive, budget dropped (Opus 4.8)", () => {
|
||||
const body = {
|
||||
model: "claude-opus-4-8",
|
||||
messages: [],
|
||||
thinking: { type: "enabled", budget_tokens: 131072 },
|
||||
};
|
||||
const result = normalizeClaudeAdaptiveThinking(body, "claude-opus-4-8");
|
||||
assert.deepEqual(result.thinking, { type: "adaptive" });
|
||||
});
|
||||
|
||||
test("Claude-shaped thinking:{type:'enabled', max_tokens} → adaptive, max_tokens dropped", () => {
|
||||
const body = {
|
||||
model: "claude-opus-4-7",
|
||||
messages: [],
|
||||
thinking: { type: "enabled", budget_tokens: 4096, max_tokens: 8000 },
|
||||
};
|
||||
const result = normalizeClaudeAdaptiveThinking(body, "claude-opus-4-7");
|
||||
assert.deepEqual(result.thinking, { type: "adaptive" });
|
||||
});
|
||||
|
||||
test("type:'enabled' with no budget still flips to adaptive (manual mode is gone)", () => {
|
||||
const body = { model: "claude-fable-5", messages: [], thinking: { type: "enabled" } };
|
||||
const result = normalizeClaudeAdaptiveThinking(body, "claude-fable-5");
|
||||
assert.deepEqual(result.thinking, { type: "adaptive" });
|
||||
});
|
||||
|
||||
test("thinking:{type:'adaptive'} is returned UNTOUCHED (same reference)", () => {
|
||||
const body = { model: "claude-opus-4-8", messages: [], thinking: { type: "adaptive" } };
|
||||
const result = normalizeClaudeAdaptiveThinking(body, "claude-opus-4-8");
|
||||
assert.equal(result, body, "already-adaptive body must not be reallocated");
|
||||
});
|
||||
|
||||
test("adaptive thinking carrying a stray budget_tokens has it stripped", () => {
|
||||
const body = {
|
||||
model: "claude-opus-4-8",
|
||||
messages: [],
|
||||
thinking: { type: "adaptive", budget_tokens: 5 },
|
||||
};
|
||||
const result = normalizeClaudeAdaptiveThinking(body, "claude-opus-4-8");
|
||||
assert.deepEqual(result.thinking, { type: "adaptive" });
|
||||
});
|
||||
|
||||
test("thinking:{type:'disabled'} is left untouched (handled by normalizeThinkingForModel)", () => {
|
||||
const body = { model: "claude-opus-4-8", messages: [], thinking: { type: "disabled" } };
|
||||
const result = normalizeClaudeAdaptiveThinking(body, "claude-opus-4-8");
|
||||
assert.equal(result, body, "disabled is a separate concern; do not touch it here");
|
||||
});
|
||||
|
||||
test("NON-adaptive-only model keeps its manual budget (regression guard for Opus 4.6)", () => {
|
||||
const body = {
|
||||
model: "claude-opus-4-6",
|
||||
messages: [],
|
||||
thinking: { type: "enabled", budget_tokens: 96000 },
|
||||
};
|
||||
const result = normalizeClaudeAdaptiveThinking(body, "claude-opus-4-6");
|
||||
assert.equal(result, body, "Opus 4.6 still supports manual extended thinking");
|
||||
assert.deepEqual(result.thinking, { type: "enabled", budget_tokens: 96000 });
|
||||
});
|
||||
|
||||
test("body without a thinking object is returned UNTOUCHED (same reference)", () => {
|
||||
const body = { model: "claude-opus-4-8", messages: [{ role: "user", content: "hi" }] };
|
||||
const result = normalizeClaudeAdaptiveThinking(body, "claude-opus-4-8");
|
||||
assert.equal(result, body);
|
||||
});
|
||||
|
||||
test("non-object body / empty model are returned unchanged", () => {
|
||||
const body = null as unknown as Record<string, unknown>;
|
||||
assert.equal(normalizeClaudeAdaptiveThinking(body, "claude-opus-4-8"), body);
|
||||
const ok = { model: "claude-opus-4-8", thinking: { type: "enabled" } };
|
||||
assert.equal(normalizeClaudeAdaptiveThinking(ok, ""), ok, "empty model → no-op");
|
||||
});
|
||||
|
||||
test("Bedrock/dated alias still resolves the adaptive-only spec", () => {
|
||||
const body = { thinking: { type: "enabled", budget_tokens: 1000 } };
|
||||
// BEDROCK_CLAUDE_ALIASES generates `anthropic.claude-opus-4-8` etc.; getModelSpec resolves it.
|
||||
const result = normalizeClaudeAdaptiveThinking(body, "anthropic.claude-opus-4-8");
|
||||
assert.deepEqual(result.thinking, { type: "adaptive" });
|
||||
});
|
||||
|
||||
test("unrelated fields are preserved when collapsing thinking", () => {
|
||||
const body = {
|
||||
model: "claude-opus-4-8",
|
||||
messages: [{ role: "user", content: "hi" }],
|
||||
output_config: { effort: "high" },
|
||||
thinking: { type: "enabled", budget_tokens: 32000 },
|
||||
};
|
||||
const result = normalizeClaudeAdaptiveThinking(body, "claude-opus-4-8") as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
assert.equal(result.model, "claude-opus-4-8");
|
||||
assert.deepEqual(result.messages, [{ role: "user", content: "hi" }]);
|
||||
assert.deepEqual(result.output_config, { effort: "high" });
|
||||
assert.deepEqual(result.thinking, { type: "adaptive" });
|
||||
});
|
||||
35
tests/unit/cli-connect-helpers.test.ts
Normal file
35
tests/unit/cli-connect-helpers.test.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { normalizeBaseUrl, hostLabel } from "../../bin/cli/commands/connect.mjs";
|
||||
import { profileNameFromModel } from "../../bin/cli/commands/configure.mjs";
|
||||
|
||||
test("normalizeBaseUrl: bare host gets http:// and the default port", () => {
|
||||
assert.equal(normalizeBaseUrl("192.168.0.15", "20128"), "http://192.168.0.15:20128");
|
||||
});
|
||||
|
||||
test("normalizeBaseUrl: host with explicit port keeps it", () => {
|
||||
assert.equal(normalizeBaseUrl("192.168.0.15:9000", "20128"), "http://192.168.0.15:9000");
|
||||
});
|
||||
|
||||
test("normalizeBaseUrl: full https URL is preserved as origin", () => {
|
||||
assert.equal(normalizeBaseUrl("https://omni.example.com", "20128"), "https://omni.example.com");
|
||||
assert.equal(normalizeBaseUrl("http://host:1234/path", "20128"), "http://host:1234");
|
||||
});
|
||||
|
||||
test("normalizeBaseUrl: empty input returns empty string", () => {
|
||||
assert.equal(normalizeBaseUrl("", "20128"), "");
|
||||
});
|
||||
|
||||
test("hostLabel strips scheme and port", () => {
|
||||
assert.equal(hostLabel("https://omni.example.com:20128"), "omni.example.com");
|
||||
assert.equal(hostLabel("192.168.0.15:20128"), "192.168.0.15");
|
||||
assert.equal(hostLabel("http://10.0.0.1"), "10.0.0.1");
|
||||
});
|
||||
|
||||
test("profileNameFromModel strips the provider prefix and non-alphanumerics", () => {
|
||||
assert.equal(profileNameFromModel("glm/glm-5.2"), "glm52");
|
||||
assert.equal(profileNameFromModel("kmc/kimi-k2.7"), "kimik27");
|
||||
assert.equal(profileNameFromModel("ollamacloud/gpt-oss:20b"), "gptoss20b");
|
||||
assert.equal(profileNameFromModel("cx/gpt-5.5"), "gpt55");
|
||||
assert.equal(profileNameFromModel("bare-model"), "baremodel");
|
||||
});
|
||||
178
tests/unit/cli-remote-mode.test.ts
Normal file
178
tests/unit/cli-remote-mode.test.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
|
||||
// Remote-mode core: the CLI must resolve BOTH baseUrl and auth from the active
|
||||
// context (canonical `contexts`/`currentContext` schema, with legacy
|
||||
// `profiles`/`activeProfile` fallback). Before this work, getBaseUrl read only
|
||||
// the legacy `profiles` schema and buildHeaders never read the context's
|
||||
// credential at all — so `omniroute contexts use <remote>` silently failed to
|
||||
// route auth to the remote server.
|
||||
|
||||
let tmpDir: string;
|
||||
let origDataDir: string | undefined;
|
||||
let origBaseUrl: string | undefined;
|
||||
let origApiKey: string | undefined;
|
||||
let origContext: string | undefined;
|
||||
|
||||
function writeConfig(cfg: unknown): void {
|
||||
mkdirSync(tmpDir, { recursive: true });
|
||||
writeFileSync(join(tmpDir, "config.json"), JSON.stringify(cfg, null, 2));
|
||||
}
|
||||
|
||||
test.before(() => {
|
||||
tmpDir = mkdtempSync(join(tmpdir(), "omniroute-remote-test-"));
|
||||
origDataDir = process.env.DATA_DIR;
|
||||
origBaseUrl = process.env.OMNIROUTE_BASE_URL;
|
||||
origApiKey = process.env.OMNIROUTE_API_KEY;
|
||||
origContext = process.env.OMNIROUTE_CONTEXT;
|
||||
process.env.DATA_DIR = tmpDir;
|
||||
delete process.env.OMNIROUTE_BASE_URL;
|
||||
delete process.env.OMNIROUTE_API_KEY;
|
||||
delete process.env.OMNIROUTE_CONTEXT;
|
||||
});
|
||||
|
||||
test.after(() => {
|
||||
if (origDataDir === undefined) delete process.env.DATA_DIR;
|
||||
else process.env.DATA_DIR = origDataDir;
|
||||
if (origBaseUrl === undefined) delete process.env.OMNIROUTE_BASE_URL;
|
||||
else process.env.OMNIROUTE_BASE_URL = origBaseUrl;
|
||||
if (origApiKey === undefined) delete process.env.OMNIROUTE_API_KEY;
|
||||
else process.env.OMNIROUTE_API_KEY = origApiKey;
|
||||
if (origContext === undefined) delete process.env.OMNIROUTE_CONTEXT;
|
||||
else process.env.OMNIROUTE_CONTEXT = origContext;
|
||||
try {
|
||||
rmSync(tmpDir, { recursive: true, force: true });
|
||||
} catch {}
|
||||
});
|
||||
|
||||
// ── getBaseUrl ────────────────────────────────────────────────────────────────
|
||||
|
||||
test("getBaseUrl reads baseUrl from the active context (canonical schema)", async () => {
|
||||
writeConfig({
|
||||
version: 1,
|
||||
currentContext: "vps",
|
||||
contexts: {
|
||||
default: { baseUrl: "http://localhost:20128", apiKey: null },
|
||||
vps: { baseUrl: "https://vps.example.com:20128", accessToken: "oma_live_x", scope: "write" },
|
||||
},
|
||||
});
|
||||
const { getBaseUrl } = await import("../../bin/cli/api.mjs");
|
||||
assert.equal(getBaseUrl(), "https://vps.example.com:20128");
|
||||
});
|
||||
|
||||
test("getBaseUrl honors the --context override", async () => {
|
||||
writeConfig({
|
||||
version: 1,
|
||||
currentContext: "default",
|
||||
contexts: {
|
||||
default: { baseUrl: "http://localhost:20128", apiKey: null },
|
||||
staging: { baseUrl: "http://staging:20128", apiKey: null },
|
||||
},
|
||||
});
|
||||
const { getBaseUrl } = await import("../../bin/cli/api.mjs");
|
||||
assert.equal(getBaseUrl({ context: "staging" }), "http://staging:20128");
|
||||
});
|
||||
|
||||
test("getBaseUrl is backward-compatible with the legacy profiles schema", async () => {
|
||||
writeConfig({
|
||||
version: 1,
|
||||
activeProfile: "old",
|
||||
profiles: { old: { baseUrl: "http://legacy:20128" } },
|
||||
});
|
||||
const { getBaseUrl } = await import("../../bin/cli/api.mjs");
|
||||
assert.equal(getBaseUrl(), "http://legacy:20128");
|
||||
});
|
||||
|
||||
test("getBaseUrl: opts.baseUrl wins over the active context", async () => {
|
||||
writeConfig({
|
||||
version: 1,
|
||||
currentContext: "vps",
|
||||
contexts: { vps: { baseUrl: "https://vps.example.com" } },
|
||||
});
|
||||
const { getBaseUrl } = await import("../../bin/cli/api.mjs");
|
||||
assert.equal(getBaseUrl({ baseUrl: "http://override:1234" }), "http://override:1234");
|
||||
});
|
||||
|
||||
// ── buildHeaders (auth resolution) ─────────────────────────────────────────────
|
||||
|
||||
test("buildHeaders injects Bearer from the active context accessToken", async () => {
|
||||
writeConfig({
|
||||
version: 1,
|
||||
currentContext: "vps",
|
||||
contexts: { vps: { baseUrl: "https://vps.example.com", accessToken: "oma_live_secret" } },
|
||||
});
|
||||
const { buildHeaders } = await import("../../bin/cli/api.mjs");
|
||||
const headers = await buildHeaders({ cliToken: "" });
|
||||
assert.equal(headers.get("authorization"), "Bearer oma_live_secret");
|
||||
});
|
||||
|
||||
test("buildHeaders prefers accessToken over apiKey in the same context", async () => {
|
||||
writeConfig({
|
||||
version: 1,
|
||||
currentContext: "vps",
|
||||
contexts: {
|
||||
vps: { baseUrl: "https://vps.example.com", accessToken: "oma_token", apiKey: "sk-legacy" },
|
||||
},
|
||||
});
|
||||
const { buildHeaders } = await import("../../bin/cli/api.mjs");
|
||||
const headers = await buildHeaders({ cliToken: "" });
|
||||
assert.equal(headers.get("authorization"), "Bearer oma_token");
|
||||
});
|
||||
|
||||
test("buildHeaders falls back to the context apiKey when no accessToken", async () => {
|
||||
writeConfig({
|
||||
version: 1,
|
||||
currentContext: "vps",
|
||||
contexts: { vps: { baseUrl: "https://vps.example.com", apiKey: "sk-ctx" } },
|
||||
});
|
||||
const { buildHeaders } = await import("../../bin/cli/api.mjs");
|
||||
const headers = await buildHeaders({ cliToken: "" });
|
||||
assert.equal(headers.get("authorization"), "Bearer sk-ctx");
|
||||
});
|
||||
|
||||
test("buildHeaders: explicit opts.apiKey wins over the context credential", async () => {
|
||||
writeConfig({
|
||||
version: 1,
|
||||
currentContext: "vps",
|
||||
contexts: { vps: { baseUrl: "https://vps.example.com", accessToken: "oma_token" } },
|
||||
});
|
||||
const { buildHeaders } = await import("../../bin/cli/api.mjs");
|
||||
const headers = await buildHeaders({ cliToken: "", apiKey: "sk-explicit" });
|
||||
assert.equal(headers.get("authorization"), "Bearer sk-explicit");
|
||||
});
|
||||
|
||||
// ── context current command ─────────────────────────────────────────────────────
|
||||
|
||||
test("commands/contexts.mjs registers a `current` subcommand", async () => {
|
||||
const { registerContexts } = await import("../../bin/cli/commands/contexts.mjs");
|
||||
// Minimal fake commander program to capture subcommand registration.
|
||||
const sub: string[] = [];
|
||||
const fakeCtx: any = {
|
||||
command(name: string) {
|
||||
sub.push(name.split(" ")[0]);
|
||||
return this;
|
||||
},
|
||||
description() {
|
||||
return this;
|
||||
},
|
||||
requiredOption() {
|
||||
return this;
|
||||
},
|
||||
option() {
|
||||
return this;
|
||||
},
|
||||
action() {
|
||||
return this;
|
||||
},
|
||||
};
|
||||
const fakeProgram: any = {
|
||||
command() {
|
||||
return fakeCtx;
|
||||
},
|
||||
};
|
||||
registerContexts(fakeProgram);
|
||||
assert.ok(sub.includes("current"), `expected a 'current' subcommand, got: ${sub.join(", ")}`);
|
||||
});
|
||||
149
tests/unit/cloud-agent-cursor-4227.test.ts
Normal file
149
tests/unit/cloud-agent-cursor-4227.test.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* #4227 — Cursor Cloud Agent (REST adapter).
|
||||
*
|
||||
* Validates the adapter mapping between Cursor's Background/Cloud Agents REST API and
|
||||
* OmniRoute's CloudAgentBase contract (status mapping, request shape, result extraction)
|
||||
* with a mocked fetch. NOTE: this proves the adapter's internal mapping, NOT the live
|
||||
* Cursor API shapes — those need a live validation run with a real Cursor API key before
|
||||
* merge (Rule #18, external-API integration; see the PR description).
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const cursorMod = await import("../../src/lib/cloudAgent/agents/cursor.ts");
|
||||
const registry = await import("../../src/lib/cloudAgent/registry.ts");
|
||||
|
||||
const CREDS = { apiKey: "key_test_123" };
|
||||
const SOURCE = { repoName: "org/repo", repoUrl: "https://github.com/org/repo", branch: "main" };
|
||||
const OPTIONS = { autoCreatePr: true };
|
||||
|
||||
function mockFetch(handler: (url: string, init?: RequestInit) => Response | Promise<Response>) {
|
||||
const original = globalThis.fetch;
|
||||
// @ts-expect-error test shim
|
||||
globalThis.fetch = async (url: string | URL | Request, init?: RequestInit) =>
|
||||
handler(String(url), init);
|
||||
return () => {
|
||||
globalThis.fetch = original;
|
||||
};
|
||||
}
|
||||
|
||||
test("#4227 registry exposes cursor-cloud as a cloud-agent provider", () => {
|
||||
assert.equal(registry.isCloudAgentProvider("cursor-cloud"), true);
|
||||
const agent = registry.getAgent("cursor-cloud");
|
||||
assert.ok(agent, "getAgent('cursor-cloud') returns an instance");
|
||||
assert.equal(agent?.providerId, "cursor-cloud");
|
||||
assert.ok(registry.getAvailableAgents().includes("cursor-cloud"));
|
||||
});
|
||||
|
||||
test("#4227 createTask posts the prompt+repo and maps CREATING → queued", async () => {
|
||||
const agent = new cursorMod.CursorCloudAgent();
|
||||
let captured: { url: string; body: any } | null = null;
|
||||
const restore = mockFetch((url, init) => {
|
||||
captured = { url, body: init?.body ? JSON.parse(String(init.body)) : null };
|
||||
return Response.json({ id: "bc-abc123", status: "CREATING", name: "agent-1" });
|
||||
});
|
||||
try {
|
||||
const task = await agent.createTask({ prompt: "fix the bug", source: SOURCE, options: OPTIONS }, CREDS);
|
||||
assert.equal(task.providerId, "cursor-cloud");
|
||||
assert.equal(task.externalId, "bc-abc123");
|
||||
assert.equal(task.status, "queued");
|
||||
assert.equal(task.prompt, "fix the bug");
|
||||
// request shape
|
||||
assert.ok(captured, "fetch was called");
|
||||
assert.match(captured!.url, /\/agents$/);
|
||||
assert.equal(captured!.body.prompt.text, "fix the bug");
|
||||
assert.equal(captured!.body.source.repository, "https://github.com/org/repo");
|
||||
assert.equal(captured!.body.source.ref, "main");
|
||||
assert.equal(captured!.body.autoCreatePr, true);
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
});
|
||||
|
||||
test("#4227 createTask surfaces an upstream error instead of swallowing it", async () => {
|
||||
const agent = new cursorMod.CursorCloudAgent();
|
||||
const restore = mockFetch(() => new Response("nope", { status: 401 }));
|
||||
try {
|
||||
await assert.rejects(
|
||||
agent.createTask({ prompt: "x", source: SOURCE, options: {} }, CREDS),
|
||||
/Cursor create agent failed: 401/
|
||||
);
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
});
|
||||
|
||||
test("#4227 getStatus maps FINISHED → completed and extracts the PR url + conversation", async () => {
|
||||
const agent = new cursorMod.CursorCloudAgent();
|
||||
const restore = mockFetch(() =>
|
||||
Response.json({
|
||||
id: "bc-abc123",
|
||||
status: "FINISHED",
|
||||
target: { prUrl: "https://github.com/org/repo/pull/7", branchName: "cursor/fix" },
|
||||
summary: "Fixed it",
|
||||
conversation: [{ type: "assistant_message", text: "done", createdAt: "2026-06-19T00:00:00Z" }],
|
||||
})
|
||||
);
|
||||
try {
|
||||
const result = await agent.getStatus("bc-abc123", CREDS);
|
||||
assert.equal(result.status, "completed");
|
||||
assert.equal(result.result?.prUrl, "https://github.com/org/repo/pull/7");
|
||||
assert.equal(result.result?.summary, "Fixed it");
|
||||
assert.equal(result.activities.length, 1);
|
||||
assert.equal(result.activities[0].content, "done");
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
});
|
||||
|
||||
test("#4227 getStatus maps Cursor enums (RUNNING→running, ERROR→failed)", async () => {
|
||||
const agent = new cursorMod.CursorCloudAgent();
|
||||
let restore = mockFetch(() => Response.json({ status: "RUNNING" }));
|
||||
try {
|
||||
assert.equal((await agent.getStatus("id", CREDS)).status, "running");
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
restore = mockFetch(() => Response.json({ status: "ERROR", error: "boom" }));
|
||||
try {
|
||||
const r = await agent.getStatus("id", CREDS);
|
||||
assert.equal(r.status, "failed");
|
||||
assert.equal(r.error, "boom");
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
});
|
||||
|
||||
test("#4227 sendMessage posts a followup; approvePlan is unsupported", async () => {
|
||||
const agent = new cursorMod.CursorCloudAgent();
|
||||
let captured: { url: string; body: any } | null = null;
|
||||
const restore = mockFetch((url, init) => {
|
||||
captured = { url, body: init?.body ? JSON.parse(String(init.body)) : null };
|
||||
return Response.json({ ok: true });
|
||||
});
|
||||
try {
|
||||
const activity = await agent.sendMessage("bc-1", "also add tests", CREDS);
|
||||
assert.equal(activity.type, "message");
|
||||
assert.equal(activity.content, "also add tests");
|
||||
assert.match(captured!.url, /\/agents\/bc-1\/followup$/);
|
||||
assert.equal(captured!.body.prompt.text, "also add tests");
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
await assert.rejects(agent.approvePlan("bc-1", CREDS), /do not support plan approval/);
|
||||
});
|
||||
|
||||
test("#4227 listSources normalizes the repositories list", async () => {
|
||||
const agent = new cursorMod.CursorCloudAgent();
|
||||
const restore = mockFetch(() =>
|
||||
Response.json({ repositories: [{ url: "https://github.com/org/repo", name: "org/repo" }] })
|
||||
);
|
||||
try {
|
||||
const sources = await agent.listSources(CREDS);
|
||||
assert.equal(sources.length, 1);
|
||||
assert.equal(sources[0].url, "https://github.com/org/repo");
|
||||
assert.equal(sources[0].name, "org/repo");
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
});
|
||||
@@ -3,9 +3,9 @@ import { describe, test } from "node:test";
|
||||
import { getAvailableAgents } from "../../src/lib/cloudAgent/registry.ts";
|
||||
|
||||
describe("cloud-agent health API — getAvailableAgents", () => {
|
||||
test("returns exactly three agents", () => {
|
||||
test("returns exactly four agents", () => {
|
||||
const agents = getAvailableAgents();
|
||||
assert.equal(agents.length, 3);
|
||||
assert.equal(agents.length, 4);
|
||||
});
|
||||
|
||||
test('includes "jules"', () => {
|
||||
@@ -20,8 +20,12 @@ describe("cloud-agent health API — getAvailableAgents", () => {
|
||||
assert.ok(getAvailableAgents().includes("codex-cloud"));
|
||||
});
|
||||
|
||||
test('includes "cursor-cloud"', () => {
|
||||
assert.ok(getAvailableAgents().includes("cursor-cloud"));
|
||||
});
|
||||
|
||||
test("returns agents in expected order", () => {
|
||||
assert.deepEqual(getAvailableAgents(), ["jules", "devin", "codex-cloud"]);
|
||||
assert.deepEqual(getAvailableAgents(), ["jules", "devin", "codex-cloud", "cursor-cloud"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -64,11 +68,13 @@ describe("cloud-agent health API — health check logic", () => {
|
||||
jules: "Jules",
|
||||
devin: "Devin",
|
||||
"codex-cloud": "Codex Cloud",
|
||||
"cursor-cloud": "Cursor Cloud",
|
||||
};
|
||||
|
||||
assert.equal(PROVIDER_NAMES["jules"], "Jules");
|
||||
assert.equal(PROVIDER_NAMES["devin"], "Devin");
|
||||
assert.equal(PROVIDER_NAMES["codex-cloud"], "Codex Cloud");
|
||||
assert.equal(PROVIDER_NAMES["cursor-cloud"], "Cursor Cloud");
|
||||
assert.equal(PROVIDER_NAMES["nonexistent"], undefined);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -131,7 +131,8 @@ test("getAvailableAgents returns all registered agents", () => {
|
||||
assert.strictEqual(agents.includes("jules"), true);
|
||||
assert.strictEqual(agents.includes("devin"), true);
|
||||
assert.strictEqual(agents.includes("codex-cloud"), true);
|
||||
assert.strictEqual(agents.length, 3);
|
||||
assert.strictEqual(agents.includes("cursor-cloud"), true);
|
||||
assert.strictEqual(agents.length, 4);
|
||||
});
|
||||
|
||||
test("getAgent returns correct agent for valid providerId", () => {
|
||||
|
||||
@@ -4,6 +4,8 @@ import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import { OMNIROUTE_RESPONSE_HEADERS } from "../../src/shared/constants/headers.ts";
|
||||
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-codex-stream-false-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
|
||||
@@ -365,3 +367,49 @@ test("handleComboChat validates non-stream quality using the original client str
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
test("non-stream chat success carries cost-telemetry meta headers (cost/version/tokens)", async () => {
|
||||
// Regression guard: the single non-stream success return in chatCore.ts routes
|
||||
// through attachOmniRouteMetaHeaders, which must always emit the cost-telemetry
|
||||
// headers. A usage-bearing JSON upstream body proves real usage flowed through.
|
||||
const { result } = await invokeChatCore({
|
||||
body: {
|
||||
model: "gpt-4o-mini",
|
||||
stream: false,
|
||||
messages: [{ role: "user", content: "Qual a capital do Brasil?" }],
|
||||
},
|
||||
provider: "openai",
|
||||
model: "gpt-4o-mini",
|
||||
responseFactory: () =>
|
||||
jsonResponse({
|
||||
id: "chatcmpl-cost-header",
|
||||
object: "chat.completion",
|
||||
model: "gpt-4o-mini",
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
message: { role: "assistant", content: "Brasilia" },
|
||||
finish_reason: "stop",
|
||||
},
|
||||
],
|
||||
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
|
||||
}),
|
||||
});
|
||||
|
||||
assert.equal(result.success, true);
|
||||
|
||||
const headers = result.response.headers;
|
||||
|
||||
const cost = headers.get(OMNIROUTE_RESPONSE_HEADERS.responseCost);
|
||||
assert.equal(typeof cost, "string");
|
||||
// 10-decimal cost format; 0.0000000000 is valid when no pricing is available.
|
||||
assert.match(String(cost), /^\d+\.\d{10}$/);
|
||||
|
||||
const version = headers.get(OMNIROUTE_RESPONSE_HEADERS.version);
|
||||
assert.equal(typeof version, "string");
|
||||
assert.ok(String(version).length > 0);
|
||||
|
||||
// Real usage from the upstream body must surface as token-count headers.
|
||||
assert.equal(headers.get(OMNIROUTE_RESPONSE_HEADERS.tokensIn), "10");
|
||||
assert.equal(headers.get(OMNIROUTE_RESPONSE_HEADERS.tokensOut), "5");
|
||||
});
|
||||
|
||||
57
tests/unit/codex-verbosity.test.ts
Normal file
57
tests/unit/codex-verbosity.test.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Codex verbosity normalization — `normalizeCodexVerbosity`.
|
||||
*
|
||||
* GPT-5 verbosity is `verbosity` on Chat Completions and `text.verbosity` on the Responses
|
||||
* API. The CodexExecutor allowlist drops `text`, so translated requests lose the hint. These
|
||||
* tests pin the fold into a single validated `text:{verbosity}` (or its removal when invalid),
|
||||
* which — paired with `text` added to the allowlist — lets verbosity reach the Codex backend.
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { normalizeCodexVerbosity } from "../../open-sse/services/codexVerbosity.ts";
|
||||
|
||||
test("chat-style top-level verbosity is lifted to text.verbosity and dropped", () => {
|
||||
const body: Record<string, unknown> = { model: "gpt-5.5", verbosity: "low", input: [] };
|
||||
normalizeCodexVerbosity(body);
|
||||
assert.deepEqual(body.text, { verbosity: "low" });
|
||||
assert.equal(body.verbosity, undefined);
|
||||
});
|
||||
|
||||
test("existing Responses text.verbosity is preserved", () => {
|
||||
const body: Record<string, unknown> = { text: { verbosity: "high" }, input: [] };
|
||||
normalizeCodexVerbosity(body);
|
||||
assert.deepEqual(body.text, { verbosity: "high" });
|
||||
});
|
||||
|
||||
test("chat-style verbosity takes precedence when both shapes are present", () => {
|
||||
const body: Record<string, unknown> = { verbosity: "medium", text: { verbosity: "high" } };
|
||||
normalizeCodexVerbosity(body);
|
||||
assert.deepEqual(body.text, { verbosity: "medium" });
|
||||
assert.equal(body.verbosity, undefined);
|
||||
});
|
||||
|
||||
test("invalid verbosity is dropped and text removed", () => {
|
||||
const body: Record<string, unknown> = { verbosity: "ultra", input: [] };
|
||||
normalizeCodexVerbosity(body);
|
||||
assert.equal(body.text, undefined);
|
||||
assert.equal(body.verbosity, undefined);
|
||||
});
|
||||
|
||||
test("no verbosity + stray non-verbosity text → text removed (status quo)", () => {
|
||||
const body: Record<string, unknown> = { text: { format: { type: "json" } }, input: [] };
|
||||
normalizeCodexVerbosity(body);
|
||||
assert.equal(body.text, undefined);
|
||||
});
|
||||
|
||||
test("verbosity is normalized case-insensitively", () => {
|
||||
const body: Record<string, unknown> = { verbosity: "HIGH" };
|
||||
normalizeCodexVerbosity(body);
|
||||
assert.deepEqual(body.text, { verbosity: "high" });
|
||||
});
|
||||
|
||||
test("body without any verbosity is left without a text field", () => {
|
||||
const body: Record<string, unknown> = { model: "gpt-5.5", input: [] };
|
||||
normalizeCodexVerbosity(body);
|
||||
assert.equal(body.text, undefined);
|
||||
assert.equal(body.model, "gpt-5.5");
|
||||
});
|
||||
@@ -203,3 +203,41 @@ test("pre-screen: backward compatible with all targets available", async () => {
|
||||
assert.equal(calls.length, 1);
|
||||
assert.equal(calls[0], "p1/m1");
|
||||
});
|
||||
|
||||
test("priority combo: quota 429 on passthrough provider does not skip another model on same provider", async () => {
|
||||
const calls: string[] = [];
|
||||
|
||||
const combo = await combosDb.createCombo({
|
||||
name: "passthrough-quota-scope",
|
||||
strategy: "priority",
|
||||
models: ["antigravity/claude-opus-4-6-thinking", "antigravity/gemini-3-flash-agent"],
|
||||
});
|
||||
|
||||
const response = await handleComboChat({
|
||||
body: { ...reqBody, model: combo.name },
|
||||
combo,
|
||||
allCombos: [combo],
|
||||
isModelAvailable: async () => true,
|
||||
relayOptions: undefined,
|
||||
signal: undefined,
|
||||
settings: {},
|
||||
log: makeLog(),
|
||||
handleSingleModel: async (_body: unknown, modelStr: string) => {
|
||||
calls.push(modelStr);
|
||||
if (modelStr.includes("claude-opus")) {
|
||||
return Response.json(
|
||||
{ error: { message: "quota exhausted for claude-opus-4-6-thinking" } },
|
||||
{ status: 429 }
|
||||
);
|
||||
}
|
||||
return okResponse(modelStr);
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(calls.at(-1), "antigravity/gemini-3-flash-agent");
|
||||
assert.ok(
|
||||
calls.includes("antigravity/claude-opus-4-6-thinking"),
|
||||
"first passthrough model should be attempted before fallback"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { applyCompression } from "../../../open-sse/services/compression/strategySelector.ts";
|
||||
import { registerCompressionEngine } from "../../../open-sse/services/compression/index.ts";
|
||||
import type { CompressionEngine } from "../../../open-sse/services/compression/engines/types.ts";
|
||||
import type { CompressionConfig } from "../../../open-sse/services/compression/types.ts";
|
||||
|
||||
// The combo proactive-fallback path calls applyCompression(stacked); a throwing engine there
|
||||
// propagates and is swallowed as a "Speculative task error", silently dropping the target. The
|
||||
// fix lets that caller opt into the TV1 bail-out (enabled, minGainPercent 0 = skip-on-throw
|
||||
// without changing the min-gain advance behavior). This documents the opt-in contract.
|
||||
const THROWING_ENGINE: CompressionEngine = {
|
||||
id: "applycompress-bailout-throw",
|
||||
name: "Throwing Test Engine",
|
||||
description: "test fixture",
|
||||
icon: "",
|
||||
targets: ["messages"],
|
||||
stackable: true,
|
||||
stackPriority: 999,
|
||||
metadata: {
|
||||
id: "applycompress-bailout-throw",
|
||||
name: "Throwing Test Engine",
|
||||
description: "test",
|
||||
inputScope: "messages",
|
||||
targetLatencyMs: 1,
|
||||
supportsPreview: false,
|
||||
stable: false,
|
||||
},
|
||||
apply() {
|
||||
throw new Error("boom from fallback engine");
|
||||
},
|
||||
compress() {
|
||||
throw new Error("boom from fallback engine");
|
||||
},
|
||||
getConfigSchema() {
|
||||
return [];
|
||||
},
|
||||
validateConfig() {
|
||||
return { valid: true, errors: [] };
|
||||
},
|
||||
};
|
||||
|
||||
function stackedConfig(): CompressionConfig {
|
||||
return {
|
||||
stackedPipeline: [{ engine: "applycompress-bailout-throw" }],
|
||||
} as unknown as CompressionConfig;
|
||||
}
|
||||
|
||||
describe("applyCompression — combo fallback bail-out opt-in", () => {
|
||||
it("opting into bail-out skips a throwing engine instead of propagating (no silent drop)", () => {
|
||||
registerCompressionEngine(THROWING_ENGINE);
|
||||
const body = { messages: [{ role: "user", content: "hello world" }] };
|
||||
|
||||
let result: ReturnType<typeof applyCompression> | undefined;
|
||||
assert.doesNotThrow(() => {
|
||||
result = applyCompression(body, "stacked", {
|
||||
config: stackedConfig(),
|
||||
bailout: { enabled: true, minGainPercent: 0 },
|
||||
});
|
||||
});
|
||||
assert.ok(result, "returns a result instead of throwing");
|
||||
assert.equal(result!.compressed, false, "a crashing engine compresses nothing");
|
||||
});
|
||||
|
||||
it("without bail-out, a throwing engine still propagates (TV1 default unchanged)", () => {
|
||||
registerCompressionEngine(THROWING_ENGINE);
|
||||
const body = { messages: [{ role: "user", content: "hello world" }] };
|
||||
|
||||
assert.throws(
|
||||
() => applyCompression(body, "stacked", { config: stackedConfig() }),
|
||||
/boom from fallback engine/,
|
||||
"the default (opt-out) path is unchanged — bail-out is strictly opt-in"
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -11,7 +11,6 @@ import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
ccrEngine,
|
||||
reconstructCcr,
|
||||
retrieveBlock,
|
||||
recordRetrieval,
|
||||
shouldSkipCompression,
|
||||
@@ -109,30 +108,6 @@ describe("ccr engine", () => {
|
||||
assert.equal(retrieved, LARGE_TEXT, "retrieved block must equal the original verbatim text");
|
||||
});
|
||||
|
||||
it("reconstructs the body to deep-equal the original (round-trip)", () => {
|
||||
resetCcrStore();
|
||||
const originalMessages = [
|
||||
{ role: "user", content: LARGE_TEXT },
|
||||
{ role: "assistant", content: "I understand your point." },
|
||||
];
|
||||
const body = makeBody(originalMessages);
|
||||
const result = ccrEngine.apply(body as Record<string, unknown>);
|
||||
|
||||
assert.equal(result.compressed, true, "should be compressed");
|
||||
|
||||
const reconstructed = reconstructCcr(result.body);
|
||||
const reconstructedMessages = reconstructed.messages as Array<{
|
||||
role: string;
|
||||
content: string;
|
||||
}>;
|
||||
|
||||
assert.deepEqual(
|
||||
reconstructedMessages,
|
||||
originalMessages,
|
||||
"reconstructed messages must deep-equal original messages"
|
||||
);
|
||||
});
|
||||
|
||||
it("does NOT compress small blocks (below minChars threshold)", () => {
|
||||
resetCcrStore();
|
||||
const body = makeBody([{ role: "user", content: SMALL_TEXT }]);
|
||||
@@ -205,14 +180,6 @@ describe("ccr engine", () => {
|
||||
assert.equal(result, null, "unknown hash must return null");
|
||||
});
|
||||
|
||||
it("reconstructCcr is idempotent when no markers are present", () => {
|
||||
const body = makeBody([{ role: "user", content: SMALL_TEXT }]);
|
||||
const reconstructed = reconstructCcr(body as Record<string, unknown>);
|
||||
assert.deepEqual(reconstructed.messages as Array<{ role: string; content: string }>, [
|
||||
{ role: "user", content: SMALL_TEXT },
|
||||
]);
|
||||
});
|
||||
|
||||
it("handles multipart content (type:text parts)", () => {
|
||||
resetCcrStore();
|
||||
const body = {
|
||||
|
||||
49
tests/unit/compression/compression-benchmark-cli.test.ts
Normal file
49
tests/unit/compression/compression-benchmark-cli.test.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
BENCHMARK_CORPUS,
|
||||
DEFAULT_BENCHMARK_ENGINES,
|
||||
benchmarkEngines,
|
||||
compareReports,
|
||||
formatBenchmarkTable,
|
||||
} from "../../../open-sse/services/compression/harness/benchmark.ts";
|
||||
|
||||
// F2.4: the benchmark harness existed but nothing invoked it and there was no report output.
|
||||
// These cover the CLI's core: a markdown formatter + a default engine set that resolves and
|
||||
// runs the corpus end-to-end, so `npm run bench:compression` produces a real A/B table.
|
||||
describe("compression benchmark CLI core", () => {
|
||||
it("formatBenchmarkTable renders a markdown table with the best engine bolded", () => {
|
||||
const md = formatBenchmarkTable([
|
||||
{ engine: "rtk", meanSavingsPercent: 42.5, meanRetention: 0.9, totalCompressedTokens: 100 },
|
||||
{ engine: "lite", meanSavingsPercent: 10, meanRetention: 0.99, totalCompressedTokens: 200 },
|
||||
]);
|
||||
assert.match(md, /\| Engine \|/, "has a header row");
|
||||
assert.match(md, /\*\*rtk\*\*/, "best (first) engine is bolded");
|
||||
assert.match(md, /42\.5/, "renders the savings number");
|
||||
assert.match(md, /\blite\b/, "renders the runner-up");
|
||||
assert.ok(!md.includes("**lite**"), "only the best engine is bolded");
|
||||
});
|
||||
|
||||
it("DEFAULT_BENCHMARK_ENGINES all resolve and run the corpus end-to-end", async () => {
|
||||
assert.ok(DEFAULT_BENCHMARK_ENGINES.length > 0, "has a default engine set");
|
||||
assert.ok(
|
||||
!DEFAULT_BENCHMARK_ENGINES.includes("llmlingua"),
|
||||
"excludes llmlingua (needs the ONNX model at runtime)"
|
||||
);
|
||||
|
||||
const reports = await benchmarkEngines(BENCHMARK_CORPUS, DEFAULT_BENCHMARK_ENGINES);
|
||||
const rows = compareReports(reports);
|
||||
assert.equal(rows.length, DEFAULT_BENCHMARK_ENGINES.length);
|
||||
for (const id of DEFAULT_BENCHMARK_ENGINES) {
|
||||
assert.ok(
|
||||
rows.some((r) => r.engine === id),
|
||||
`${id} present in the comparison table`
|
||||
);
|
||||
}
|
||||
// The formatted table is non-empty and lists every engine.
|
||||
const md = formatBenchmarkTable(rows);
|
||||
for (const id of DEFAULT_BENCHMARK_ENGINES) {
|
||||
assert.ok(md.includes(id), `${id} appears in the markdown table`);
|
||||
}
|
||||
});
|
||||
});
|
||||
55
tests/unit/compression/compression-budget-gate.test.ts
Normal file
55
tests/unit/compression/compression-budget-gate.test.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { describe, it } 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 {
|
||||
BENCHMARK_CORPUS,
|
||||
DEFAULT_BENCHMARK_ENGINES,
|
||||
benchmarkEngines,
|
||||
runBenchmarkGate,
|
||||
} from "../../../open-sse/services/compression/harness/benchmark.ts";
|
||||
import {
|
||||
tokensPerTask,
|
||||
type BudgetBaseline,
|
||||
} from "../../../open-sse/services/compression/harness/budgetGate.ts";
|
||||
|
||||
const BASELINE_PATH = path.join(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
"../../../scripts/check/compression-budget-baseline.json"
|
||||
);
|
||||
|
||||
describe("compression budget gate (F2.4 / N4 ratchet)", () => {
|
||||
it("the committed baseline matches the current benchmark (not stale, no regression)", async () => {
|
||||
const baselines = JSON.parse(fs.readFileSync(BASELINE_PATH, "utf8")) as Record<
|
||||
string,
|
||||
BudgetBaseline
|
||||
>;
|
||||
const reports = await benchmarkEngines(BENCHMARK_CORPUS, DEFAULT_BENCHMARK_ENGINES);
|
||||
const failed = runBenchmarkGate(reports, baselines, 2).filter((r) => !r.gate.passed);
|
||||
assert.equal(
|
||||
failed.length,
|
||||
0,
|
||||
`tokens-per-task regressed vs the committed baseline: ${JSON.stringify(failed)} — ` +
|
||||
`if intentional, run: npm run check:compression-budget -- --update`
|
||||
);
|
||||
});
|
||||
|
||||
it("flags a regression when tokens-per-task rises beyond tolerance", async () => {
|
||||
// ultra is stateless/deterministic. Build a baseline tighter than reality → current regresses.
|
||||
const reports = await benchmarkEngines(BENCHMARK_CORPUS, ["ultra"]);
|
||||
const tight: Record<string, BudgetBaseline> = {
|
||||
ultra: {
|
||||
tasks: Object.fromEntries(
|
||||
Object.entries(tokensPerTask(reports.ultra)).map(([task, n]) => [
|
||||
task,
|
||||
Math.max(1, Math.floor(n / 2)),
|
||||
])
|
||||
),
|
||||
},
|
||||
};
|
||||
const failed = runBenchmarkGate(reports, tight, 2).filter((r) => !r.gate.passed);
|
||||
assert.ok(failed.length > 0, "a halved baseline must be flagged as a regression");
|
||||
});
|
||||
});
|
||||
28
tests/unit/compression/compression-registry-clear.test.ts
Normal file
28
tests/unit/compression/compression-registry-clear.test.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
registerBuiltinCompressionEngines,
|
||||
clearCompressionEngineRegistry,
|
||||
getCompressionEngine,
|
||||
} from "../../../open-sse/services/compression/index.ts";
|
||||
|
||||
// F5.3: registerBuiltinCompressionEngines() guards on a module-level `registered` latch.
|
||||
// clearCompressionEngineRegistry() empties the engine map but does NOT reset that latch, so
|
||||
// a subsequent registerBuiltinCompressionEngines() no-ops and leaves the registry empty —
|
||||
// any getCompressionEngine() then returns null. This file runs in its own process so the
|
||||
// latch starts false (no cross-file interference).
|
||||
describe("builtin compression engine registration after clear", () => {
|
||||
it("re-registers builtin engines after the registry is cleared", () => {
|
||||
registerBuiltinCompressionEngines();
|
||||
assert.ok(getCompressionEngine("rtk"), "builtins registered initially");
|
||||
|
||||
clearCompressionEngineRegistry();
|
||||
assert.equal(getCompressionEngine("rtk"), null, "registry emptied by clear");
|
||||
|
||||
// The `registered` latch must not block this re-registration.
|
||||
registerBuiltinCompressionEngines();
|
||||
assert.ok(getCompressionEngine("rtk"), "builtins must repopulate after a clear");
|
||||
assert.ok(getCompressionEngine("session-dedup"), "all builtins restored, not just one");
|
||||
});
|
||||
});
|
||||
135
tests/unit/compression/compression-step-streaming.test.ts
Normal file
135
tests/unit/compression/compression-step-streaming.test.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
stepEventsToRunModel,
|
||||
appendInFlightStep,
|
||||
clearInFlightOnComplete,
|
||||
} from "../../../src/app/(dashboard)/dashboard/compression/studio/compressionFlowModel.ts";
|
||||
import { applyStackedCompression } from "../../../open-sse/services/compression/strategySelector.ts";
|
||||
import { registerCompressionEngine } from "../../../open-sse/services/compression/index.ts";
|
||||
import type { CompressionEngine } from "../../../open-sse/services/compression/engines/types.ts";
|
||||
import type { CompressionStepPayload } from "../../../src/lib/events/types.ts";
|
||||
|
||||
function step(over: Partial<CompressionStepPayload>): CompressionStepPayload {
|
||||
return {
|
||||
requestId: "r1",
|
||||
comboId: null,
|
||||
mode: "stacked",
|
||||
stepIndex: 0,
|
||||
totalSteps: 2,
|
||||
engine: "e",
|
||||
state: "done",
|
||||
originalTokens: 1000,
|
||||
compressedTokens: 800,
|
||||
savingsPercent: 20,
|
||||
timestamp: 1,
|
||||
...over,
|
||||
};
|
||||
}
|
||||
|
||||
describe("stepEventsToRunModel", () => {
|
||||
it("builds a run model from accumulated step events (run-level totals span first→last)", () => {
|
||||
const model = stepEventsToRunModel([
|
||||
step({ engine: "a", stepIndex: 0, originalTokens: 1000, compressedTokens: 900, savingsPercent: 10, timestamp: 1 }),
|
||||
step({ engine: "b", stepIndex: 1, originalTokens: 900, compressedTokens: 700, savingsPercent: 22, timestamp: 2 }),
|
||||
]);
|
||||
assert.equal(model.requestId, "r1");
|
||||
assert.equal(model.mode, "stacked");
|
||||
assert.equal(model.originalTokens, 1000); // first step's input
|
||||
assert.equal(model.compressedTokens, 700); // last step's output
|
||||
assert.equal(model.savingsPercent, 30); // (1000-700)/1000
|
||||
assert.equal(model.steps.length, 2);
|
||||
assert.equal(model.steps[1].engine, "b");
|
||||
assert.equal(model.timestamp, 2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("in-flight step reducer", () => {
|
||||
it("appends steps for the same requestId and starts fresh on a new requestId", () => {
|
||||
let s = appendInFlightStep(null, step({ requestId: "r1", engine: "a" }));
|
||||
assert.equal(s.requestId, "r1");
|
||||
assert.equal(s.steps.length, 1);
|
||||
s = appendInFlightStep(s, step({ requestId: "r1", engine: "b" }));
|
||||
assert.equal(s.steps.length, 2);
|
||||
// A new requestId replaces the in-flight run (latest wins).
|
||||
s = appendInFlightStep(s, step({ requestId: "r2", engine: "x" }));
|
||||
assert.equal(s.requestId, "r2");
|
||||
assert.equal(s.steps.length, 1);
|
||||
});
|
||||
|
||||
it("clears the in-flight run only when the completing requestId matches", () => {
|
||||
const s = appendInFlightStep(null, step({ requestId: "r1" }));
|
||||
assert.equal(clearInFlightOnComplete(s, "other"), s);
|
||||
assert.equal(clearInFlightOnComplete(s, "r1"), null);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Integration: applyStackedCompression emits a step per engine ───────────────
|
||||
function fakeEngine(id: string, compressed: boolean, orig: number, comp: number): CompressionEngine {
|
||||
return {
|
||||
id,
|
||||
name: id,
|
||||
description: "",
|
||||
icon: "",
|
||||
targets: ["messages"],
|
||||
stackable: true,
|
||||
stackPriority: 1,
|
||||
metadata: {
|
||||
id,
|
||||
name: id,
|
||||
description: "",
|
||||
inputScope: "messages",
|
||||
targetLatencyMs: 1,
|
||||
supportsPreview: false,
|
||||
stable: false,
|
||||
},
|
||||
apply(body) {
|
||||
return {
|
||||
body,
|
||||
compressed,
|
||||
stats: {
|
||||
originalTokens: orig,
|
||||
compressedTokens: comp,
|
||||
savingsPercent: orig > 0 ? Math.round(((orig - comp) / orig) * 100) : 0,
|
||||
techniquesUsed: [],
|
||||
mode: "stacked",
|
||||
timestamp: 0,
|
||||
durationMs: 2,
|
||||
},
|
||||
};
|
||||
},
|
||||
compress(body) {
|
||||
return this.apply(body);
|
||||
},
|
||||
getConfigSchema() {
|
||||
return [];
|
||||
},
|
||||
validateConfig() {
|
||||
return { valid: true, errors: [] };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("applyStackedCompression — onEngineStep emission", () => {
|
||||
it("fires onEngineStep once per engine with index/total/state", () => {
|
||||
registerCompressionEngine(fakeEngine("step-e1", true, 1000, 900));
|
||||
registerCompressionEngine(fakeEngine("step-e2", false, 900, 900));
|
||||
|
||||
const captured: Array<{ stepIndex: number; totalSteps: number; engine: string; state: string }> = [];
|
||||
applyStackedCompression(
|
||||
{ messages: [{ role: "user", content: "hello" }] },
|
||||
[{ engine: "step-e1" }, { engine: "step-e2" }],
|
||||
{ onEngineStep: (s) => captured.push(s) }
|
||||
);
|
||||
|
||||
assert.equal(captured.length, 2, "one step per engine");
|
||||
assert.equal(captured[0].engine, "step-e1");
|
||||
assert.equal(captured[0].stepIndex, 0);
|
||||
assert.equal(captured[0].totalSteps, 2);
|
||||
assert.equal(captured[0].state, "done");
|
||||
assert.equal(captured[1].engine, "step-e2");
|
||||
assert.equal(captured[1].stepIndex, 1);
|
||||
assert.equal(captured[1].state, "skipped");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
readMcpToolProfileFromEnv,
|
||||
reduceToolManifest,
|
||||
} from "../../../open-sse/mcp-server/toolCardinality.ts";
|
||||
|
||||
// F4.3 wiring: the MCP server now consults an opt-in tool profile (MCP_TOOL_DENY / MCP_TOOL_ALLOW)
|
||||
// and disables denied tools so they are not announced to the model (token savings). Default (no
|
||||
// env) is a no-op — every tool stays registered.
|
||||
describe("MCP tool profile from env (cardinality opt-in)", () => {
|
||||
it("returns null when no deny/allow env is set (no-op)", () => {
|
||||
assert.equal(readMcpToolProfileFromEnv({}), null);
|
||||
assert.equal(readMcpToolProfileFromEnv({ MCP_TOOL_DENY: "", MCP_TOOL_ALLOW: " " }), null);
|
||||
});
|
||||
|
||||
it("parses MCP_TOOL_DENY / MCP_TOOL_ALLOW into a profile (trimmed, empties dropped)", () => {
|
||||
const p = readMcpToolProfileFromEnv({ MCP_TOOL_DENY: "a, b ,c", MCP_TOOL_ALLOW: "x ," });
|
||||
assert.deepEqual(p?.denyTools, ["a", "b", "c"]);
|
||||
assert.deepEqual(p?.allowTools, ["x"]);
|
||||
});
|
||||
|
||||
it("the deny gate drops a denied tool and keeps others (single-entry manifest decision)", () => {
|
||||
const profile = readMcpToolProfileFromEnv({ MCP_TOOL_DENY: "noisy_tool" });
|
||||
assert.ok(profile);
|
||||
assert.equal(reduceToolManifest([{ name: "noisy_tool", scopes: [] }], profile).length, 0);
|
||||
assert.equal(reduceToolManifest([{ name: "useful_tool", scopes: [] }], profile).length, 1);
|
||||
});
|
||||
|
||||
it("allow-list mode keeps only the listed tools", () => {
|
||||
const profile = readMcpToolProfileFromEnv({ MCP_TOOL_ALLOW: "keep_me" });
|
||||
assert.ok(profile);
|
||||
assert.equal(reduceToolManifest([{ name: "keep_me", scopes: [] }], profile).length, 1);
|
||||
assert.equal(reduceToolManifest([{ name: "other", scopes: [] }], profile).length, 0);
|
||||
});
|
||||
});
|
||||
66
tests/unit/compression/engine-breakdown-synthesis.test.ts
Normal file
66
tests/unit/compression/engine-breakdown-synthesis.test.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { ensureEngineBreakdown } from "../../../open-sse/services/compression/engineBreakdown.ts";
|
||||
import type { CompressionStats } from "../../../open-sse/services/compression/types.ts";
|
||||
|
||||
function stats(over: Partial<CompressionStats>): CompressionStats {
|
||||
return {
|
||||
originalTokens: 1000,
|
||||
compressedTokens: 700,
|
||||
savingsPercent: 30,
|
||||
techniquesUsed: [],
|
||||
mode: "rtk",
|
||||
timestamp: 0,
|
||||
...over,
|
||||
};
|
||||
}
|
||||
|
||||
// Single-engine compression modes (rtk/lite/standard/aggressive/ultra) produce stats with an
|
||||
// empty engineBreakdown — only the stacked pipeline fills it. The dashboard studio then renders
|
||||
// an empty Input→Output pipeline (no engine node, inert replay) for the most common case.
|
||||
// ensureEngineBreakdown synthesizes a 1-entry breakdown from the overall stats so the studio
|
||||
// always shows at least one real engine node.
|
||||
describe("ensureEngineBreakdown", () => {
|
||||
it("returns the existing breakdown unchanged when present (stacked)", () => {
|
||||
const bd = [
|
||||
{
|
||||
engine: "rtk",
|
||||
originalTokens: 1000,
|
||||
compressedTokens: 700,
|
||||
savingsPercent: 30,
|
||||
techniquesUsed: ["a"],
|
||||
},
|
||||
];
|
||||
assert.deepEqual(ensureEngineBreakdown(stats({ engineBreakdown: bd })), bd);
|
||||
});
|
||||
|
||||
it("synthesizes a 1-entry breakdown for single-engine modes (empty/undefined breakdown)", () => {
|
||||
const out = ensureEngineBreakdown(
|
||||
stats({
|
||||
mode: "lite",
|
||||
engine: "lite",
|
||||
originalTokens: 2000,
|
||||
compressedTokens: 1500,
|
||||
savingsPercent: 25,
|
||||
techniquesUsed: ["lite-strip"],
|
||||
rulesApplied: ["r1"],
|
||||
durationMs: 3,
|
||||
})
|
||||
);
|
||||
assert.equal(out.length, 1);
|
||||
assert.equal(out[0].engine, "lite");
|
||||
assert.equal(out[0].originalTokens, 2000);
|
||||
assert.equal(out[0].compressedTokens, 1500);
|
||||
assert.equal(out[0].savingsPercent, 25);
|
||||
assert.deepEqual(out[0].techniquesUsed, ["lite-strip"]);
|
||||
assert.deepEqual(out[0].rulesApplied, ["r1"]);
|
||||
assert.equal(out[0].durationMs, 3);
|
||||
});
|
||||
|
||||
it("falls back to mode for the engine label when stats.engine is absent", () => {
|
||||
const out = ensureEngineBreakdown(stats({ mode: "standard", engineBreakdown: [] }));
|
||||
assert.equal(out.length, 1);
|
||||
assert.equal(out[0].engine, "standard");
|
||||
assert.deepEqual(out[0].techniquesUsed, []);
|
||||
});
|
||||
});
|
||||
119
tests/unit/compression/engine-enabled-toggle.test.ts
Normal file
119
tests/unit/compression/engine-enabled-toggle.test.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Registry `enabled` toggle — the stacked loop must honor setEngineEnabled.
|
||||
*
|
||||
* `enabled` was a flag the stacked pipeline never consulted: getCompressionEngine
|
||||
* returned the engine regardless, so flipping it via setEngineEnabled had no effect
|
||||
* (the toggle "lied"). Both the sync and async stacked loops now skip a step whose
|
||||
* engine is disabled.
|
||||
*/
|
||||
import { describe, it, before, after, beforeEach } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
applyStackedCompression,
|
||||
applyStackedCompressionAsync,
|
||||
} from "../../../open-sse/services/compression/index.ts";
|
||||
import {
|
||||
registerCompressionEngine,
|
||||
unregisterCompressionEngine,
|
||||
setEngineEnabled,
|
||||
} from "../../../open-sse/services/compression/engines/registry.ts";
|
||||
import type {
|
||||
CompressionEngine,
|
||||
CompressionEngineTarget,
|
||||
} from "../../../open-sse/services/compression/engines/types.ts";
|
||||
import type {
|
||||
CompressionPipelineStep,
|
||||
CompressionResult,
|
||||
} from "../../../open-sse/services/compression/types.ts";
|
||||
|
||||
const ENGINE_ID = "enabled-toggle-engine";
|
||||
|
||||
/** Engine that tags user content and reports a real gain when it runs. */
|
||||
function makeTaggingEngine(id: string): CompressionEngine {
|
||||
return {
|
||||
id,
|
||||
name: id,
|
||||
description: id,
|
||||
icon: "x",
|
||||
targets: ["messages"] as CompressionEngineTarget[],
|
||||
stackable: true,
|
||||
stackPriority: 0,
|
||||
metadata: {
|
||||
id,
|
||||
name: id,
|
||||
description: id,
|
||||
inputScope: "messages",
|
||||
targetLatencyMs: 1,
|
||||
supportsPreview: false,
|
||||
stable: true,
|
||||
},
|
||||
compress: (body) => ({ body, compressed: false, stats: null }),
|
||||
getConfigSchema: () => [],
|
||||
validateConfig: () => ({ valid: true, errors: [] }),
|
||||
apply: (body) => {
|
||||
const messages = (body.messages as Array<{ role: string; content: string }>) ?? [];
|
||||
const next = messages.map((m) =>
|
||||
m.role === "user" ? { ...m, content: m.content + "|tagged" } : m
|
||||
);
|
||||
return {
|
||||
body: { ...body, messages: next },
|
||||
compressed: true,
|
||||
stats: {
|
||||
originalTokens: 100,
|
||||
compressedTokens: 70,
|
||||
savingsPercent: 30,
|
||||
techniquesUsed: [id],
|
||||
mode: "stacked",
|
||||
timestamp: 0,
|
||||
durationMs: 0.1,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function pipeline(...ids: string[]): CompressionPipelineStep[] {
|
||||
return ids.map((engine) => ({ engine })) as unknown as CompressionPipelineStep[];
|
||||
}
|
||||
|
||||
function userContent(result: CompressionResult): string {
|
||||
const messages = result.body.messages as Array<{ role: string; content: string }>;
|
||||
return messages.find((m) => m.role === "user")!.content;
|
||||
}
|
||||
|
||||
function freshBody() {
|
||||
return { messages: [{ role: "user", content: "hi" }] };
|
||||
}
|
||||
|
||||
describe("registry enabled toggle — stacked loop honors setEngineEnabled", () => {
|
||||
before(() => registerCompressionEngine(makeTaggingEngine(ENGINE_ID)));
|
||||
beforeEach(() => setEngineEnabled(ENGINE_ID, true)); // default-on before each case
|
||||
after(() => unregisterCompressionEngine(ENGINE_ID));
|
||||
|
||||
it("applies the engine while enabled (sync)", () => {
|
||||
const result = applyStackedCompression(freshBody(), pipeline(ENGINE_ID));
|
||||
assert.equal(result.compressed, true);
|
||||
assert.equal(userContent(result), "hi|tagged");
|
||||
});
|
||||
|
||||
it("skips the engine once disabled (sync)", () => {
|
||||
setEngineEnabled(ENGINE_ID, false);
|
||||
const result = applyStackedCompression(freshBody(), pipeline(ENGINE_ID));
|
||||
assert.equal(result.compressed, false);
|
||||
assert.equal(userContent(result), "hi"); // unchanged — step skipped
|
||||
});
|
||||
|
||||
it("applies the engine while enabled (async)", async () => {
|
||||
const result = await applyStackedCompressionAsync(freshBody(), pipeline(ENGINE_ID));
|
||||
assert.equal(result.compressed, true);
|
||||
assert.equal(userContent(result), "hi|tagged");
|
||||
});
|
||||
|
||||
it("skips the engine once disabled (async)", async () => {
|
||||
setEngineEnabled(ENGINE_ID, false);
|
||||
const result = await applyStackedCompressionAsync(freshBody(), pipeline(ENGINE_ID));
|
||||
assert.equal(result.compressed, false);
|
||||
assert.equal(userContent(result), "hi");
|
||||
});
|
||||
});
|
||||
221
tests/unit/compression/gcf-benchmark.test.ts
Normal file
221
tests/unit/compression/gcf-benchmark.test.ts
Normal file
@@ -0,0 +1,221 @@
|
||||
/**
|
||||
* GCF (Graph Compact Format) vs legacy omni-tabular benchmark.
|
||||
*
|
||||
* Compares compression savings, round-trip correctness, and coverage on
|
||||
* realistic payloads including cases the legacy encoder cannot handle.
|
||||
*/
|
||||
|
||||
import { describe, it, before } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
let encodeTabular: (arr: Record<string, unknown>[]) => string;
|
||||
let decodeTabular: (text: string) => Record<string, unknown>[];
|
||||
let encodeTabularBlockLegacy: (arr: Record<string, unknown>[]) => string;
|
||||
let detectHomogeneous: (arr: unknown[]) => string[] | null;
|
||||
let reconstructHeadroom: (body: Record<string, unknown>) => Record<string, unknown>;
|
||||
|
||||
before(async () => {
|
||||
const tabMod = await import("../../../open-sse/services/compression/engines/headroom/tabular.ts");
|
||||
encodeTabular = tabMod.encodeTabular;
|
||||
decodeTabular = tabMod.decodeTabular;
|
||||
encodeTabularBlockLegacy = tabMod.encodeTabularBlockLegacy;
|
||||
|
||||
const scMod =
|
||||
await import("../../../open-sse/services/compression/engines/headroom/smartcrusher.ts");
|
||||
detectHomogeneous = scMod.detectHomogeneous;
|
||||
|
||||
const idxMod = await import("../../../open-sse/services/compression/engines/headroom/index.ts");
|
||||
reconstructHeadroom = idxMod.reconstructHeadroom;
|
||||
});
|
||||
|
||||
// ─── test payloads ──────────────────────────────────────────────────────────
|
||||
|
||||
interface Payload {
|
||||
name: string;
|
||||
description: string;
|
||||
data: Record<string, unknown>[];
|
||||
legacyCanHandle: boolean;
|
||||
}
|
||||
|
||||
function buildPayloads(): Payload[] {
|
||||
return [
|
||||
{
|
||||
name: "homogeneous-simple",
|
||||
description: "50 rows, 4 uniform columns (id, name, value, active)",
|
||||
data: Array.from({ length: 50 }, (_, i) => ({
|
||||
id: i + 1,
|
||||
name: `item-${i + 1}`,
|
||||
value: (i + 1) * 10,
|
||||
active: i % 2 === 0,
|
||||
})),
|
||||
legacyCanHandle: true,
|
||||
},
|
||||
{
|
||||
name: "homogeneous-wide",
|
||||
description: "30 rows, 8 columns with varied types",
|
||||
data: Array.from({ length: 30 }, (_, i) => ({
|
||||
id: i,
|
||||
firstName: `First-${i}`,
|
||||
lastName: `Last-${i}`,
|
||||
email: `user${i}@example.com`,
|
||||
age: 20 + (i % 50),
|
||||
salary: 50000 + i * 1000,
|
||||
department: ["Engineering", "Sales", "Marketing", "Support"][i % 4],
|
||||
active: i % 3 !== 0,
|
||||
})),
|
||||
legacyCanHandle: true,
|
||||
},
|
||||
{
|
||||
name: "heterogeneous-keys",
|
||||
description: "20 rows with different key sets (GCF handles, legacy skips)",
|
||||
data: [
|
||||
...Array.from({ length: 10 }, (_, i) => ({
|
||||
id: i,
|
||||
name: `user-${i}`,
|
||||
role: "admin",
|
||||
})),
|
||||
...Array.from({ length: 10 }, (_, i) => ({
|
||||
id: i + 10,
|
||||
email: `u${i}@test.com`,
|
||||
verified: true,
|
||||
})),
|
||||
],
|
||||
legacyCanHandle: false,
|
||||
},
|
||||
{
|
||||
name: "mixed-type-columns",
|
||||
description: "25 rows with nullable and mixed-type columns",
|
||||
data: Array.from({ length: 25 }, (_, i) => ({
|
||||
id: i + 1,
|
||||
score: i % 3 === 0 ? null : (i + 1) * 7,
|
||||
label: i % 4 === 0 ? null : `label-${i}`,
|
||||
value: i % 2 === 0 ? i * 10 : `str-${i}`,
|
||||
})),
|
||||
legacyCanHandle: false,
|
||||
},
|
||||
{
|
||||
name: "nested-objects",
|
||||
description: "20 rows with nested object values",
|
||||
data: Array.from({ length: 20 }, (_, i) => ({
|
||||
id: i,
|
||||
user: {
|
||||
name: `user-${i}`,
|
||||
email: `user${i}@example.com`,
|
||||
tier: i % 3 === 0 ? "premium" : "free",
|
||||
},
|
||||
amount: i * 100,
|
||||
})),
|
||||
// Legacy detects as homogeneous (same keys), but JSON-stringifies nested objects.
|
||||
// GCF encodes nested objects natively with inline schemas for better compression.
|
||||
legacyCanHandle: true,
|
||||
},
|
||||
{
|
||||
name: "api-response-realistic",
|
||||
description: "Realistic API response: 40 rows with mixed fields",
|
||||
data: Array.from({ length: 40 }, (_, i) => ({
|
||||
id: `req-${i.toString(16).padStart(4, "0")}`,
|
||||
timestamp: `2024-01-${((i % 28) + 1).toString().padStart(2, "0")}T${(i % 24).toString().padStart(2, "0")}:00:00Z`,
|
||||
method: ["GET", "POST", "PUT", "DELETE"][i % 4],
|
||||
path: `/api/v1/resources/${i}`,
|
||||
status: [200, 201, 400, 404, 500][i % 5],
|
||||
latencyMs: 10 + Math.floor(i * 3.7),
|
||||
userId: `user-${i % 15}`,
|
||||
cached: i % 3 === 0,
|
||||
})),
|
||||
legacyCanHandle: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// ─── benchmark tests ────────────────────────────────────────────────────────
|
||||
|
||||
describe("GCF benchmark — compression savings", () => {
|
||||
const payloads = buildPayloads();
|
||||
|
||||
for (const payload of payloads) {
|
||||
it(`${payload.name}: GCF compresses with positive savings`, () => {
|
||||
const jsonStr = JSON.stringify(payload.data);
|
||||
const gcfEncoded = encodeTabular(payload.data);
|
||||
const savings = ((jsonStr.length - gcfEncoded.length) / jsonStr.length) * 100;
|
||||
assert.ok(
|
||||
savings > 0,
|
||||
`GCF should save space on ${payload.name} (got ${savings.toFixed(1)}%)`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
for (const payload of payloads) {
|
||||
it(`${payload.name}: GCF round-trips losslessly`, () => {
|
||||
const gcfEncoded = encodeTabular(payload.data);
|
||||
const decoded = decodeTabular(gcfEncoded);
|
||||
assert.deepEqual(decoded, payload.data, `${payload.name} must round-trip without data loss`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("GCF benchmark — coverage comparison with legacy", () => {
|
||||
const payloads = buildPayloads();
|
||||
|
||||
it("legacy omni-tabular handles only homogeneous payloads", () => {
|
||||
for (const payload of payloads) {
|
||||
const isHomogeneous = detectHomogeneous(payload.data) !== null;
|
||||
if (payload.legacyCanHandle) {
|
||||
// Legacy CAN handle it (but may still use mixed types that corrupt round-trip)
|
||||
// For truly homogeneous data, detectHomogeneous should return non-null
|
||||
// (some payloads are "legacyCanHandle" in terms of key structure but have mixed types)
|
||||
} else {
|
||||
assert.equal(
|
||||
isHomogeneous,
|
||||
false,
|
||||
`${payload.name}: legacy should NOT detect as homogeneous`
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("GCF handles ALL payloads (100% coverage)", () => {
|
||||
for (const payload of payloads) {
|
||||
const gcfEncoded = encodeTabular(payload.data);
|
||||
assert.ok(gcfEncoded.includes("gcf-generic"), `${payload.name}: must produce GCF output`);
|
||||
const decoded = decodeTabular(gcfEncoded);
|
||||
assert.deepEqual(decoded, payload.data, `${payload.name}: GCF must round-trip`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("GCF benchmark — savings table", () => {
|
||||
it("prints a comparison table (informational)", () => {
|
||||
const payloads = buildPayloads();
|
||||
const rows: string[] = [];
|
||||
rows.push("| Payload | JSON | GCF | Savings | Legacy | Legacy Savings | GCF Advantage |");
|
||||
rows.push("|---------|------|-----|---------|--------|----------------|---------------|");
|
||||
|
||||
for (const payload of payloads) {
|
||||
const jsonStr = JSON.stringify(payload.data);
|
||||
const gcfEncoded = encodeTabular(payload.data);
|
||||
const gcfSavings = ((jsonStr.length - gcfEncoded.length) / jsonStr.length) * 100;
|
||||
|
||||
let legacySize = "-";
|
||||
let legacySavings = "-";
|
||||
let advantage = "N/A (legacy can't encode)";
|
||||
|
||||
if (payload.legacyCanHandle && detectHomogeneous(payload.data)) {
|
||||
const legacyBlock = `\`\`\`omni-tabular\n${encodeTabularBlockLegacy(payload.data)}\n\`\`\``;
|
||||
legacySize = String(legacyBlock.length);
|
||||
const ls = ((jsonStr.length - legacyBlock.length) / jsonStr.length) * 100;
|
||||
legacySavings = ls.toFixed(1) + "%";
|
||||
advantage = (gcfSavings - ls).toFixed(1) + "pp";
|
||||
}
|
||||
|
||||
rows.push(
|
||||
`| ${payload.name} | ${jsonStr.length} | ${gcfEncoded.length} | ${gcfSavings.toFixed(1)}% | ${legacySize} | ${legacySavings} | ${advantage} |`
|
||||
);
|
||||
}
|
||||
|
||||
// Print to stdout for visibility in test output
|
||||
console.log("\n" + rows.join("\n") + "\n");
|
||||
|
||||
// Assert the table was built (not empty)
|
||||
assert.ok(rows.length > 2, "benchmark table should have data rows");
|
||||
});
|
||||
});
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
checkTokensPerTaskGate,
|
||||
replayTranscripts,
|
||||
transcriptsToCorpus,
|
||||
requestBodyToTranscript,
|
||||
requestBodiesToTranscripts,
|
||||
} from "../../../open-sse/services/compression/harness/index.ts";
|
||||
|
||||
const SAMPLE = "Call fetchUser() at https://api.example.com/v1 with MAX_RETRIES set to 3.0.0";
|
||||
@@ -136,3 +138,45 @@ describe("compression harness — transcript replay (TV3)", () => {
|
||||
assert.equal(report.meanRetention, 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("compression harness — transcript loader (TV3)", () => {
|
||||
it("builds a transcript from a captured request body, flattening content blocks", () => {
|
||||
const transcript = requestBodyToTranscript("req-1", {
|
||||
model: "gpt-x",
|
||||
messages: [
|
||||
{ role: "system", content: "You are helpful." },
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: "first block" },
|
||||
{ type: "image_url", image_url: { url: "data:..." } },
|
||||
{ type: "text", text: "second block" },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
assert.equal(transcript.id, "req-1");
|
||||
assert.equal(transcript.turns.length, 2);
|
||||
assert.equal(transcript.turns[0].role, "system");
|
||||
assert.equal(transcript.turns[0].content, "You are helpful.");
|
||||
// multimodal content flattened to its text blocks (image dropped)
|
||||
assert.equal(transcript.turns[1].content, "first block\nsecond block");
|
||||
});
|
||||
|
||||
it("returns an empty transcript for a body without a messages array", () => {
|
||||
assert.deepEqual(requestBodyToTranscript("empty", { foo: 1 }), { id: "empty", turns: [] });
|
||||
assert.deepEqual(requestBodyToTranscript("nullish", null), { id: "nullish", turns: [] });
|
||||
});
|
||||
|
||||
it("maps captured bodies into transcripts that feed the replay corpus", () => {
|
||||
const transcripts = requestBodiesToTranscripts([
|
||||
{ id: "a", body: { messages: [{ role: "user", content: "hi" }] } },
|
||||
{ id: "b", body: { messages: [{ role: "user", content: " " }] } }, // empty turn → skipped
|
||||
]);
|
||||
assert.equal(transcripts.length, 2);
|
||||
const corpus = transcriptsToCorpus(transcripts);
|
||||
// transcript "a" contributes one case; "b" is all-blank so transcriptsToCorpus drops it
|
||||
assert.equal(corpus.length, 1);
|
||||
assert.equal(corpus[0].task, "a");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -88,10 +88,11 @@ describe("tabular encoder round-trip", () => {
|
||||
assert.deepEqual(decoded, original);
|
||||
});
|
||||
|
||||
it("encoded form contains an explicit [N rows] count marker", async () => {
|
||||
it("encoded form contains an explicit [N] count marker with field declaration", async () => {
|
||||
const original = makeRows(25);
|
||||
const encoded = encodeTabular(original);
|
||||
assert.match(encoded, /\[25 rows\]/);
|
||||
// GCF uses [N]{fields} format (e.g. [25]{id,name,value,active})
|
||||
assert.match(encoded, /\[25\]\{/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -144,7 +145,7 @@ describe("headroomEngine.apply — compression", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("compressed body contains the [N rows] count marker", async () => {
|
||||
it("compressed body contains the [N] count marker with field declaration", async () => {
|
||||
const n = 22;
|
||||
const rows = makeRows(n);
|
||||
const body = makeBody(rows);
|
||||
@@ -153,7 +154,8 @@ describe("headroomEngine.apply — compression", () => {
|
||||
assert.equal(result.compressed, true);
|
||||
|
||||
const bodyStr = JSON.stringify(result.body);
|
||||
assert.match(bodyStr, /\[22 rows\]/, "compressed body must contain [N rows] marker");
|
||||
// GCF uses [N]{fields} format
|
||||
assert.match(bodyStr, /\[22\]\{/, "compressed body must contain [N]{fields} marker");
|
||||
});
|
||||
|
||||
it("also compresses when the array is inside a ```json fence in message content", async () => {
|
||||
@@ -175,16 +177,16 @@ describe("headroomEngine.apply — compression", () => {
|
||||
// ─── 3. Conservative guards — nested/flat should NOT regress ─────────────────
|
||||
|
||||
describe("headroomEngine.apply — conservative guards (no regression)", () => {
|
||||
it("does NOT compress a heterogeneous array (objects with different key sets)", async () => {
|
||||
// Objects have different keys — not homogeneous
|
||||
it("compresses a heterogeneous array (objects with different key sets) via GCF", async () => {
|
||||
// GCF handles heterogeneous arrays natively: missing fields become ~ (absent)
|
||||
const rows: Record<string, unknown>[] = [
|
||||
...Array.from({ length: 10 }, (_, i) => ({ id: i, name: `n${i}` })),
|
||||
...Array.from({ length: 10 }, (_, i) => ({ key: i, label: `l${i}`, extra: true })),
|
||||
];
|
||||
const body = makeBody(rows);
|
||||
const result = headroomEngine.apply(body);
|
||||
assert.equal(result.compressed, false, "heterogeneous array should NOT be compressed");
|
||||
assert.deepEqual(result.body, body);
|
||||
// GCF encodes heterogeneous arrays with union of all keys
|
||||
assert.equal(result.compressed, true, "heterogeneous array should be compressed by GCF");
|
||||
});
|
||||
|
||||
it("does NOT compress a tiny array below minRows (< default 8)", async () => {
|
||||
@@ -305,3 +307,140 @@ describe("headroomEngine — losslessness on mixed-type columns (regression)", (
|
||||
assert.deepEqual(restored, body, "mixed-type column must round-trip without data loss");
|
||||
});
|
||||
});
|
||||
|
||||
// ─── 6. GCF encoding: capabilities beyond legacy omni-tabular ──────────────
|
||||
|
||||
describe("GCF encoding — advanced capabilities", () => {
|
||||
async function reconstruct(body: Record<string, unknown>) {
|
||||
const mod = await import("../../../open-sse/services/compression/engines/headroom/index.ts");
|
||||
return mod.reconstructHeadroom(body);
|
||||
}
|
||||
|
||||
it("compresses heterogeneous arrays (different key sets) losslessly", async () => {
|
||||
const rows: Record<string, unknown>[] = [
|
||||
...Array.from({ length: 10 }, (_, i) => ({ id: i, name: `user-${i}` })),
|
||||
...Array.from({ length: 10 }, (_, i) => ({
|
||||
id: i + 10,
|
||||
email: `u${i}@test.com`,
|
||||
verified: true,
|
||||
})),
|
||||
];
|
||||
const body = makeBody(rows);
|
||||
const result = headroomEngine.apply(body);
|
||||
assert.equal(result.compressed, true, "heterogeneous array should be compressed");
|
||||
const restored = await reconstruct(result.body);
|
||||
assert.deepEqual(restored, body, "heterogeneous array must round-trip losslessly");
|
||||
});
|
||||
|
||||
it("compresses arrays with nested objects losslessly", async () => {
|
||||
const rows = Array.from({ length: 15 }, (_, i) => ({
|
||||
id: i,
|
||||
name: `item-${i}`,
|
||||
metadata: { category: `cat-${i % 3}`, priority: i % 5 },
|
||||
}));
|
||||
const body = makeBody(rows);
|
||||
const result = headroomEngine.apply(body);
|
||||
assert.equal(result.compressed, true, "nested objects should be compressed");
|
||||
const restored = await reconstruct(result.body);
|
||||
assert.deepEqual(restored, body, "nested objects must round-trip losslessly");
|
||||
});
|
||||
|
||||
it("compresses arrays with nested arrays losslessly", async () => {
|
||||
// Use enough rows with enough data to overcome GCF overhead on nested arrays
|
||||
const rows = Array.from({ length: 30 }, (_, i) => ({
|
||||
id: i,
|
||||
name: `item-${i}-with-longer-name-for-savings`,
|
||||
tags: ["alpha", "beta", `tag-${i}`],
|
||||
scores: [i * 10, i * 20, i * 30],
|
||||
}));
|
||||
const body = makeBody(rows);
|
||||
const result = headroomEngine.apply(body);
|
||||
assert.equal(result.compressed, true, "nested arrays should be compressed");
|
||||
const restored = await reconstruct(result.body);
|
||||
assert.deepEqual(restored, body, "nested arrays must round-trip losslessly");
|
||||
});
|
||||
|
||||
it("uses gcf-generic fence marker (not omni-tabular)", async () => {
|
||||
const rows = makeRows(20);
|
||||
const encoded = encodeTabular(rows);
|
||||
assert.ok(encoded.includes("```gcf-generic"), "must use gcf-generic fence marker");
|
||||
assert.ok(!encoded.includes("omni-tabular"), "must not use legacy omni-tabular marker");
|
||||
});
|
||||
|
||||
it("still decodes legacy omni-tabular encoded content (backward compat)", async () => {
|
||||
// Import legacy encoder
|
||||
const mod = await import("../../../open-sse/services/compression/engines/headroom/tabular.ts");
|
||||
const legacyEncode = mod.encodeTabularBlockLegacy;
|
||||
|
||||
const rows = makeRows(10);
|
||||
const legacyBlock = `\`\`\`omni-tabular\n${legacyEncode(rows)}\n\`\`\``;
|
||||
const decoded = decodeTabular(legacyBlock);
|
||||
assert.deepEqual(decoded, rows, "legacy omni-tabular content must still decode correctly");
|
||||
});
|
||||
});
|
||||
|
||||
// ─── 7. GCF vs legacy benchmark comparison ─────────────────────────────────
|
||||
|
||||
describe("GCF vs legacy omni-tabular — compression comparison", () => {
|
||||
it("GCF achieves comparable or better compression on homogeneous arrays", async () => {
|
||||
const rows = makeRows(50);
|
||||
const jsonStr = JSON.stringify(rows);
|
||||
|
||||
// Legacy omni-tabular
|
||||
const mod = await import("../../../open-sse/services/compression/engines/headroom/tabular.ts");
|
||||
const legacyBlock = `\`\`\`omni-tabular\n${mod.encodeTabularBlockLegacy(rows)}\n\`\`\``;
|
||||
const legacySavings = ((jsonStr.length - legacyBlock.length) / jsonStr.length) * 100;
|
||||
|
||||
// GCF
|
||||
const gcfEncoded = encodeTabular(rows);
|
||||
const gcfSavings = ((jsonStr.length - gcfEncoded.length) / jsonStr.length) * 100;
|
||||
|
||||
// GCF should achieve at least as much savings as legacy on homogeneous data
|
||||
assert.ok(
|
||||
gcfSavings >= legacySavings * 0.8, // allow 20% tolerance
|
||||
`GCF savings (${gcfSavings.toFixed(1)}%) should be within 80% of legacy (${legacySavings.toFixed(1)}%)`
|
||||
);
|
||||
});
|
||||
|
||||
it("GCF compresses cases that legacy omni-tabular skips entirely", async () => {
|
||||
// Heterogeneous: legacy would skip, GCF handles it
|
||||
const heteroRows: Record<string, unknown>[] = [
|
||||
...Array.from({ length: 10 }, (_, i) => ({ id: i, name: `user-${i}`, role: "admin" })),
|
||||
...Array.from({ length: 10 }, (_, i) => ({ id: i + 10, email: `u${i}@test.com` })),
|
||||
];
|
||||
const jsonStr = JSON.stringify(heteroRows);
|
||||
|
||||
// Legacy encoder would produce nothing useful for heterogeneous data
|
||||
// (detectHomogeneous returns null)
|
||||
const { detectHomogeneous } =
|
||||
await import("../../../open-sse/services/compression/engines/headroom/smartcrusher.ts");
|
||||
assert.equal(detectHomogeneous(heteroRows), null, "legacy should reject heterogeneous arrays");
|
||||
|
||||
// GCF compresses it
|
||||
const gcfEncoded = encodeTabular(heteroRows);
|
||||
const gcfSavings = ((jsonStr.length - gcfEncoded.length) / jsonStr.length) * 100;
|
||||
assert.ok(
|
||||
gcfSavings > 0,
|
||||
`GCF should compress heterogeneous arrays (savings: ${gcfSavings.toFixed(1)}%)`
|
||||
);
|
||||
});
|
||||
|
||||
it("GCF compresses nested objects that legacy omni-tabular JSON-stringifies", async () => {
|
||||
const nestedRows = Array.from({ length: 20 }, (_, i) => ({
|
||||
id: i,
|
||||
user: {
|
||||
name: `user-${i}`,
|
||||
email: `user${i}@example.com`,
|
||||
tier: i % 3 === 0 ? "premium" : "free",
|
||||
},
|
||||
value: i * 100,
|
||||
}));
|
||||
const jsonStr = JSON.stringify(nestedRows);
|
||||
const gcfEncoded = encodeTabular(nestedRows);
|
||||
const gcfSavings = ((jsonStr.length - gcfEncoded.length) / jsonStr.length) * 100;
|
||||
assert.ok(
|
||||
gcfSavings >= 30,
|
||||
`GCF should achieve >=30% savings on nested objects (got ${gcfSavings.toFixed(1)}%)`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
110
tests/unit/compression/mcp-accessibility-config.test.ts
Normal file
110
tests/unit/compression/mcp-accessibility-config.test.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { describe, it, beforeEach, afterEach, after } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import { mcpAccessibilityConfigSchema } from "../../../src/shared/validation/compressionConfigSchemas.ts";
|
||||
|
||||
// mcpAccessibility tunes how the MCP server filters oversized tool outputs: server.ts reads the
|
||||
// `compression/mcpAccessibility` DB key on every tool call (readMcpAccessibilityConfig). The
|
||||
// #4206 numeric bounds existed but were unreachable in production — no write route, and the
|
||||
// settings PUT schema (.strict()) rejected the key, so get/setMcpAccessibilityConfig had no
|
||||
// callers. This proves the new schema + dedicated sub-route + DB round-trip make the config
|
||||
// settable end to end, with clampMcpAccessibilityConfig owning the numeric floors.
|
||||
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-mcpaccess-"));
|
||||
const ORIGINAL_DATA_DIR = process.env.DATA_DIR;
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
|
||||
const core = await import("../../../src/lib/db/core.ts");
|
||||
const { getMcpAccessibilityConfig, setMcpAccessibilityConfig } =
|
||||
await import("../../../src/lib/db/compression.ts");
|
||||
const route = await import("../../../src/app/api/settings/compression/mcp-accessibility/route.ts");
|
||||
|
||||
function resetDir() {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
describe("mcpAccessibility config reachability", () => {
|
||||
beforeEach(resetDir);
|
||||
afterEach(() => core.resetDbInstance());
|
||||
after(() => {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
if (ORIGINAL_DATA_DIR === undefined) delete process.env.DATA_DIR;
|
||||
else process.env.DATA_DIR = ORIGINAL_DATA_DIR;
|
||||
});
|
||||
|
||||
it("validates partial updates and rejects unknown / invalid fields", () => {
|
||||
assert.equal(
|
||||
mcpAccessibilityConfigSchema.safeParse({ enabled: false, maxTextChars: 8000 }).success,
|
||||
true
|
||||
);
|
||||
// unknown key rejected (.strict)
|
||||
assert.equal(mcpAccessibilityConfigSchema.safeParse({ bogus: 1 }).success, false);
|
||||
// non-positive maxTextChars rejected at the schema boundary
|
||||
assert.equal(mcpAccessibilityConfigSchema.safeParse({ maxTextChars: -5 }).success, false);
|
||||
assert.equal(mcpAccessibilityConfigSchema.safeParse({ collapseThreshold: 0 }).success, false);
|
||||
});
|
||||
|
||||
it("persists a partial update through set/getMcpAccessibilityConfig (clamp owns floors)", async () => {
|
||||
await setMcpAccessibilityConfig({ maxTextChars: 8000, collapseThreshold: 12 });
|
||||
let cfg = await getMcpAccessibilityConfig();
|
||||
assert.equal(cfg.maxTextChars, 8000);
|
||||
assert.equal(cfg.collapseThreshold, 12);
|
||||
|
||||
// below the engine floor (MCP_ACCESSIBILITY_MIN_MAX_TEXT_CHARS) → clamp reverts to the
|
||||
// default, the documented "misconfiguration falls back to the default" behavior.
|
||||
await setMcpAccessibilityConfig({ maxTextChars: 100 });
|
||||
cfg = await getMcpAccessibilityConfig();
|
||||
assert.equal(cfg.maxTextChars, 50000);
|
||||
});
|
||||
|
||||
it("PUT + GET round-trip through the dedicated sub-route persists the config", async () => {
|
||||
const putRes = await route.PUT(
|
||||
new Request("http://localhost/api/settings/compression/mcp-accessibility", {
|
||||
method: "PUT",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ maxTextChars: 12000, minLengthToProcess: 500 }),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
}) as any
|
||||
);
|
||||
assert.equal(putRes.status, 200);
|
||||
const putBody = await putRes.json();
|
||||
assert.equal(putBody.maxTextChars, 12000);
|
||||
assert.equal(putBody.minLengthToProcess, 500);
|
||||
|
||||
// Survives a fresh read from a new DB handle (not just the write-path return value).
|
||||
core.resetDbInstance();
|
||||
const getRes = await route.GET(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
new Request("http://localhost/api/settings/compression/mcp-accessibility") as any
|
||||
);
|
||||
assert.equal(getRes.status, 200);
|
||||
const getBody = await getRes.json();
|
||||
assert.equal(getBody.maxTextChars, 12000);
|
||||
assert.equal(getBody.minLengthToProcess, 500);
|
||||
});
|
||||
|
||||
it("PUT merges over the current config (does not reset untouched fields)", async () => {
|
||||
await setMcpAccessibilityConfig({ maxTextChars: 12000, collapseThreshold: 9 });
|
||||
|
||||
const putRes = await route.PUT(
|
||||
new Request("http://localhost/api/settings/compression/mcp-accessibility", {
|
||||
method: "PUT",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ enabled: false }),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
}) as any
|
||||
);
|
||||
assert.equal(putRes.status, 200);
|
||||
const body = await putRes.json();
|
||||
assert.equal(body.enabled, false);
|
||||
// untouched fields preserved (partial-merge-over-current, not over the defaults)
|
||||
assert.equal(body.maxTextChars, 12000);
|
||||
assert.equal(body.collapseThreshold, 9);
|
||||
});
|
||||
});
|
||||
46
tests/unit/compression/mcpAccessibility-clamp.test.ts
Normal file
46
tests/unit/compression/mcpAccessibility-clamp.test.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { smartFilterText } from "../../../open-sse/services/compression/engines/mcpAccessibility/index.ts";
|
||||
import { DEFAULT_MCP_ACCESSIBILITY_CONFIG } from "../../../open-sse/services/compression/engines/mcpAccessibility/constants.ts";
|
||||
|
||||
// F5.3: `headSize = config.maxTextChars - 300` goes negative when maxTextChars is below the
|
||||
// 300-char tail reservation, and `out.slice(0, negative)` counts from the END — silently
|
||||
// keeping a wrong (and oversized) fragment instead of the intended head. maxTextChars is
|
||||
// only floored to > 0 on the write path, so a stored value in [1,300] reaches this code.
|
||||
test("clamps head to >=0 when maxTextChars is below the tail reservation (≤300)", () => {
|
||||
const config = {
|
||||
...DEFAULT_MCP_ACCESSIBILITY_CONFIG,
|
||||
maxTextChars: 50,
|
||||
minLengthToProcess: 1,
|
||||
};
|
||||
const input = "A".repeat(500);
|
||||
const out = smartFilterText(input, config);
|
||||
|
||||
// Bug: headSize = -250 → slice(0,-250) keeps the first 250 chars → output leaks a long
|
||||
// run of 'A' and is far larger than maxTextChars. Fixed: headSize clamps to 0 → empty
|
||||
// head → output is just the truncation notice (which contains no capital 'A').
|
||||
assert.ok(!out.includes("A"), "head must be empty (clamped) — no leaked content from a negative slice");
|
||||
assert.ok(out.includes("truncated"), "still emits the truncation notice");
|
||||
});
|
||||
|
||||
test("reports omitted chars relative to the filtered text, not the raw input", () => {
|
||||
// Noise lines get stripped before truncation, so `omitted` must be measured against the
|
||||
// filtered text (`out`), not the longer raw `text`.
|
||||
const noise = "- generic:\n".repeat(10); // stripped by NOISE_PATTERNS
|
||||
const input = noise + "B".repeat(1000);
|
||||
const config = {
|
||||
...DEFAULT_MCP_ACCESSIBILITY_CONFIG,
|
||||
maxTextChars: 400,
|
||||
minLengthToProcess: 1,
|
||||
};
|
||||
const out = smartFilterText(input, config);
|
||||
const match = out.match(/truncated (\d+) chars/);
|
||||
assert.ok(match, "emits a truncation notice with an omitted count");
|
||||
const omitted = Number(match[1]);
|
||||
// Filtered text is ~1010 chars; head keeps 100 → omitted ~910. The raw input is 1110, so
|
||||
// the buggy `text.length - head.length` would report ~1010 (> filtered length).
|
||||
assert.ok(
|
||||
omitted <= 1000,
|
||||
`omitted must reflect the filtered text (<=1000), got ${omitted}`
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
smartFilterText,
|
||||
clampMcpAccessibilityConfig,
|
||||
} from "../../../open-sse/services/compression/engines/mcpAccessibility/index.ts";
|
||||
import { DEFAULT_MCP_ACCESSIBILITY_CONFIG } from "../../../open-sse/services/compression/engines/mcpAccessibility/constants.ts";
|
||||
|
||||
// smartFilterText reserves 300 chars for the truncation tail/notice, so any maxTextChars below
|
||||
// that leaves headSize <= 0 and the whole tool result is replaced by the notice (total data
|
||||
// loss). The DB normalizer floored maxTextChars only at > 0, and the production read path in
|
||||
// server.ts bypassed bounding entirely. clampMcpAccessibilityConfig is the shared guard.
|
||||
test("clamps maxTextChars below the tail reserve to the default", () => {
|
||||
for (const bad of [1, 50, 300, 599]) {
|
||||
assert.equal(
|
||||
clampMcpAccessibilityConfig({ maxTextChars: bad }).maxTextChars,
|
||||
DEFAULT_MCP_ACCESSIBILITY_CONFIG.maxTextChars,
|
||||
`maxTextChars=${bad} must fall back to default`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("keeps a sane maxTextChars (>= 600)", () => {
|
||||
assert.equal(clampMcpAccessibilityConfig({ maxTextChars: 600 }).maxTextChars, 600);
|
||||
assert.equal(clampMcpAccessibilityConfig({ maxTextChars: 1000 }).maxTextChars, 1000);
|
||||
assert.equal(clampMcpAccessibilityConfig({ maxTextChars: 1234.9 }).maxTextChars, 1234);
|
||||
});
|
||||
|
||||
test("bounds the other numeric fields and honors enabled", () => {
|
||||
const c = clampMcpAccessibilityConfig({
|
||||
collapseThreshold: -5,
|
||||
minLengthToProcess: 0,
|
||||
collapseKeepHead: -1,
|
||||
enabled: false,
|
||||
});
|
||||
assert.equal(c.collapseThreshold, DEFAULT_MCP_ACCESSIBILITY_CONFIG.collapseThreshold);
|
||||
assert.equal(c.minLengthToProcess, DEFAULT_MCP_ACCESSIBILITY_CONFIG.minLengthToProcess);
|
||||
assert.equal(c.collapseKeepHead, DEFAULT_MCP_ACCESSIBILITY_CONFIG.collapseKeepHead);
|
||||
assert.equal(c.enabled, false);
|
||||
});
|
||||
|
||||
test("a clamped config never lets smartFilterText truncate the whole text away", () => {
|
||||
// The previously-dangerous stored value: maxTextChars=50 → clamps to default, so a
|
||||
// 1000-char tool result is NOT replaced wholesale by the truncation notice.
|
||||
const cfg = clampMcpAccessibilityConfig({ maxTextChars: 50, minLengthToProcess: 1 });
|
||||
const out = smartFilterText("A".repeat(1000), cfg);
|
||||
assert.ok(out.includes("A".repeat(500)), "content preserved (not nuked by a tiny maxTextChars)");
|
||||
});
|
||||
57
tests/unit/compression/rtk-filter-deduplicate.test.ts
Normal file
57
tests/unit/compression/rtk-filter-deduplicate.test.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { applyLineFilter } from "../../../open-sse/services/compression/engines/rtk/lineFilter.ts";
|
||||
import type { RtkFilterDefinition } from "../../../open-sse/services/compression/engines/rtk/filterSchema.ts";
|
||||
|
||||
// A custom RTK filter can set `deduplicate: true` (rules.deduplicate in the filter JSON), and it
|
||||
// was carried onto RtkFilterDefinition — but applyLineFilter never read it, so the flag did
|
||||
// nothing. This wires it: when set, consecutive duplicate lines in the filter's output are
|
||||
// collapsed (the same line-dedup the engine offers globally, now controllable per filter).
|
||||
function filter(over: Partial<RtkFilterDefinition>): RtkFilterDefinition {
|
||||
return {
|
||||
id: "t",
|
||||
name: "t",
|
||||
description: "",
|
||||
commandTypes: [],
|
||||
commandPatterns: [],
|
||||
matchPatterns: [],
|
||||
category: "generic",
|
||||
priority: 50,
|
||||
stripPatterns: [],
|
||||
keepPatterns: [],
|
||||
priorityPatterns: [],
|
||||
collapsePatterns: [],
|
||||
stripAnsi: false,
|
||||
replace: [],
|
||||
matchOutput: [],
|
||||
truncateLineAt: 0,
|
||||
onEmpty: "",
|
||||
filterStderr: false,
|
||||
deduplicate: false,
|
||||
maxLines: 0,
|
||||
preserveHead: 20,
|
||||
preserveTail: 20,
|
||||
tests: [],
|
||||
...over,
|
||||
};
|
||||
}
|
||||
|
||||
const TEXT = ["start", "dup", "dup", "dup", "dup", "end"].join("\n");
|
||||
|
||||
describe("applyLineFilter — per-filter deduplicate flag", () => {
|
||||
it("collapses repeated lines and records the rule when deduplicate is set", () => {
|
||||
const result = applyLineFilter(TEXT, filter({ deduplicate: true }));
|
||||
assert.ok(result.appliedRules.includes("t:deduplicate"), "records the deduplicate rule");
|
||||
const dupCount = result.text.split(/\r?\n/).filter((l) => l === "dup").length;
|
||||
assert.ok(dupCount < 4, `the 4-line dup run must be collapsed (got ${dupCount} dup lines)`);
|
||||
assert.ok(result.text.includes("start") && result.text.includes("end"), "keeps non-dup lines");
|
||||
});
|
||||
|
||||
it("does NOT deduplicate when the flag is off (default)", () => {
|
||||
const result = applyLineFilter(TEXT, filter({ deduplicate: false }));
|
||||
assert.ok(!result.appliedRules.includes("t:deduplicate"));
|
||||
const dupCount = result.text.split(/\r?\n/).filter((l) => l === "dup").length;
|
||||
assert.equal(dupCount, 4, "the dup run must remain intact when deduplicate is off");
|
||||
});
|
||||
});
|
||||
27
tests/unit/compression/rtk-filter-loader-resilience.test.ts
Normal file
27
tests/unit/compression/rtk-filter-loader-resilience.test.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { describe, it, afterEach, mock } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
|
||||
import { loadRtkFilters } from "../../../open-sse/services/compression/engines/rtk/filterLoader.ts";
|
||||
|
||||
// F5.3: collectFilterSources() enumerates the builtin filters dir with an unguarded
|
||||
// fs.readdirSync. A read failure there (lost permission, TOCTOU between existsSync and
|
||||
// readdirSync, NFS timeout, removed dir on a read-only container) must NOT propagate into
|
||||
// the compression pipeline — compression is best-effort and may never fail the request.
|
||||
describe("RTK filter loader resilience", () => {
|
||||
afterEach(() => {
|
||||
mock.restoreAll();
|
||||
// Drop any cache poisoned during the mocked run.
|
||||
loadRtkFilters({ refresh: true });
|
||||
});
|
||||
|
||||
it("degrades gracefully (does not throw) when the builtin dir cannot be read", () => {
|
||||
// The only readdirSync on the loadRtkFilters path is the builtin enumeration, so an
|
||||
// unconditional throw exercises exactly that call site.
|
||||
mock.method(fs, "readdirSync", () => {
|
||||
throw Object.assign(new Error("EACCES (mock)"), { code: "EACCES" });
|
||||
});
|
||||
|
||||
assert.doesNotThrow(() => loadRtkFilters({ refresh: true }));
|
||||
});
|
||||
});
|
||||
54
tests/unit/compression/rtk-filter-redos-guard.test.ts
Normal file
54
tests/unit/compression/rtk-filter-redos-guard.test.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
validateRtkFilter,
|
||||
isReDoSProne,
|
||||
} from "../../../open-sse/services/compression/engines/rtk/filterSchema.ts";
|
||||
|
||||
// Custom RTK filters (DATA_DIR/rtk/filters.json) carry user-supplied regex strings that are
|
||||
// compiled and run against untrusted tool output. A nested unbounded quantifier ((a+)+, (a*)*)
|
||||
// can trigger catastrophic backtracking (ReDoS). validateRtkFilter must drop such patterns so
|
||||
// they are never compiled. Dependency-free heuristic (safe-regex not installable here).
|
||||
describe("RTK filter ReDoS guard", () => {
|
||||
it("flags nested unbounded quantifiers and accepts safe patterns", () => {
|
||||
assert.equal(isReDoSProne("(a+)+"), true);
|
||||
assert.equal(isReDoSProne("(a*)*"), true);
|
||||
assert.equal(isReDoSProne("([a-z]+)+"), true);
|
||||
assert.equal(isReDoSProne("(a+|b)+"), true);
|
||||
assert.equal(isReDoSProne("(?:\\d+)+"), true);
|
||||
|
||||
assert.equal(isReDoSProne("(ab)+"), false);
|
||||
assert.equal(isReDoSProne("\\d{1,8}"), false);
|
||||
assert.equal(isReDoSProne("error|fail|FAIL"), false);
|
||||
assert.equal(isReDoSProne("^\\s*$"), false);
|
||||
});
|
||||
|
||||
it("strips ReDoS-prone patterns from a canonical filter at validation", () => {
|
||||
const def = validateRtkFilter({
|
||||
id: "x",
|
||||
label: "X",
|
||||
category: "generic",
|
||||
match: { commands: [], patterns: ["(a+)+", "ERROR\\b"], outputTypes: [] },
|
||||
rules: { dropPatterns: ["(.*)*", "^\\s*$"] },
|
||||
// preserve provided explicitly: a pack filter omitting it crashes validateRtkFilter
|
||||
// (pre-existing: rtkFilterPreserveSchema.default({}) leaves errorPatterns undefined).
|
||||
preserve: { errorPatterns: [], summaryPatterns: [] },
|
||||
});
|
||||
|
||||
assert.deepEqual(def.matchPatterns, ["ERROR\\b"], "catastrophic matchPattern dropped");
|
||||
assert.deepEqual(def.stripPatterns, ["^\\s*$"], "catastrophic dropPattern removed");
|
||||
});
|
||||
|
||||
it("strips ReDoS-prone patterns from a legacy filter at validation", () => {
|
||||
const def = validateRtkFilter({
|
||||
id: "y",
|
||||
name: "Y",
|
||||
category: "generic",
|
||||
commandTypes: ["shell"],
|
||||
stripPatterns: ["([a-z]+)*", "keepme"],
|
||||
});
|
||||
|
||||
assert.deepEqual(def.stripPatterns, ["keepme"], "catastrophic legacy stripPattern removed");
|
||||
});
|
||||
});
|
||||
63
tests/unit/compression/rtk-grouping-config.test.ts
Normal file
63
tests/unit/compression/rtk-grouping-config.test.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { describe, it, beforeEach, afterEach, after } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import { rtkConfigSchema } from "../../../src/shared/validation/compressionConfigSchemas.ts";
|
||||
import { DEFAULT_RTK_CONFIG } from "../../../open-sse/services/compression/types.ts";
|
||||
|
||||
// The RTK R5 grouping feature is read by the engine (config.enableGrouping / groupingThreshold)
|
||||
// but was unreachable in production: the Zod schema (.strict()) rejected the two fields on write
|
||||
// and normalizeRtkConfig dropped them on read. This proves both gates now let them through.
|
||||
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-rtk-grouping-"));
|
||||
const ORIGINAL_DATA_DIR = process.env.DATA_DIR;
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
|
||||
const core = await import("../../../src/lib/db/core.ts");
|
||||
const { getCompressionSettings, updateCompressionSettings } = await import(
|
||||
"../../../src/lib/db/compression.ts"
|
||||
);
|
||||
|
||||
describe("RTK grouping config persistence (R5)", () => {
|
||||
beforeEach(() => {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
core.resetDbInstance();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
if (ORIGINAL_DATA_DIR === undefined) delete process.env.DATA_DIR;
|
||||
else process.env.DATA_DIR = ORIGINAL_DATA_DIR;
|
||||
});
|
||||
|
||||
it("accepts enableGrouping / groupingThreshold on the write schema", () => {
|
||||
assert.equal(
|
||||
rtkConfigSchema.safeParse({ enableGrouping: true, groupingThreshold: 5 }).success,
|
||||
true
|
||||
);
|
||||
// groupingThreshold below the minimum run length (2) is rejected.
|
||||
assert.equal(rtkConfigSchema.safeParse({ groupingThreshold: 1 }).success, false);
|
||||
});
|
||||
|
||||
it("preserves enableGrouping / groupingThreshold through a DB round-trip", async () => {
|
||||
const settings = await updateCompressionSettings({
|
||||
rtkConfig: { ...DEFAULT_RTK_CONFIG, enableGrouping: true, groupingThreshold: 7 },
|
||||
});
|
||||
assert.equal(settings.rtkConfig.enableGrouping, true);
|
||||
assert.equal(settings.rtkConfig.groupingThreshold, 7);
|
||||
|
||||
// Survives a fresh read (not just the write-path return value).
|
||||
core.resetDbInstance();
|
||||
const reread = await getCompressionSettings();
|
||||
assert.equal(reread.rtkConfig.enableGrouping, true);
|
||||
assert.equal(reread.rtkConfig.groupingThreshold, 7);
|
||||
});
|
||||
});
|
||||
@@ -128,4 +128,40 @@ describe("RTK raw output retention", () => {
|
||||
assert.ok(pointer);
|
||||
assert.ok(readRtkRawOutput(pointer.id)?.includes("[REDACTED"));
|
||||
});
|
||||
|
||||
it("never throws when the raw-output write fails (disk error → null pointer)", () => {
|
||||
// Point DATA_DIR underneath a regular FILE so mkdirSync(recursive) throws ENOTDIR.
|
||||
// maybePersistRtkRawOutput is best-effort capture: a write failure must degrade to a
|
||||
// skipped capture (null), never propagate into the compression pipeline (F5.3).
|
||||
const blocker = path.join(os.tmpdir(), `omniroute-rtk-blocked-${process.pid}-${Date.now()}`);
|
||||
fs.writeFileSync(blocker, "x");
|
||||
process.env.DATA_DIR = path.join(blocker, "nested");
|
||||
|
||||
let pointer: ReturnType<typeof maybePersistRtkRawOutput> | undefined;
|
||||
assert.doesNotThrow(() => {
|
||||
pointer = maybePersistRtkRawOutput("error: boom\n" + "noise\n".repeat(8), {
|
||||
retention: "always",
|
||||
});
|
||||
});
|
||||
assert.equal(pointer, null);
|
||||
fs.rmSync(blocker, { force: true });
|
||||
});
|
||||
|
||||
it("redacts Basic/Proxy auth headers and key fields the base patterns miss", () => {
|
||||
const basic = redactRtkRawOutput("> Authorization: Basic dXNlcjpwYXNzd29yZA==");
|
||||
assert.equal(basic.redacted, true);
|
||||
assert.ok(!basic.text.includes("dXNlcjpwYXNzd29yZA=="), "Authorization: Basic base64 redacted");
|
||||
|
||||
const proxy = redactRtkRawOutput("Proxy-Authorization: Basic c2VjcmV0OnBhc3M=");
|
||||
assert.equal(proxy.redacted, true);
|
||||
assert.ok(!proxy.text.includes("c2VjcmV0OnBhc3M="), "Proxy-Authorization redacted");
|
||||
|
||||
const pkey = redactRtkRawOutput("private_key=abc123def456ghi");
|
||||
assert.equal(pkey.redacted, true);
|
||||
assert.ok(!pkey.text.includes("abc123def456ghi"), "private_key value redacted");
|
||||
|
||||
const cred = redactRtkRawOutput("credential: my-credential-value-x");
|
||||
assert.equal(cred.redacted, true);
|
||||
assert.ok(!cred.text.includes("my-credential-value-x"), "credential value redacted");
|
||||
});
|
||||
});
|
||||
|
||||
136
tests/unit/compression/rtk-strip-comments.test.ts
Normal file
136
tests/unit/compression/rtk-strip-comments.test.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import { describe, it, beforeEach, afterEach, after } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import {
|
||||
applyRtkCompression,
|
||||
stripCode,
|
||||
} from "../../../open-sse/services/compression/index.ts";
|
||||
import { rtkConfigSchema } from "../../../src/shared/validation/compressionConfigSchemas.ts";
|
||||
import { DEFAULT_RTK_CONFIG } from "../../../open-sse/services/compression/types.ts";
|
||||
|
||||
// codeStripper has always supported removeComments + preserveDocstrings, but the feature was
|
||||
// unreachable: the RTK engine called stripCode with no options (so comments were never removed
|
||||
// through the runtime), and preserveDocstrings was folded into opts yet never honored by
|
||||
// stripJsTsComments. This proves the new RTK config fields wire it end to end and that
|
||||
// preserveDocstrings now keeps JSDoc.
|
||||
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-rtk-strip-"));
|
||||
const ORIGINAL_DATA_DIR = process.env.DATA_DIR;
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
|
||||
const core = await import("../../../src/lib/db/core.ts");
|
||||
const { getCompressionSettings, updateCompressionSettings } = await import(
|
||||
"../../../src/lib/db/compression.ts"
|
||||
);
|
||||
|
||||
describe("RTK strip-code-comments — stripCode behavior", () => {
|
||||
it("removes line/block comments but keeps JSDoc when preserveDocstrings is on", () => {
|
||||
const code = [
|
||||
"/** Adds two numbers. */",
|
||||
"function add(a, b) {",
|
||||
" // inline note",
|
||||
" return a + b; /* trailing */",
|
||||
"}",
|
||||
].join("\n");
|
||||
const out = stripCode(code, "typescript", {
|
||||
removeComments: true,
|
||||
preserveDocstrings: true,
|
||||
removeEmptyLines: false,
|
||||
collapseWhitespace: false,
|
||||
});
|
||||
assert.ok(out.text.includes("/** Adds two numbers. */"), "JSDoc preserved");
|
||||
assert.ok(!out.text.includes("inline note"), "line comment removed");
|
||||
assert.ok(!out.text.includes("trailing"), "trailing block comment removed");
|
||||
});
|
||||
|
||||
it("removes JSDoc too when preserveDocstrings is off", () => {
|
||||
const out = stripCode("/** doc */\nconst x = 1; // n", "typescript", {
|
||||
removeComments: true,
|
||||
preserveDocstrings: false,
|
||||
removeEmptyLines: false,
|
||||
collapseWhitespace: false,
|
||||
});
|
||||
assert.ok(!out.text.includes("doc"), "JSDoc removed when not preserving");
|
||||
assert.ok(!out.text.includes("// n"), "line comment removed");
|
||||
});
|
||||
});
|
||||
|
||||
describe("RTK strip-code-comments — runtime reachability", () => {
|
||||
it("strips fenced-block comments when applyToCodeBlocks + stripCodeComments are on", () => {
|
||||
// RTK only processes tool/assistant messages (shouldCompressMessage); code-block stripping
|
||||
// rides on applyToCodeBlocks for assistant content regardless of applyToAssistantMessages.
|
||||
const body = {
|
||||
messages: [
|
||||
{
|
||||
role: "assistant",
|
||||
content: "```ts\n// secret note\nconst x = 1;\n/* block secret */\nconst y = 2;\n```",
|
||||
},
|
||||
],
|
||||
};
|
||||
const result = applyRtkCompression(body, {
|
||||
config: {
|
||||
...DEFAULT_RTK_CONFIG,
|
||||
enabled: true,
|
||||
applyToCodeBlocks: true,
|
||||
stripCodeComments: true,
|
||||
},
|
||||
});
|
||||
const serialized = JSON.stringify(result.body.messages);
|
||||
assert.ok(!serialized.includes("secret note"), "line comment stripped via runtime");
|
||||
assert.ok(!serialized.includes("block secret"), "block comment stripped via runtime");
|
||||
assert.match(serialized, /const x = 1/);
|
||||
assert.match(serialized, /const y = 2/);
|
||||
});
|
||||
|
||||
it("leaves fenced-block comments intact when stripCodeComments is off (default)", () => {
|
||||
const body = {
|
||||
messages: [{ role: "assistant", content: "```ts\n// keep me\nconst x = 1;\n```" }],
|
||||
};
|
||||
const result = applyRtkCompression(body, {
|
||||
config: { ...DEFAULT_RTK_CONFIG, enabled: true, applyToCodeBlocks: true },
|
||||
});
|
||||
assert.match(JSON.stringify(result.body.messages), /keep me/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("RTK strip-code-comments — config persistence", () => {
|
||||
beforeEach(() => {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
core.resetDbInstance();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
if (ORIGINAL_DATA_DIR === undefined) delete process.env.DATA_DIR;
|
||||
else process.env.DATA_DIR = ORIGINAL_DATA_DIR;
|
||||
});
|
||||
|
||||
it("accepts stripCodeComments / preserveDocstrings on the write schema", () => {
|
||||
assert.equal(
|
||||
rtkConfigSchema.safeParse({ stripCodeComments: true, preserveDocstrings: false }).success,
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves stripCodeComments / preserveDocstrings through a DB round-trip", async () => {
|
||||
const settings = await updateCompressionSettings({
|
||||
rtkConfig: { ...DEFAULT_RTK_CONFIG, stripCodeComments: true, preserveDocstrings: false },
|
||||
});
|
||||
assert.equal(settings.rtkConfig.stripCodeComments, true);
|
||||
assert.equal(settings.rtkConfig.preserveDocstrings, false);
|
||||
|
||||
core.resetDbInstance();
|
||||
const reread = await getCompressionSettings();
|
||||
assert.equal(reread.rtkConfig.stripCodeComments, true);
|
||||
assert.equal(reread.rtkConfig.preserveDocstrings, false);
|
||||
});
|
||||
});
|
||||
@@ -6,10 +6,7 @@
|
||||
import { describe, it, before } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
sessionDedupEngine,
|
||||
reconstructSessionDedup,
|
||||
} from "../../../open-sse/services/compression/engines/session-dedup/index.ts";
|
||||
import { sessionDedupEngine } from "../../../open-sse/services/compression/engines/session-dedup/index.ts";
|
||||
import {
|
||||
registerBuiltinCompressionEngines,
|
||||
getCompressionEngine,
|
||||
@@ -88,26 +85,6 @@ describe("session-dedup engine", () => {
|
||||
assert.ok(result.stats!.compressedTokens < result.stats!.originalTokens);
|
||||
});
|
||||
|
||||
it("round-trip: reconstructSessionDedup restores original body exactly", () => {
|
||||
const body = makeBody([
|
||||
{ role: "user", content: `Here is the code:\n${REPEATED_BLOCK}` },
|
||||
{ role: "assistant", content: "I understand the code." },
|
||||
{ role: "user", content: `Please review again:\n${REPEATED_BLOCK}` },
|
||||
]);
|
||||
|
||||
const result = sessionDedupEngine.apply(body as Record<string, unknown>);
|
||||
assert.equal(result.compressed, true);
|
||||
|
||||
const restored = reconstructSessionDedup(result.body);
|
||||
|
||||
// Deep-equal to original
|
||||
assert.deepEqual(
|
||||
restored.messages,
|
||||
body.messages,
|
||||
"reconstructed body must deep-equal original"
|
||||
);
|
||||
});
|
||||
|
||||
it("does NOT dedup small/unique blocks (no false positives)", () => {
|
||||
const body = makeBody([
|
||||
{ role: "user", content: "hi" },
|
||||
|
||||
103
tests/unit/compression/ultra-slm-tier.test.ts
Normal file
103
tests/unit/compression/ultra-slm-tier.test.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* Ultra SLM tier — wiring the ultra mode's modelPath / slmFallbackToAggressive config
|
||||
* to the real model path (the llmlingua engine).
|
||||
*
|
||||
* Until now ultra was a pure heuristic (pruneByScore) and modelPath /
|
||||
* slmFallbackToAggressive were inert config. The async entry point now routes ultra
|
||||
* through the llmlingua engine when modelPath is set, falling back per
|
||||
* slmFallbackToAggressive when the model is unavailable / yields no gain.
|
||||
*
|
||||
* The llmlingua backend is injectable (setLlmlinguaBackend), so the tier is testable
|
||||
* without the real ONNX model.
|
||||
*/
|
||||
import { describe, it, after, afterEach } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { applyCompressionAsync } from "../../../open-sse/services/compression/index.ts";
|
||||
import { setLlmlinguaBackend } from "../../../open-sse/services/compression/engines/llmlingua/index.ts";
|
||||
import { DEFAULT_ULTRA_CONFIG } from "../../../open-sse/services/compression/types.ts";
|
||||
|
||||
// Comfortably above the llmlingua default 2000-token floor (estimate ≈ chars / 4).
|
||||
const LARGE_PROSE = "The quick brown fox jumps over the lazy dog every morning. ".repeat(260);
|
||||
|
||||
function body() {
|
||||
return { model: "gpt-4o", messages: [{ role: "user", content: LARGE_PROSE }] };
|
||||
}
|
||||
|
||||
function ultraOpts(ultra: Record<string, unknown>) {
|
||||
// Only config.ultra is read by the ultra SLM tier; the rest of CompressionConfig is unused here.
|
||||
return { config: { ultra: { ...DEFAULT_ULTRA_CONFIG, ...ultra } } } as unknown as Parameters<
|
||||
typeof applyCompressionAsync
|
||||
>[2];
|
||||
}
|
||||
|
||||
let backendCalls = 0;
|
||||
function trackingCompressingBackend(text: string): Promise<string> {
|
||||
backendCalls++;
|
||||
return Promise.resolve(text.slice(0, Math.max(1, Math.floor(text.length / 3))));
|
||||
}
|
||||
function identityBackend(text: string): Promise<string> {
|
||||
backendCalls++;
|
||||
return Promise.resolve(text); // no gain → llmlingua reports compressed:false
|
||||
}
|
||||
function throwingBackend(_text: string): Promise<string> {
|
||||
backendCalls++;
|
||||
return Promise.reject(new Error("model unavailable"));
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
backendCalls = 0;
|
||||
});
|
||||
after(() => setLlmlinguaBackend(null));
|
||||
|
||||
function techniques(stats: unknown): string[] {
|
||||
return ((stats as { techniquesUsed?: string[] } | null)?.techniquesUsed ?? []) as string[];
|
||||
}
|
||||
|
||||
describe("ultra SLM tier — modelPath routes through llmlingua", () => {
|
||||
it("runs the SLM tier when modelPath is set and the model compresses", async () => {
|
||||
setLlmlinguaBackend(trackingCompressingBackend);
|
||||
const result = await applyCompressionAsync(
|
||||
body(),
|
||||
"ultra",
|
||||
ultraOpts({ modelPath: "/models/fake.onnx", compressionRate: 0.5 })
|
||||
);
|
||||
assert.equal(backendCalls > 0, true, "backend was consulted");
|
||||
assert.equal(result.compressed, true);
|
||||
assert.equal((result.stats as { mode?: string } | null)?.mode, "ultra");
|
||||
assert.ok(techniques(result.stats).includes("ultra-slm"), "tagged as the ultra SLM tier");
|
||||
});
|
||||
|
||||
it("falls back to aggressive when the model yields no gain and slmFallbackToAggressive is on", async () => {
|
||||
setLlmlinguaBackend(identityBackend);
|
||||
const result = await applyCompressionAsync(
|
||||
body(),
|
||||
"ultra",
|
||||
ultraOpts({ modelPath: "/models/fake.onnx", slmFallbackToAggressive: true })
|
||||
);
|
||||
assert.ok(techniques(result.stats).includes("aggressive"), "fell back to aggressive");
|
||||
assert.ok(!techniques(result.stats).includes("ultra-slm"));
|
||||
});
|
||||
|
||||
it("falls back to the heuristic when the model fails and slmFallbackToAggressive is off", async () => {
|
||||
setLlmlinguaBackend(throwingBackend);
|
||||
const result = await applyCompressionAsync(
|
||||
body(),
|
||||
"ultra",
|
||||
ultraOpts({ modelPath: "/models/fake.onnx", slmFallbackToAggressive: false })
|
||||
);
|
||||
const techs = techniques(result.stats);
|
||||
assert.equal((result.stats as { mode?: string } | null)?.mode, "ultra");
|
||||
assert.ok(techs.includes("ultra"), "heuristic ultra ran");
|
||||
assert.ok(!techs.includes("ultra-slm"), "not the SLM tier");
|
||||
assert.ok(!techs.includes("aggressive"), "not the aggressive fallback");
|
||||
});
|
||||
|
||||
it("uses the heuristic and never touches the model when modelPath is unset", async () => {
|
||||
setLlmlinguaBackend(throwingBackend); // would blow up if (wrongly) consulted
|
||||
const result = await applyCompressionAsync(body(), "ultra", ultraOpts({ modelPath: "" }));
|
||||
assert.equal(backendCalls, 0, "model not consulted without modelPath");
|
||||
assert.equal((result.stats as { mode?: string } | null)?.mode, "ultra");
|
||||
assert.ok(!techniques(result.stats).includes("ultra-slm"));
|
||||
});
|
||||
});
|
||||
@@ -6,11 +6,7 @@ import {
|
||||
STOPWORDS,
|
||||
FORCE_PRESERVE_RE,
|
||||
} from "../../../open-sse/services/compression/ultraHeuristic.ts";
|
||||
import {
|
||||
ultraCompress,
|
||||
createSLMStub,
|
||||
type SLMInterface,
|
||||
} from "../../../open-sse/services/compression/ultra.ts";
|
||||
import { ultraCompress } from "../../../open-sse/services/compression/ultra.ts";
|
||||
import type { UltraConfig } from "../../../open-sse/services/compression/types.ts";
|
||||
|
||||
describe("scoreToken", () => {
|
||||
@@ -255,35 +251,3 @@ describe("ultraCompress", () => {
|
||||
assert.strictEqual(result.stats.savingsPercent, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createSLMStub", () => {
|
||||
it("should return object with compress function", () => {
|
||||
const stub = createSLMStub();
|
||||
assert(stub);
|
||||
assert(typeof stub.compress === "function");
|
||||
});
|
||||
|
||||
it("stub.compress should return a string", async () => {
|
||||
const stub = createSLMStub();
|
||||
const result = await stub.compress("hello world", 0.5);
|
||||
assert(typeof result === "string");
|
||||
});
|
||||
|
||||
it("stub.compress result should be <= input length", async () => {
|
||||
const stub = createSLMStub();
|
||||
const input = "the quick brown fox jumps over the lazy dog";
|
||||
const result = await stub.compress(input, 0.5);
|
||||
assert(result.length <= input.length);
|
||||
});
|
||||
|
||||
it("stub.compress on empty string should return empty", async () => {
|
||||
const stub = createSLMStub();
|
||||
const result = await stub.compress("", 0.5);
|
||||
assert.strictEqual(result, "");
|
||||
});
|
||||
|
||||
it("stub should have a name or identifier", () => {
|
||||
const stub = createSLMStub();
|
||||
assert(stub !== null && typeof stub === "object");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -43,12 +43,25 @@ function seedConnections(count = 8) {
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForFile(filePath) {
|
||||
for (let attempt = 0; attempt < 20; attempt++) {
|
||||
if (fs.existsSync(filePath)) return;
|
||||
// backupDbFile() kicks off db.backup() fire-and-forget — better-sqlite3 creates
|
||||
// the destination file when the page copy STARTS, not when it finishes. Waiting
|
||||
// on file existence alone races the copy: under load listDbBackups() can open a
|
||||
// partially-written backup and read connectionCount as 0 (flake repro: under CPU
|
||||
// contention the backup file existed but the seeded rows were not yet copied →
|
||||
// connectionCount 0 ≠ 12). Wait for the real completion condition instead — the
|
||||
// backup actually contains the seeded connections. The 30s ceiling only bounds
|
||||
// the failure case; the green path returns as soon as the data lands (sub-second
|
||||
// in practice — the wide ceiling absorbs heavy CI page-copy contention).
|
||||
async function waitForBackupEntry(filename, expectedConnectionCount, timeoutMs = 30000) {
|
||||
const startedAt = Date.now();
|
||||
while (Date.now() - startedAt < timeoutMs) {
|
||||
const entry = (await backupDb.listDbBackups()).find((backup) => backup.id === filename);
|
||||
if (entry && entry.connectionCount === expectedConnectionCount) return entry;
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
}
|
||||
throw new Error(`Timed out waiting for file: ${filePath}`);
|
||||
throw new Error(
|
||||
`Timed out waiting for backup ${filename} to finish copying ${expectedConnectionCount} connections`
|
||||
);
|
||||
}
|
||||
|
||||
function makeDbBackupsJsonRequest(method: string, body: unknown): NextRequest {
|
||||
@@ -75,13 +88,12 @@ test("backupDbFile creates manual backups and listDbBackups returns metadata", a
|
||||
assert.ok(result);
|
||||
|
||||
const backupPath = path.join(core.DB_BACKUPS_DIR, result.filename);
|
||||
await waitForFile(backupPath);
|
||||
// Wait for the async backup to finish copying the 12 seeded connections, not
|
||||
// merely for the destination file to appear (see waitForBackupEntry).
|
||||
const entry = await waitForBackupEntry(result.filename, 12);
|
||||
|
||||
const backups = await backupDb.listDbBackups();
|
||||
|
||||
assert.equal(backups.length >= 1, true);
|
||||
assert.equal(backups[0].reason, "manual");
|
||||
assert.equal(backups[0].connectionCount, 12);
|
||||
assert.equal(entry.reason, "manual");
|
||||
assert.equal(entry.connectionCount, 12);
|
||||
assert.equal(fs.existsSync(backupPath), true);
|
||||
});
|
||||
|
||||
|
||||
306
tests/unit/design-grid-background.test.ts
Normal file
306
tests/unit/design-grid-background.test.ts
Normal file
@@ -0,0 +1,306 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
|
||||
// Static guards for the shared visual identity (Phase 1: graph-paper grid wallpaper).
|
||||
// These lock in the cross-product design contract so an accidental edit can't silently
|
||||
// remove the grid or re-introduce the opaque wrapper that hides it. See design.md.
|
||||
|
||||
const globalsCss = fs.readFileSync(new URL("../../src/app/globals.css", import.meta.url), "utf8");
|
||||
const dashboardLayout = fs.readFileSync(
|
||||
new URL("../../src/shared/components/layouts/DashboardLayout.tsx", import.meta.url),
|
||||
"utf8"
|
||||
);
|
||||
|
||||
test("globals.css defines the grid wallpaper tokens for both themes", () => {
|
||||
// light (opacity tuned up from the site's 0.045 so the grid is visible on the
|
||||
// dense dashboard — see the token comment in globals.css)
|
||||
assert.match(globalsCss, /--grid-line:\s*rgba\(0,\s*0,\s*0,\s*0\.07\)/);
|
||||
// dark
|
||||
assert.match(globalsCss, /--grid-line:\s*rgba\(255,\s*255,\s*255,\s*0\.06\)/);
|
||||
// size (shrunk ~30% from 46px for a tighter grid) + alternating-section overlay
|
||||
assert.match(globalsCss, /--grid-size:\s*32px/);
|
||||
assert.match(globalsCss, /--section-alt:\s*rgba\(0,\s*0,\s*0,\s*0\.022\)/);
|
||||
assert.match(globalsCss, /--section-alt:\s*rgba\(255,\s*255,\s*255,\s*0\.018\)/);
|
||||
});
|
||||
|
||||
test("globals.css renders the grid via a body::before fixed layer", () => {
|
||||
// The pseudo-element must exist and be the grid renderer.
|
||||
const before = globalsCss.slice(globalsCss.indexOf("body::before"));
|
||||
assert.ok(before.length > 0, "body::before rule is present");
|
||||
assert.match(before, /position:\s*fixed/);
|
||||
assert.match(before, /z-index:\s*-1/);
|
||||
assert.match(before, /pointer-events:\s*none/);
|
||||
assert.match(before, /linear-gradient\(to right,\s*var\(--grid-line\) 1px, transparent 1px\)/);
|
||||
assert.match(before, /linear-gradient\(to bottom,\s*var\(--grid-line\) 1px, transparent 1px\)/);
|
||||
assert.match(before, /background-size:\s*var\(--grid-size\) var\(--grid-size\)/);
|
||||
});
|
||||
|
||||
test("globals.css adds the shared identity tokens", () => {
|
||||
assert.match(globalsCss, /--surface-2:\s*#f5f5fa/); // light
|
||||
assert.match(globalsCss, /--surface-2:\s*#1c2230/); // dark
|
||||
assert.match(globalsCss, /--radius:\s*14px/);
|
||||
assert.match(
|
||||
globalsCss,
|
||||
/--grad-brand:\s*linear-gradient\(135deg,\s*var\(--color-primary\),\s*var\(--color-accent-light\)\)/
|
||||
);
|
||||
// exposed to Tailwind as bg-surface-2 for later phases
|
||||
assert.match(globalsCss, /--color-surface-2:\s*var\(--surface-2\)/);
|
||||
});
|
||||
|
||||
test("DashboardLayout wrapper stays transparent so the grid shows through", () => {
|
||||
// Regression guard: the outer shell must NOT paint an opaque bg over the body grid.
|
||||
assert.ok(
|
||||
!dashboardLayout.includes("overflow-hidden bg-bg"),
|
||||
"DashboardLayout outer wrapper must not use bg-bg (it would hide the grid wallpaper)"
|
||||
);
|
||||
assert.ok(
|
||||
dashboardLayout.includes('className="flex h-dvh min-h-0 w-full overflow-hidden"'),
|
||||
"DashboardLayout outer wrapper is present and transparent"
|
||||
);
|
||||
});
|
||||
|
||||
// ── Phase 2: primitives adopt the shared radius scale, brand gradient & border token ──
|
||||
|
||||
const read = (p: string) => fs.readFileSync(new URL(p, import.meta.url), "utf8");
|
||||
|
||||
test("globals.css exposes the semantic radius utilities", () => {
|
||||
assert.match(globalsCss, /--radius-control:\s*9px/); // :root value
|
||||
assert.match(globalsCss, /--radius-card:\s*var\(--radius\)/); // @theme → rounded-card (14px)
|
||||
assert.match(globalsCss, /--radius-control:\s*var\(--radius-control\)/); // @theme → rounded-control
|
||||
});
|
||||
|
||||
test("Button uses the brand gradient + accent variant + control radius", () => {
|
||||
const button = read("../../src/shared/components/Button.tsx");
|
||||
assert.match(button, /primary:\s*"bg-\[image:var\(--grad-brand\)\]/);
|
||||
assert.match(button, /accent:\s*"bg-accent/);
|
||||
assert.ok(
|
||||
!button.includes("from-primary to-primary-hover"),
|
||||
"the flat red→red gradient is replaced by --grad-brand"
|
||||
);
|
||||
assert.ok(button.includes("rounded-control"), "button sizes use the control radius");
|
||||
});
|
||||
|
||||
test("Card / Modal / Input / Select adopt the radius scale and border token", () => {
|
||||
const card = read("../../src/shared/components/Card.tsx");
|
||||
const modal = read("../../src/shared/components/Modal.tsx");
|
||||
const input = read("../../src/shared/components/Input.tsx");
|
||||
const select = read("../../src/shared/components/Select.tsx");
|
||||
assert.ok(card.includes("border border-border"), "card uses the --color-border token");
|
||||
assert.ok(card.includes("rounded-card"), "card uses rounded-card (14px)");
|
||||
assert.ok(!card.includes("border-black/5"), "the under-weight /5 border is gone");
|
||||
assert.ok(modal.includes("rounded-card"), "modal uses rounded-card");
|
||||
assert.ok(input.includes("rounded-control"), "input uses rounded-control (9px)");
|
||||
assert.ok(select.includes("rounded-control"), "select uses rounded-control (9px)");
|
||||
});
|
||||
|
||||
// ── Phase 3 (partial): status hex centralized + mono font token ──
|
||||
|
||||
test("status colors come from one canonical module", () => {
|
||||
const mod = read("../../src/shared/constants/statusColors.ts");
|
||||
assert.match(mod, /success:\s*"#22c55e"/);
|
||||
assert.match(mod, /warning:\s*"#f59e0b"/);
|
||||
assert.match(mod, /error:\s*"#ef4444"/);
|
||||
|
||||
const edges = read("../../src/shared/components/flow/edgeStyles.ts");
|
||||
const badge = read("../../src/shared/components/TokenHealthBadge.tsx");
|
||||
assert.ok(
|
||||
edges.includes('from "@/shared/constants/statusColors"'),
|
||||
"edgeStyles imports the module"
|
||||
);
|
||||
assert.ok(edges.includes("STATUS_HEX.success"), "edgeStyles uses STATUS_HEX, not a literal");
|
||||
assert.ok(!edges.includes('"#22c55e"'), "edgeStyles no longer hardcodes the success hex");
|
||||
assert.ok(badge.includes("STATUS_HEX.success"), "TokenHealthBadge uses STATUS_HEX");
|
||||
assert.ok(!badge.includes('"#22c55e"'), "TokenHealthBadge no longer hardcodes the success hex");
|
||||
});
|
||||
|
||||
test("globals.css defines a monospace token (site parity)", () => {
|
||||
assert.match(globalsCss, /--font-mono:\s*ui-monospace/);
|
||||
});
|
||||
|
||||
test("DataTable is theme-aware via --table-* tokens (dark = the exact old values)", () => {
|
||||
// The dark token values must equal the rgba the component used to hardcode, so dark
|
||||
// stays byte-identical while light gets fixed.
|
||||
assert.match(globalsCss, /--table-header-bg:\s*rgba\(15,\s*15,\s*25,\s*0\.95\)/); // dark
|
||||
assert.match(globalsCss, /--table-row-zebra:\s*rgba\(255,\s*255,\s*255,\s*0\.02\)/); // dark
|
||||
assert.match(globalsCss, /--table-row-hover:\s*rgba\(255,\s*255,\s*255,\s*0\.04\)/); // dark
|
||||
assert.match(globalsCss, /--table-cell-border:\s*rgba\(255,\s*255,\s*255,\s*0\.04\)/); // dark
|
||||
assert.match(globalsCss, /--table-header-bg:\s*rgba\(249,\s*249,\s*251,\s*0\.95\)/); // light fix
|
||||
|
||||
const dt = read("../../src/shared/components/DataTable.tsx");
|
||||
assert.ok(dt.includes("var(--table-header-bg)"), "header uses the token");
|
||||
assert.ok(dt.includes("var(--table-row-zebra)"), "zebra uses the token");
|
||||
assert.ok(dt.includes("var(--color-border)"), "header border uses the brand token");
|
||||
assert.ok(
|
||||
!/rgba\(|#[0-9a-fA-F]{3,6}/.test(dt),
|
||||
"DataTable no longer hardcodes any color literal"
|
||||
);
|
||||
assert.ok(
|
||||
!dt.includes("--text-secondary") && !dt.includes("--bg-table-header"),
|
||||
"the dead var fallbacks are gone"
|
||||
);
|
||||
});
|
||||
|
||||
// ── Phase 4 (safe additives): cn() merge + Checkbox / Textarea primitives ──
|
||||
|
||||
test("cn() dedupes conflicting Tailwind classes via tailwind-merge", () => {
|
||||
const cnSrc = read("../../src/shared/utils/cn.ts");
|
||||
assert.match(cnSrc, /from "tailwind-merge"/);
|
||||
assert.match(cnSrc, /from "clsx"/);
|
||||
assert.match(cnSrc, /twMerge\(clsx\(/);
|
||||
});
|
||||
|
||||
test("Checkbox + Textarea primitives exist and are exported", () => {
|
||||
const barrel = read("../../src/shared/components/index.tsx");
|
||||
assert.ok(barrel.includes('export { default as Checkbox } from "./Checkbox"'));
|
||||
assert.ok(barrel.includes('export { default as Textarea } from "./Textarea"'));
|
||||
const checkbox = read("../../src/shared/components/Checkbox.tsx");
|
||||
const textarea = read("../../src/shared/components/Textarea.tsx");
|
||||
assert.ok(
|
||||
checkbox.includes("accent-[var(--color-accent)]"),
|
||||
"checkbox uses the brand accent token"
|
||||
);
|
||||
assert.ok(textarea.includes("rounded-control"), "textarea uses the control radius");
|
||||
});
|
||||
|
||||
// ── C6: form controls share one accent focus ring (separate from the red error state) ──
|
||||
|
||||
test("form controls focus on the accent ring, not the red primary", () => {
|
||||
// The global :focus-visible ring already uses --color-accent. Align the form
|
||||
// controls to it so keyboard focus is one consistent violet everywhere and the
|
||||
// red focus ring no longer collides with the red error state.
|
||||
assert.match(globalsCss, /--focus-ring:.*var\(--color-accent\)/);
|
||||
for (const name of ["Input", "Select", "Textarea", "Toggle", "Checkbox"]) {
|
||||
const src = read(`../../src/shared/components/${name}.tsx`);
|
||||
assert.ok(/ring-accent\/30/.test(src), `${name} uses the accent focus ring`);
|
||||
assert.ok(
|
||||
!/(?:focus|focus-visible):ring-primary\/30/.test(src),
|
||||
`${name} no longer uses the red primary focus ring`
|
||||
);
|
||||
// the red error ring stays intact where the control has an error state
|
||||
if (src.includes("error")) {
|
||||
assert.ok(src.includes("ring-red-500/20"), `${name} keeps the red error ring`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ── Phase 5: the grid reaches every standalone screen + the shell is fluid up to 4K ──
|
||||
|
||||
test("standalone full-screen pages stay transparent so the grid shows through", () => {
|
||||
// Same contract as the DashboardLayout shell: a full-viewport wrapper must not paint
|
||||
// an opaque bg-bg over the body::before grid. These are the auth / error / legal /
|
||||
// status / onboarding screens that render outside the dashboard layout — the ones the
|
||||
// first grid phase missed. (?![\w-]) keeps bg-bg-alt / bg-bg-main from matching.
|
||||
const pages = [
|
||||
"../../src/app/login/page.tsx",
|
||||
"../../src/app/forgot-password/page.tsx",
|
||||
"../../src/app/callback/page.tsx",
|
||||
"../../src/app/maintenance/page.tsx",
|
||||
"../../src/app/offline/page.tsx",
|
||||
"../../src/app/status/page.tsx",
|
||||
"../../src/app/terms/page.tsx",
|
||||
"../../src/app/privacy/page.tsx",
|
||||
"../../src/app/(dashboard)/dashboard/onboarding/page.tsx",
|
||||
"../../src/shared/components/ErrorPageScaffold.tsx",
|
||||
];
|
||||
for (const p of pages) {
|
||||
const src = read(p);
|
||||
assert.ok(
|
||||
!/min-h-screen[^"]*\bbg-bg(?![\w-])/.test(src) &&
|
||||
!/\bbg-bg(?![\w-])[^"]*min-h-screen/.test(src),
|
||||
`${p} must not paint bg-bg on a min-h-screen wrapper (it would hide the grid)`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("DashboardLayout content shell is fluid up to ~4K before centering", () => {
|
||||
// The inner content wrapper grows with the viewport up to a 4K cap (3840px) instead
|
||||
// of the old max-w-7xl (1280px) that left wide side gutters on large monitors.
|
||||
assert.ok(
|
||||
dashboardLayout.includes("max-w-[3840px] mx-auto"),
|
||||
"content wrapper caps at 3840px (4K) and centers only beyond that"
|
||||
);
|
||||
assert.ok(!dashboardLayout.includes("max-w-7xl"), "the old 1280px max-w-7xl cap is gone");
|
||||
});
|
||||
|
||||
// ── Phase 6: data tables are opaque content surfaces so the grid never bleeds through ──
|
||||
//
|
||||
// The dashboard content area is intentionally transparent (the body::before grid shows
|
||||
// through as a wallpaper). A data table whose nearest ancestor is NOT an opaque surface
|
||||
// would let the grid bleed through its transparent even-rows / low-alpha zebra rows.
|
||||
// Cards already carry bg-surface; these guards cover the shared table primitives and the
|
||||
// tables that render *without* a Card. Tables verified to live inside a <Card>/Modal are
|
||||
// intentionally left untouched (bg-surface there would be a redundant no-op).
|
||||
|
||||
test("DataTable primitive paints its own opaque surface", () => {
|
||||
const dt = read("../../src/shared/components/DataTable.tsx");
|
||||
assert.ok(
|
||||
dt.includes("var(--color-surface)"),
|
||||
"DataTable scroll container is opaque (its even rows are transparent by design)"
|
||||
);
|
||||
});
|
||||
|
||||
test("log table cards are opaque (no semi-transparent bg-black tint)", () => {
|
||||
// bg-black/5|20 on a <Card> wins over the Card's own bg-surface via tailwind-merge,
|
||||
// turning the big log tables ~95% transparent — the grid bled straight through them.
|
||||
for (const p of [
|
||||
"../../src/shared/components/ProxyLogger.tsx",
|
||||
"../../src/shared/components/RequestLoggerV2.tsx",
|
||||
]) {
|
||||
const src = read(p);
|
||||
assert.ok(
|
||||
!src.includes("bg-black/5") && !src.includes("bg-black/20"),
|
||||
`${p} must not tint the table Card with bg-black/5|20 (it drops the Card's opaque surface)`
|
||||
);
|
||||
assert.ok(src.includes("bg-surface"), `${p} table card uses the opaque bg-surface`);
|
||||
}
|
||||
});
|
||||
|
||||
test("card-less data tables wrap their table in an opaque surface", () => {
|
||||
const expect = [
|
||||
[
|
||||
"../../src/app/(dashboard)/dashboard/batch/BatchListTab.tsx",
|
||||
"rounded-xl border border-[var(--color-border)] bg-[var(--color-surface)]",
|
||||
],
|
||||
[
|
||||
"../../src/app/(dashboard)/dashboard/batch/FilesListTab.tsx",
|
||||
"rounded-xl border border-[var(--color-border)] bg-[var(--color-surface)]",
|
||||
],
|
||||
[
|
||||
"../../src/app/(dashboard)/dashboard/cache/components/CacheEntriesTab.tsx",
|
||||
"overflow-x-auto bg-surface",
|
||||
],
|
||||
[
|
||||
"../../src/app/(dashboard)/dashboard/settings/components/proxy/FreePoolTab.tsx",
|
||||
"rounded border border-border bg-surface",
|
||||
],
|
||||
[
|
||||
"../../src/app/(dashboard)/dashboard/tools/agent-bridge/components/ModelMappingTable.tsx",
|
||||
"overflow-hidden bg-surface",
|
||||
],
|
||||
[
|
||||
"../../src/app/(dashboard)/dashboard/tools/traffic-inspector/components/shared/HeaderTable.tsx",
|
||||
"bg-surface",
|
||||
],
|
||||
];
|
||||
for (const [p, needle] of expect) {
|
||||
const src = read(p);
|
||||
assert.ok(
|
||||
src.includes(needle),
|
||||
`${p} must include "${needle}" so the table is opaque over the grid`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("semi-transparent cache table boxes are now opaque", () => {
|
||||
for (const p of [
|
||||
"../../src/app/(dashboard)/dashboard/cache/components/ReasoningCacheTab.tsx",
|
||||
"../../src/app/(dashboard)/dashboard/cache/page.tsx",
|
||||
]) {
|
||||
const src = read(p);
|
||||
assert.ok(
|
||||
!src.includes("bg-surface/35"),
|
||||
`${p} table box no longer uses the ~35%-opaque bg-surface/35 (the grid bled through it)`
|
||||
);
|
||||
}
|
||||
});
|
||||
38
tests/unit/discontinued-providers-2026.test.ts
Normal file
38
tests/unit/discontinued-providers-2026.test.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert";
|
||||
|
||||
// 2026-06-17 free-tier refresh + 2026-06-18 live re-verification: providers whose free tier is
|
||||
// confirmed gone have hasFree flipped to false so the dashboard / onboarding no longer advertises a
|
||||
// free tier that does not exist. The budget catalog already dropped them. The 2026-06-18 batch
|
||||
// (gitlawb, gitlawb-gmi, aimlapi, yi) was each re-verified against the official source before flipping
|
||||
// (aimlapi docs: "The Free Tier is currently paused"; gitlawb GitHub issue #1345: MiMo revoked).
|
||||
describe("2026 discontinued free tiers — providers.ts hasFree reconciliation", () => {
|
||||
it("APIKEY_PROVIDERS dead tiers no longer advertise a free tier", async () => {
|
||||
const { APIKEY_PROVIDERS } = await import("../../src/shared/constants/providers.ts");
|
||||
for (const id of ["chutes", "kluster", "glhf", "phind", "gitlawb", "gitlawb-gmi", "aimlapi", "yi"]) {
|
||||
const p = (APIKEY_PROVIDERS as Record<string, { hasFree?: boolean }>)[id];
|
||||
assert.ok(p, `${id} should still exist in APIKEY_PROVIDERS (provider not removed, only its free flag)`);
|
||||
assert.strictEqual(p.hasFree, false, `${id} should have hasFree:false (discontinued in 2026)`);
|
||||
}
|
||||
});
|
||||
|
||||
it("WEB_COOKIE_PROVIDERS phind (web/cookie path) no longer advertises a free tier", async () => {
|
||||
const { WEB_COOKIE_PROVIDERS } = await import("../../src/shared/constants/providers.ts");
|
||||
const p = (WEB_COOKIE_PROVIDERS as Record<string, { hasFree?: boolean }>)["phind"];
|
||||
assert.ok(p, "phind should still exist in WEB_COOKIE_PROVIDERS");
|
||||
assert.strictEqual(p.hasFree, false, "phind web/cookie should have hasFree:false (phind.com shut down 2026-01)");
|
||||
});
|
||||
|
||||
it("intentionally-kept providers still advertise free (genuinely free / ToS-flagged, not flipped)", async () => {
|
||||
const { NOAUTH_PROVIDERS, APIKEY_PROVIDERS } = await import("../../src/shared/constants/providers.ts");
|
||||
// theoldllm is a keyless, no-signup web chat (genuinely free, just no catalogable API tier) — kept.
|
||||
// iflytek/sparkdesk stay hasFree:true but carry a ToS-caution freeNote (Spark Lite is free, the ToS
|
||||
// restricts proxy/relay use). gitlawb/gitlawb-gmi/aimlapi/yi were re-verified dead 2026-06-18 and are
|
||||
// asserted false above — keeping them out of this list guards against a silent re-flip-to-true.
|
||||
const noauth = NOAUTH_PROVIDERS as Record<string, { hasFree?: boolean }>;
|
||||
const apikey = APIKEY_PROVIDERS as Record<string, { hasFree?: boolean; freeNote?: string }>;
|
||||
assert.strictEqual(noauth["theoldllm"]?.hasFree, true, "theoldllm intentionally kept hasFree:true");
|
||||
assert.strictEqual(apikey["iflytek"]?.hasFree, true, "iflytek kept free with ToS-caution note");
|
||||
assert.match(apikey["iflytek"]?.freeNote ?? "", /caution/i, "iflytek freeNote should carry a caution");
|
||||
});
|
||||
});
|
||||
76
tests/unit/embeddings-cost-telemetry-headers.test.ts
Normal file
76
tests/unit/embeddings-cost-telemetry-headers.test.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
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";
|
||||
|
||||
// Isolate the DB to a temp dir BEFORE importing any module that opens it.
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-embed-telemetry-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const providersDb = await import("../../src/lib/db/providers.ts");
|
||||
const { createEmbeddingResponse } = await import("../../src/lib/embeddings/service.ts");
|
||||
const { OMNIROUTE_RESPONSE_HEADERS } = await import("../../src/shared/constants/headers.ts");
|
||||
|
||||
test.after(() => {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("createEmbeddingResponse emits X-OmniRoute-* cost telemetry headers on success", async () => {
|
||||
// Seed a credentialed apikey connection so getProviderCredentials resolves and
|
||||
// the success path runs (no real upstream is hit — fetch is mocked below).
|
||||
await providersDb.createProviderConnection({
|
||||
provider: "mistral",
|
||||
authType: "apikey",
|
||||
name: "Test Mistral",
|
||||
apiKey: "mistral-test-key",
|
||||
});
|
||||
|
||||
const PROMPT_TOKENS = 7;
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = async () =>
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
data: [{ object: "embedding", embedding: [0.1, 0.2], index: 0 }],
|
||||
usage: { prompt_tokens: PROMPT_TOKENS, total_tokens: PROMPT_TOKENS },
|
||||
}),
|
||||
{ status: 200, headers: { "content-type": "application/json" } }
|
||||
);
|
||||
|
||||
try {
|
||||
const res = await createEmbeddingResponse({
|
||||
model: "mistral/mistral-embed",
|
||||
input: "hello world",
|
||||
});
|
||||
|
||||
assert.equal(res.status, 200, "embedding success path should return 200");
|
||||
|
||||
const cost = res.headers.get(OMNIROUTE_RESPONSE_HEADERS.responseCost);
|
||||
assert.ok(cost, "X-OmniRoute-Response-Cost header must be present");
|
||||
assert.match(
|
||||
cost,
|
||||
/^\d+\.\d{10}$/,
|
||||
`X-OmniRoute-Response-Cost must be a 10-decimal cost string, got "${cost}"`
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
res.headers.get(OMNIROUTE_RESPONSE_HEADERS.tokensIn),
|
||||
String(PROMPT_TOKENS),
|
||||
"X-OmniRoute-Tokens-In must equal the upstream prompt_tokens"
|
||||
);
|
||||
|
||||
const version = res.headers.get(OMNIROUTE_RESPONSE_HEADERS.version);
|
||||
assert.ok(
|
||||
version && version.length > 0,
|
||||
"X-OmniRoute-Version header must be present and non-empty"
|
||||
);
|
||||
|
||||
// Sanity: the body is still the embeddings payload, unchanged.
|
||||
const body = await res.json();
|
||||
assert.deepEqual(body.usage, { prompt_tokens: PROMPT_TOKENS, total_tokens: PROMPT_TOKENS });
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
141
tests/unit/free-models.test.ts
Normal file
141
tests/unit/free-models.test.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
isFreeModel,
|
||||
providerHasFreeModels,
|
||||
selectModelsForImport,
|
||||
sortModelsFreeFirst,
|
||||
} from "@/shared/utils/freeModels";
|
||||
import { FREE_MODEL_BUDGETS } from "@omniroute/open-sse/config/freeModelCatalog";
|
||||
|
||||
test("providerHasFreeModels: true for a provider in the free catalog", () => {
|
||||
assert.equal(providerHasFreeModels("openrouter"), true);
|
||||
});
|
||||
|
||||
test("providerHasFreeModels: false for an unknown provider", () => {
|
||||
assert.equal(providerHasFreeModels("totally-not-a-real-provider-xyz"), false);
|
||||
});
|
||||
|
||||
test("providerHasFreeModels: resolves a provider alias to its canonical id", () => {
|
||||
// "ollamacloud" is the dashboard alias for the canonical id "ollama-cloud".
|
||||
assert.equal(providerHasFreeModels("ollamacloud"), true);
|
||||
});
|
||||
|
||||
test("isFreeModel: catalog membership works when called with the provider alias", () => {
|
||||
// deepseek-v4-pro is a free ollama-cloud model; the list view calls with the alias.
|
||||
assert.equal(isFreeModel("ollamacloud", { id: "deepseek-v4-pro" }), true);
|
||||
});
|
||||
|
||||
test("isFreeModel: model id ending in :free is free", () => {
|
||||
assert.equal(isFreeModel("openrouter", { id: "deepseek/deepseek-r1:free" }), true);
|
||||
});
|
||||
|
||||
test("isFreeModel: zero prompt+completion price is free", () => {
|
||||
assert.equal(
|
||||
isFreeModel("openrouter", { id: "x/y", pricing: { prompt: "0", completion: "0" } }),
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
test("isFreeModel: a priced model is NOT free", () => {
|
||||
assert.equal(
|
||||
isFreeModel("openrouter", {
|
||||
id: "openai/gpt-4o",
|
||||
pricing: { prompt: "0.0000025", completion: "0.00001" },
|
||||
}),
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
test("isFreeModel: a model with no pricing and no :free suffix is NOT free", () => {
|
||||
assert.equal(isFreeModel("openrouter", { id: "some/paid-model" }), false);
|
||||
});
|
||||
|
||||
test("isFreeModel: a model id listed in the free catalog for that provider is free", () => {
|
||||
const sample = FREE_MODEL_BUDGETS[0];
|
||||
assert.equal(isFreeModel(sample.provider, { id: sample.modelId }), true);
|
||||
});
|
||||
|
||||
test("selectModelsForImport: passthrough when importFreeOnly is false", () => {
|
||||
const models = [
|
||||
{ id: "a:free" },
|
||||
{ id: "b", pricing: { prompt: "0.01", completion: "0.02" } },
|
||||
];
|
||||
const result = selectModelsForImport("openrouter", models, false);
|
||||
assert.equal(result.models.length, 2);
|
||||
assert.equal(result.freeFilterEmpty, false);
|
||||
});
|
||||
|
||||
test("selectModelsForImport: keeps only free models when importFreeOnly is true", () => {
|
||||
const models = [
|
||||
{ id: "free-one:free" },
|
||||
{ id: "paid-one", pricing: { prompt: "0.01", completion: "0.02" } },
|
||||
{ id: "free-two", pricing: { prompt: "0", completion: "0" } },
|
||||
];
|
||||
const result = selectModelsForImport("openrouter", models, true);
|
||||
assert.deepEqual(
|
||||
result.models.map((m) => m.id),
|
||||
["free-one:free", "free-two"]
|
||||
);
|
||||
assert.equal(result.freeFilterEmpty, false);
|
||||
});
|
||||
|
||||
test("selectModelsForImport: flags freeFilterEmpty when models exist but none are free", () => {
|
||||
const models = [{ id: "paid", pricing: { prompt: "0.01", completion: "0.02" } }];
|
||||
const result = selectModelsForImport("openrouter", models, true);
|
||||
assert.equal(result.models.length, 0);
|
||||
assert.equal(result.freeFilterEmpty, true);
|
||||
});
|
||||
|
||||
test("selectModelsForImport: empty fetched list is not flagged as freeFilterEmpty", () => {
|
||||
const result = selectModelsForImport("openrouter", [], true);
|
||||
assert.equal(result.models.length, 0);
|
||||
assert.equal(result.freeFilterEmpty, false);
|
||||
});
|
||||
|
||||
test("sortModelsFreeFirst: free models come before paid ones", () => {
|
||||
const items = [
|
||||
{ id: "z-paid", isFree: false },
|
||||
{ id: "a-free", isFree: true },
|
||||
{ id: "m-paid", isFree: false },
|
||||
{ id: "b-free", isFree: true },
|
||||
];
|
||||
const sorted = sortModelsFreeFirst(items, { isFree: (m) => m.isFree, key: (m) => m.id });
|
||||
assert.deepEqual(
|
||||
sorted.map((m) => m.id),
|
||||
["a-free", "b-free", "m-paid", "z-paid"]
|
||||
);
|
||||
});
|
||||
|
||||
test("sortModelsFreeFirst: deterministic (alphabetical) within each group, regardless of input order", () => {
|
||||
const a = sortModelsFreeFirst(
|
||||
[
|
||||
{ id: "c", isFree: true },
|
||||
{ id: "a", isFree: true },
|
||||
{ id: "b", isFree: true },
|
||||
],
|
||||
{ isFree: (m) => m.isFree, key: (m) => m.id }
|
||||
);
|
||||
// Re-sorting a shuffled copy yields the same order — stable across refetch/re-render.
|
||||
const b = sortModelsFreeFirst(
|
||||
[
|
||||
{ id: "b", isFree: true },
|
||||
{ id: "c", isFree: true },
|
||||
{ id: "a", isFree: true },
|
||||
],
|
||||
{ isFree: (m) => m.isFree, key: (m) => m.id }
|
||||
);
|
||||
assert.deepEqual(a.map((m) => m.id), ["a", "b", "c"]);
|
||||
assert.deepEqual(b.map((m) => m.id), ["a", "b", "c"]);
|
||||
});
|
||||
|
||||
test("sortModelsFreeFirst: does not mutate the input array", () => {
|
||||
const items = [
|
||||
{ id: "z", isFree: false },
|
||||
{ id: "a", isFree: true },
|
||||
];
|
||||
const before = items.map((m) => m.id);
|
||||
sortModelsFreeFirst(items, { isFree: (m) => m.isFree, key: (m) => m.id });
|
||||
assert.deepEqual(items.map((m) => m.id), before);
|
||||
});
|
||||
97
tests/unit/free-web-search.test.ts
Normal file
97
tests/unit/free-web-search.test.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { parseDuckDuckGoLite } from "../../open-sse/services/freeWebSearch.ts";
|
||||
|
||||
// Real DuckDuckGo lite shape (captured from lite.duckduckgo.com/lite/):
|
||||
// double-quoted href BEFORE single-quoted class='result-link', direct URLs,
|
||||
// <b> highlight tags inside titles/snippets, link[i] aligns with snippet[i].
|
||||
const REAL_LITE_HTML = `
|
||||
<table>
|
||||
<tr>
|
||||
<td valign="top">1. </td>
|
||||
<td>
|
||||
<a rel="nofollow" href="https://platform.claude.com/docs/en/api/overview" class='result-link'>API overview - <b>Claude</b> API Docs - Anthropic</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td class='result-snippet'><b>Claude</b> <b>API</b> Documentation. New to Claude?</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td valign="top">2. </td>
|
||||
<td>
|
||||
<a rel="nofollow" href="https://www.anthropic.com/api" class='result-link'>Claude Platform | Claude - Anthropic</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td class='result-snippet'>Build with the most capable models.</td>
|
||||
</tr>
|
||||
</table>`;
|
||||
|
||||
test("parseDuckDuckGoLite extracts aligned title/url/snippet from real lite HTML", () => {
|
||||
const results = parseDuckDuckGoLite(REAL_LITE_HTML);
|
||||
assert.equal(results.length, 2);
|
||||
|
||||
assert.equal(results[0].url, "https://platform.claude.com/docs/en/api/overview");
|
||||
assert.equal(results[0].title, "API overview - Claude API Docs - Anthropic");
|
||||
assert.ok(results[0].snippet.includes("Claude API Documentation"));
|
||||
|
||||
assert.equal(results[1].url, "https://www.anthropic.com/api");
|
||||
assert.equal(results[1].title, "Claude Platform | Claude - Anthropic");
|
||||
assert.ok(results[1].snippet.includes("most capable models"));
|
||||
});
|
||||
|
||||
test("parseDuckDuckGoLite strips <b> highlight tags and collapses whitespace", () => {
|
||||
const results = parseDuckDuckGoLite(REAL_LITE_HTML);
|
||||
assert.doesNotMatch(results[0].title, /<b>|<\/b>/);
|
||||
assert.doesNotMatch(results[0].snippet, /<b>|<\/b>/);
|
||||
});
|
||||
|
||||
test("parseDuckDuckGoLite decodes a uddg redirect href (defensive)", () => {
|
||||
const html = `<a rel="nofollow" href="//duckduckgo.com/l/?uddg=https%3A%2F%2Fexample.com%2Fa%3Fb%3D1&rut=x" class='result-link'>Example</a>
|
||||
<td class='result-snippet'>snippet</td>`;
|
||||
const results = parseDuckDuckGoLite(html);
|
||||
assert.equal(results[0].url, "https://example.com/a?b=1");
|
||||
});
|
||||
|
||||
test("parseDuckDuckGoLite upgrades protocol-relative hrefs to https", () => {
|
||||
const html = `<a href="//example.org/path" class='result-link'>Proto rel</a>
|
||||
<td class='result-snippet'>s</td>`;
|
||||
const results = parseDuckDuckGoLite(html);
|
||||
assert.equal(results[0].url, "https://example.org/path");
|
||||
});
|
||||
|
||||
test("parseDuckDuckGoLite returns [] when there are no result links", () => {
|
||||
assert.deepEqual(parseDuckDuckGoLite("<html><body>no results</body></html>"), []);
|
||||
assert.deepEqual(parseDuckDuckGoLite(""), []);
|
||||
});
|
||||
|
||||
test("parseDuckDuckGoLite drops non-http(s) result URLs (javascript:/data:/file:)", () => {
|
||||
const html = `<a href="javascript:alert(1)" class='result-link'>XSS</a>
|
||||
<td class='result-snippet'>bad</td>
|
||||
<a href="https://ok.example.com" class='result-link'>Safe</a>
|
||||
<td class='result-snippet'>good</td>`;
|
||||
const results = parseDuckDuckGoLite(html);
|
||||
assert.equal(results.length, 1, "the javascript: link must be discarded");
|
||||
assert.equal(results[0].url, "https://ok.example.com");
|
||||
assert.doesNotMatch(JSON.stringify(results), /javascript:/);
|
||||
});
|
||||
|
||||
test("parseDuckDuckGoLite is bounded on adversarial HTML (no catastrophic backtracking)", () => {
|
||||
// Many unclosed result-link anchors + a huge filler tail must return promptly.
|
||||
const pathological =
|
||||
`<a href="https://x.com" class='result-link'>`.repeat(2000) + "x".repeat(2_000_000);
|
||||
const start = Date.now();
|
||||
const results = parseDuckDuckGoLite(pathological);
|
||||
assert.ok(Date.now() - start < 1000, "must not hang on adversarial HTML");
|
||||
assert.ok(Array.isArray(results));
|
||||
});
|
||||
|
||||
test("parseDuckDuckGoLite tolerates a missing snippet (empty string, not crash)", () => {
|
||||
const html = `<a href="https://x.com" class='result-link'>Only a link</a>`;
|
||||
const results = parseDuckDuckGoLite(html);
|
||||
assert.equal(results.length, 1);
|
||||
assert.equal(results[0].snippet, "");
|
||||
});
|
||||
109
tests/unit/gemini-midstream-error-4177.test.ts
Normal file
109
tests/unit/gemini-midstream-error-4177.test.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Regression test for #4177 — Gemini mid-stream error silently swallowed.
|
||||
*
|
||||
* When the upstream Gemini SSE stream emits a JSON error object
|
||||
* (e.g. `{"error":{"code":503,"message":"...","status":"UNAVAILABLE"}}`) instead of a
|
||||
* `candidates` payload — typically after some partial reasoning content — the
|
||||
* translator must surface it as `state.upstreamError` so the streaming pipeline can
|
||||
* error the response out (and trigger combo fallback) rather than ending the stream
|
||||
* with a default `finish_reason: "stop"` and a misleading HTTP 200.
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const { geminiToOpenAIResponse } =
|
||||
await import("../../open-sse/translator/response/gemini-to-openai.ts");
|
||||
|
||||
type StreamState = {
|
||||
toolCalls: Map<string, unknown>;
|
||||
messageId?: string;
|
||||
model?: string;
|
||||
upstreamError?: { status: number; type: string; code: string; message: string };
|
||||
};
|
||||
|
||||
function createStreamingState(): StreamState {
|
||||
return {
|
||||
toolCalls: new Map(),
|
||||
};
|
||||
}
|
||||
|
||||
test("#4177 Gemini mid-stream 503 UNAVAILABLE is surfaced as upstreamError, not dropped", () => {
|
||||
const state = createStreamingState();
|
||||
const result = geminiToOpenAIResponse(
|
||||
{
|
||||
error: {
|
||||
code: 503,
|
||||
message:
|
||||
"This model is currently experiencing high demand. Spikes in demand are usually temporary. Please try again later.",
|
||||
status: "UNAVAILABLE",
|
||||
},
|
||||
},
|
||||
state
|
||||
);
|
||||
|
||||
// The error chunk produces no delta output...
|
||||
assert.equal(result, null);
|
||||
// ...but it MUST be recorded so stream.ts can error the stream out.
|
||||
assert.ok(state.upstreamError, "expected state.upstreamError to be set for a 503 error chunk");
|
||||
assert.equal(state.upstreamError.status, 503);
|
||||
assert.equal(state.upstreamError.type, "server_error");
|
||||
assert.equal(state.upstreamError.code, "UNAVAILABLE");
|
||||
assert.match(state.upstreamError.message, /high demand/);
|
||||
});
|
||||
|
||||
test("#4177 Gemini RESOURCE_EXHAUSTED maps to a 429 rate-limit upstreamError", () => {
|
||||
const state = createStreamingState();
|
||||
const result = geminiToOpenAIResponse(
|
||||
{
|
||||
error: {
|
||||
code: 429,
|
||||
message: "Resource has been exhausted (e.g. check quota).",
|
||||
status: "RESOURCE_EXHAUSTED",
|
||||
},
|
||||
},
|
||||
state
|
||||
);
|
||||
|
||||
assert.equal(result, null);
|
||||
assert.ok(state.upstreamError);
|
||||
assert.equal(state.upstreamError.status, 429);
|
||||
assert.equal(state.upstreamError.type, "rate_limit_error");
|
||||
assert.equal(state.upstreamError.code, "RESOURCE_EXHAUSTED");
|
||||
});
|
||||
|
||||
test("#4177 Gemini error wrapped in a `response` envelope (Antigravity/Cloud Code) is detected", () => {
|
||||
const state = createStreamingState();
|
||||
const result = geminiToOpenAIResponse(
|
||||
{
|
||||
response: {
|
||||
error: { code: 503, message: "overloaded", status: "UNAVAILABLE" },
|
||||
},
|
||||
},
|
||||
state
|
||||
);
|
||||
|
||||
assert.equal(result, null);
|
||||
assert.ok(state.upstreamError);
|
||||
assert.equal(state.upstreamError.status, 503);
|
||||
});
|
||||
|
||||
test("#4177 a valid candidate chunk does NOT set upstreamError (no false positive)", () => {
|
||||
const state = createStreamingState();
|
||||
const result = geminiToOpenAIResponse(
|
||||
{
|
||||
responseId: "resp-ok",
|
||||
modelVersion: "gemini-2.5-flash",
|
||||
candidates: [
|
||||
{
|
||||
content: { role: "model", parts: [{ text: "hello" }] },
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
usageMetadata: { promptTokenCount: 1, totalTokenCount: 2 },
|
||||
},
|
||||
state
|
||||
);
|
||||
|
||||
assert.ok(result, "expected normal chunk to translate to OpenAI deltas");
|
||||
assert.equal(state.upstreamError, undefined);
|
||||
});
|
||||
@@ -10,7 +10,8 @@ describe("Gitlawb Opengateway providers", () => {
|
||||
assert.strictEqual(provider.id, "gitlawb");
|
||||
assert.strictEqual(provider.alias, "glb");
|
||||
assert.ok(provider.name.includes("Gitlawb"));
|
||||
assert.strictEqual(provider.hasFree, true);
|
||||
// Free MiMo (xiaomi/mimo-v2.5) revoked 2026-05; Opengateway is now pay-as-you-go (re-verified 2026-06-18).
|
||||
assert.strictEqual(provider.hasFree, false);
|
||||
});
|
||||
|
||||
it("should have registry entry with correct baseUrl", async () => {
|
||||
@@ -52,7 +53,8 @@ describe("Gitlawb Opengateway providers", () => {
|
||||
assert.ok(provider, "gitlawb-gmi should exist in APIKEY_PROVIDERS");
|
||||
assert.strictEqual(provider.id, "gitlawb-gmi");
|
||||
assert.strictEqual(provider.alias, "glb-gmi");
|
||||
assert.strictEqual(provider.hasFree, true);
|
||||
// Free Nemotron promo ended 2026-06; GMI Cloud route is pay-as-you-go (re-verified 2026-06-18).
|
||||
assert.strictEqual(provider.hasFree, false);
|
||||
});
|
||||
|
||||
it("should have registry entry with gmi-cloud baseUrl", async () => {
|
||||
|
||||
@@ -640,3 +640,44 @@ test("GlmExecutor Anthropic fallback keeps tool names unprefixed", async () => {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
// Regression for #4255 — GLM-5.2+ thinking models share a single max_tokens
|
||||
// budget for reasoning + response. When the client omits max_tokens, the
|
||||
// executor must default to the model's full output capacity (131072) so deep
|
||||
// reasoning isn't truncated by the generic GLM default (16_384). Scoped to
|
||||
// GLM-5.2+ via transformForTransport — non-thinking GLM models are untouched.
|
||||
test("GlmExecutor defaults GLM-5.2+ max_tokens to 131072 when the client omits it", () => {
|
||||
const executor = new GlmExecutor("glm");
|
||||
const body = { messages: [{ role: "user", content: "hi" }] };
|
||||
|
||||
const transformed = executor.transformForTransport("glm-5.2", body, false, {
|
||||
apiKey: "glm-key",
|
||||
}, "openai") as any;
|
||||
|
||||
assert.equal((body as any).max_tokens, undefined, "caller body must not be mutated");
|
||||
assert.equal(transformed.max_tokens, 131072);
|
||||
});
|
||||
|
||||
test("GlmExecutor preserves a client-supplied max_tokens for GLM-5.2+ (no override)", () => {
|
||||
const executor = new GlmExecutor("glm");
|
||||
const body = { messages: [{ role: "user", content: "hi" }], max_tokens: 4096 };
|
||||
|
||||
const transformed = executor.transformForTransport("glm-5.2", body, false, {
|
||||
apiKey: "glm-key",
|
||||
}, "openai") as any;
|
||||
|
||||
assert.equal(transformed.max_tokens, 4096);
|
||||
});
|
||||
|
||||
test("GlmExecutor does NOT bump max_tokens for non-thinking GLM (glm-4.6)", () => {
|
||||
const executor = new GlmExecutor("glm");
|
||||
const body = { messages: [{ role: "user", content: "hi" }] };
|
||||
|
||||
const transformed = executor.transformForTransport("glm-4.6", body, false, {
|
||||
apiKey: "glm-key",
|
||||
}, "openai") as any;
|
||||
|
||||
// Stays at the generic GLM default (16_384) — never the 131072 thinking budget.
|
||||
assert.notEqual(transformed.max_tokens, 131072);
|
||||
assert.equal(transformed.max_tokens, 16_384);
|
||||
});
|
||||
|
||||
99
tests/unit/gpt5-sampling-guard.test.ts
Normal file
99
tests/unit/gpt5-sampling-guard.test.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* GPT-5 sampling guard — `stripGpt5SamplingWhenReasoning`.
|
||||
*
|
||||
* GPT-5 reasoning models reject non-default `temperature`/`top_p` with HTTP 400 when a
|
||||
* reasoning effort is active, but GPT-5.1+ accept them again under `reasoning_effort:"none"`
|
||||
* (the default). These tests pin the conditional strip on the `openai` Chat Completions
|
||||
* surface: drop sampling only when reasoning is active, never for `none` / non-gpt-5 / other
|
||||
* providers. Refs: litellm#27351, Azure Foundry reasoning matrix, openai-python#2072.
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { stripGpt5SamplingWhenReasoning } from "../../open-sse/services/gpt5SamplingGuard.ts";
|
||||
|
||||
test("strips temperature+top_p for openai gpt-5.x when reasoning_effort is active", () => {
|
||||
const body = {
|
||||
model: "gpt-5.4",
|
||||
temperature: 0.7,
|
||||
top_p: 0.9,
|
||||
reasoning_effort: "high",
|
||||
messages: [],
|
||||
};
|
||||
const result = stripGpt5SamplingWhenReasoning(body, "openai", "gpt-5.4");
|
||||
assert.equal(result.temperature, undefined);
|
||||
assert.equal(result.top_p, undefined);
|
||||
assert.equal(result.reasoning_effort, "high"); // effort itself is untouched
|
||||
});
|
||||
|
||||
test("keeps temperature when reasoning_effort=none (gpt-5.1+ non-reasoning mode)", () => {
|
||||
const body = { model: "gpt-5.4", temperature: 0.7, reasoning_effort: "none", messages: [] };
|
||||
const result = stripGpt5SamplingWhenReasoning(body, "openai", "gpt-5.4");
|
||||
assert.equal(result.temperature, 0.7);
|
||||
});
|
||||
|
||||
test("keeps sampling when there is no reasoning signal (default none for gpt-5.1+)", () => {
|
||||
const body = { model: "gpt-5.5", temperature: 0.5, top_p: 0.8, messages: [] };
|
||||
const result = stripGpt5SamplingWhenReasoning(body, "openai", "gpt-5.5");
|
||||
assert.equal(result.temperature, 0.5);
|
||||
assert.equal(result.top_p, 0.8);
|
||||
});
|
||||
|
||||
test("nested reasoning.effort active also triggers the strip", () => {
|
||||
const body = { model: "gpt-5.4", top_p: 0.8, reasoning: { effort: "medium" }, messages: [] };
|
||||
const result = stripGpt5SamplingWhenReasoning(body, "openai", "gpt-5.4");
|
||||
assert.equal(result.top_p, undefined);
|
||||
});
|
||||
|
||||
test("model suffix -high triggers strip; -none keeps sampling", () => {
|
||||
const high = stripGpt5SamplingWhenReasoning(
|
||||
{ model: "gpt-5.4-high", temperature: 0.3 },
|
||||
"openai",
|
||||
"gpt-5.4-high"
|
||||
);
|
||||
assert.equal(high.temperature, undefined);
|
||||
|
||||
const none = stripGpt5SamplingWhenReasoning(
|
||||
{ model: "gpt-5.4-none", temperature: 0.3 },
|
||||
"openai",
|
||||
"gpt-5.4-none"
|
||||
);
|
||||
assert.equal(none.temperature, 0.3);
|
||||
});
|
||||
|
||||
test("non-openai provider is untouched (codex is guarded by the executor allowlist)", () => {
|
||||
const body = { model: "gpt-5.4", temperature: 0.7, reasoning_effort: "high" };
|
||||
const result = stripGpt5SamplingWhenReasoning(body, "codex", "gpt-5.4");
|
||||
assert.equal(result.temperature, 0.7);
|
||||
});
|
||||
|
||||
test("non-gpt-5 openai model is untouched (e.g. gpt-4o)", () => {
|
||||
const body = { model: "gpt-4o", temperature: 0.7, reasoning_effort: "high" };
|
||||
const result = stripGpt5SamplingWhenReasoning(body, "openai", "gpt-4o");
|
||||
assert.equal(result.temperature, 0.7);
|
||||
});
|
||||
|
||||
test("returns the same reference when no sampling params are present", () => {
|
||||
const body = { model: "gpt-5.4", reasoning_effort: "high", messages: [] };
|
||||
const result = stripGpt5SamplingWhenReasoning(body, "openai", "gpt-5.4");
|
||||
assert.equal(result, body);
|
||||
});
|
||||
|
||||
test("non-string model is a no-op", () => {
|
||||
const body = { temperature: 0.7, reasoning_effort: "high" };
|
||||
const result = stripGpt5SamplingWhenReasoning(body, "openai", null);
|
||||
assert.equal(result.temperature, 0.7);
|
||||
});
|
||||
|
||||
test("logs the stripped params when a logger is provided", () => {
|
||||
const calls: Array<[string, string]> = [];
|
||||
const log = { warn: (tag: string, message: string) => calls.push([tag, message]) };
|
||||
stripGpt5SamplingWhenReasoning(
|
||||
{ model: "gpt-5.4", temperature: 0.7, reasoning_effort: "high" },
|
||||
"openai",
|
||||
"gpt-5.4",
|
||||
log
|
||||
);
|
||||
assert.equal(calls.length, 1);
|
||||
assert.equal(calls[0][0], "PARAMS");
|
||||
assert.match(calls[0][1], /temperature/);
|
||||
});
|
||||
@@ -39,6 +39,10 @@ test("InterceptedRequestSchema — rejects invalid source enum", () => {
|
||||
assert.ok(!InterceptedRequestSchema.safeParse({ ...validInterceptedRequest, source: "invalid-source" }).success);
|
||||
});
|
||||
|
||||
test("InterceptedRequestSchema — accepts the tproxy source (decrypt capture mode)", () => {
|
||||
assert.ok(InterceptedRequestSchema.safeParse({ ...validInterceptedRequest, source: "tproxy" }).success);
|
||||
});
|
||||
|
||||
test("InterceptedRequestSchema — rejects negative requestSize", () => {
|
||||
assert.ok(!InterceptedRequestSchema.safeParse({ ...validInterceptedRequest, requestSize: -1 }).success);
|
||||
});
|
||||
|
||||
66
tests/unit/kimi-k2.7-code-registration.test.ts
Normal file
66
tests/unit/kimi-k2.7-code-registration.test.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
// Kimi K2.7 Code (released 2026-06-12) is Moonshot's coding-focused successor to
|
||||
// K2.6: 1T MoE, 256K context, thinking-only (preserve_thinking forced), with a
|
||||
// fixed sampling regime (temperature=1.0 / top_p=0.95). It must be advertised on
|
||||
// both the OAuth coding endpoint (api.kimi.com/coding, Anthropic format — the
|
||||
// path validated live on the test VPS) and the OpenAI endpoint
|
||||
// (api.moonshot.ai/v1). Two ids: `kimi-k2.7-code` and `kimi-k2.7-code-highspeed`.
|
||||
const { getRegistryEntry, getUnsupportedParams } = await import(
|
||||
"../../open-sse/config/providerRegistry.ts"
|
||||
);
|
||||
const { getResolvedModelCapabilities, supportsReasoning } = await import(
|
||||
"../../src/lib/modelCapabilities.ts"
|
||||
);
|
||||
|
||||
const K27 = "kimi-k2.7-code";
|
||||
const K27_HS = "kimi-k2.7-code-highspeed";
|
||||
|
||||
function modelIds(provider: string): string[] {
|
||||
const entry = getRegistryEntry(provider);
|
||||
assert.ok(entry, `${provider} registry entry must exist`);
|
||||
return (entry.models ?? []).map((m) => m.id);
|
||||
}
|
||||
|
||||
test("kimi-coding (OAuth) advertises kimi-k2.7-code + highspeed", () => {
|
||||
const ids = modelIds("kimi-coding");
|
||||
assert.ok(ids.includes(K27), "kimi-coding must list kimi-k2.7-code");
|
||||
assert.ok(ids.includes(K27_HS), "kimi-coding must list kimi-k2.7-code-highspeed");
|
||||
assert.ok(ids.includes("kimi-k2.6"), "existing kimi-k2.6 stays listed");
|
||||
});
|
||||
|
||||
test("kimi-coding-apikey advertises kimi-k2.7-code (shares KIMI_CODING_SHARED)", () => {
|
||||
const ids = modelIds("kimi-coding-apikey");
|
||||
assert.ok(ids.includes(K27), "kimi-coding-apikey must list kimi-k2.7-code");
|
||||
assert.ok(ids.includes(K27_HS), "kimi-coding-apikey must list kimi-k2.7-code-highspeed");
|
||||
});
|
||||
|
||||
test("moonshot (OpenAI endpoint) advertises kimi-k2.7-code + highspeed", () => {
|
||||
const ids = modelIds("moonshot");
|
||||
assert.ok(ids.includes(K27), "moonshot must list kimi-k2.7-code");
|
||||
assert.ok(ids.includes(K27_HS), "moonshot must list kimi-k2.7-code-highspeed");
|
||||
assert.ok(ids.includes("kimi-k2.6"), "existing kimi-k2.6 stays listed");
|
||||
});
|
||||
|
||||
test("kimi (OpenAI endpoint) advertises kimi-k2.7-code + highspeed", () => {
|
||||
const ids = modelIds("kimi");
|
||||
assert.ok(ids.includes(K27), "kimi must list kimi-k2.7-code");
|
||||
assert.ok(ids.includes(K27_HS), "kimi must list kimi-k2.7-code-highspeed");
|
||||
});
|
||||
|
||||
test("kimi-k2.7-code reports native 262144 context and is reasoning-capable", () => {
|
||||
const caps = getResolvedModelCapabilities({ provider: "kimi-coding", model: K27 });
|
||||
assert.equal(caps.contextWindow, 262144, "context window must be the native 256K (262144)");
|
||||
// thinking-only model: the thinking budget pipeline must not strip its thinking
|
||||
// config (applyThinkingBudget early-exits via supportsReasoning(model)).
|
||||
assert.equal(supportsReasoning(K27), true, "kimi-k2.7-code must be reasoning-capable");
|
||||
});
|
||||
|
||||
test("kimi-k2.7-code strips client temperature/top_p (fixed sampling upstream)", () => {
|
||||
for (const provider of ["kimi-coding", "kimi-coding-apikey", "moonshot", "kimi"]) {
|
||||
const unsupported = getUnsupportedParams(provider, K27);
|
||||
assert.ok(unsupported.includes("temperature"), `${provider}: temperature must be stripped`);
|
||||
assert.ok(unsupported.includes("top_p"), `${provider}: top_p must be stripped`);
|
||||
}
|
||||
});
|
||||
101
tests/unit/log-redaction.test.ts
Normal file
101
tests/unit/log-redaction.test.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { redactSecrets, redactLogArgs } from "../../src/shared/utils/logRedaction.ts";
|
||||
|
||||
test("redactSecrets removes Authorization: Bearer tokens", () => {
|
||||
const out = redactSecrets("upstream failed; Authorization: Bearer sk-abc123DEF456ghi789");
|
||||
assert.match(out, /Authorization: Bearer \[REDACTED\]/);
|
||||
assert.doesNotMatch(out, /sk-abc123/);
|
||||
});
|
||||
|
||||
test("redactSecrets removes a bare bearer token", () => {
|
||||
const out = redactSecrets("header bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.payload.sig");
|
||||
assert.match(out, /Bearer \[REDACTED\]/i);
|
||||
});
|
||||
|
||||
test("redactSecrets removes x-api-key values", () => {
|
||||
const out = redactSecrets("sent x-api-key: 9f8e7d6c5b4a3210ffff");
|
||||
assert.match(out, /x-api-key: \[REDACTED\]/i);
|
||||
assert.doesNotMatch(out, /9f8e7d6c/);
|
||||
});
|
||||
|
||||
test("redactSecrets removes a Telegram bot token in a URL", () => {
|
||||
const out = redactSecrets("posting to https://api.telegram.org/bot123456789:AAExampleTokenValue_abcdEFGH/send");
|
||||
assert.match(out, /api\.telegram\.org\/bot\[REDACTED\]/);
|
||||
assert.doesNotMatch(out, /AAExampleTokenValue/);
|
||||
});
|
||||
|
||||
test("redactSecrets removes sk- style API keys anywhere", () => {
|
||||
const out = redactSecrets("key=sk-proj-ABCDEFGHIJ1234567890 done");
|
||||
assert.match(out, /sk-\[REDACTED\]/);
|
||||
assert.doesNotMatch(out, /ABCDEFGHIJ1234567890/);
|
||||
});
|
||||
|
||||
test("redactSecrets returns clean strings unchanged (same reference)", () => {
|
||||
const clean = "request completed in 42ms for model gpt-4o";
|
||||
assert.equal(redactSecrets(clean), clean);
|
||||
});
|
||||
|
||||
test("redactSecrets is bounded on adversarial input (no catastrophic backtracking)", () => {
|
||||
const huge = "Authorization: Bearer " + "a".repeat(2_000_000);
|
||||
const start = Date.now();
|
||||
const out = redactSecrets(huge);
|
||||
assert.ok(Date.now() - start < 1000, "must not hang");
|
||||
assert.match(out, /\[REDACTED\]/);
|
||||
});
|
||||
|
||||
test("redactLogArgs scrubs a string message argument", () => {
|
||||
const [msg] = redactLogArgs(["call failed: Authorization: Bearer sk-secret1234567890abcd"]);
|
||||
assert.doesNotMatch(String(msg), /sk-secret/);
|
||||
assert.match(String(msg), /\[REDACTED\]/);
|
||||
});
|
||||
|
||||
test("redactLogArgs scrubs nested object string values", () => {
|
||||
const [obj] = redactLogArgs([
|
||||
{ req: { headers: { authorization: "Bearer sk-deadbeefdeadbeef1234" } }, model: "gpt-4o" },
|
||||
]) as [{ req: { headers: { authorization: string } }; model: string }];
|
||||
assert.doesNotMatch(obj.req.headers.authorization, /sk-deadbeef/);
|
||||
assert.match(obj.req.headers.authorization, /\[REDACTED\]/);
|
||||
assert.equal(obj.model, "gpt-4o", "non-secret fields are preserved");
|
||||
});
|
||||
|
||||
test("redactLogArgs scrubs an Error's message and stack when a secret is present", () => {
|
||||
const err = new Error("connect failed with Authorization: Bearer sk-leakedKey1234567890");
|
||||
const [scrubbed] = redactLogArgs([err]) as [Error];
|
||||
assert.notEqual(scrubbed, err, "a secret-bearing error is replaced with a redacted clone");
|
||||
assert.ok(scrubbed instanceof Error, "the redacted view is still a real Error");
|
||||
assert.doesNotMatch(scrubbed.message, /sk-leakedKey/);
|
||||
assert.match(scrubbed.message, /\[REDACTED\]/);
|
||||
assert.doesNotMatch(String(scrubbed.stack), /sk-leakedKey/);
|
||||
});
|
||||
|
||||
test("redactLogArgs leaves a clean Error untouched (preserves pino's serializer)", () => {
|
||||
const err = new Error("plain timeout after 30s");
|
||||
const [same] = redactLogArgs([err]);
|
||||
assert.equal(same, err, "no secret → original Error instance is returned unchanged");
|
||||
});
|
||||
|
||||
test("redactLogArgs returns the original object when nothing was redacted (no allocation)", () => {
|
||||
const obj = { model: "gpt-4o", tokens: 42, nested: { a: "b" } };
|
||||
const [same] = redactLogArgs([obj]);
|
||||
assert.equal(same, obj, "clean object identity is preserved");
|
||||
});
|
||||
|
||||
test("redactLogArgs survives circular references", () => {
|
||||
const a: Record<string, unknown> = { name: "a" };
|
||||
a.self = a;
|
||||
assert.doesNotThrow(() => redactLogArgs([a]));
|
||||
});
|
||||
|
||||
test("redactLogArgs is bounded on huge/deep objects", () => {
|
||||
const deep: Record<string, unknown> = {};
|
||||
let cur = deep;
|
||||
for (let i = 0; i < 10_000; i++) {
|
||||
cur.next = { token: "Bearer sk-xxxxxxxxxxxxxxxx" };
|
||||
cur = cur.next as Record<string, unknown>;
|
||||
}
|
||||
const start = Date.now();
|
||||
assert.doesNotThrow(() => redactLogArgs([deep]));
|
||||
assert.ok(Date.now() - start < 1000, "must stay bounded on pathological structures");
|
||||
});
|
||||
47
tests/unit/logger-redaction-wiring.test.ts
Normal file
47
tests/unit/logger-redaction-wiring.test.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
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 file logging BEFORE importing the logger (buildLogger runs at import time).
|
||||
const dir = mkdtempSync(join(tmpdir(), "omniroute-logredact-"));
|
||||
const logFile = join(dir, "app.log");
|
||||
process.env.NODE_ENV = "production"; // JSON to file, no pino-pretty
|
||||
process.env.APP_LOG_TO_FILE = "true";
|
||||
process.env.APP_LOG_FILE_PATH = logFile;
|
||||
process.env.APP_LOG_LEVEL = "debug";
|
||||
|
||||
const { createLogger } = await import("../../src/shared/utils/logger.ts");
|
||||
|
||||
/** Poll the (worker-thread-written) log file until the predicate holds or timeout. */
|
||||
async function readLogWhen(
|
||||
predicate: (contents: string) => boolean,
|
||||
timeoutMs = 4000
|
||||
): Promise<string> {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
if (existsSync(logFile)) {
|
||||
const contents = readFileSync(logFile, "utf8");
|
||||
if (predicate(contents)) return contents;
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
}
|
||||
return existsSync(logFile) ? readFileSync(logFile, "utf8") : "";
|
||||
}
|
||||
|
||||
test("logger redacts a Bearer secret in a free-form message and an error stack (end-to-end)", async () => {
|
||||
const log = createLogger("redact-test");
|
||||
|
||||
log.info("upstream call Authorization: Bearer sk-superSecretKey1234567890done");
|
||||
const err = new Error("boom while sending Authorization: Bearer sk-anotherSecretABCDEFGH12");
|
||||
log.error({ err }, "request failed");
|
||||
|
||||
const contents = await readLogWhen(
|
||||
(c) => c.includes("[REDACTED]") && !c.includes("sk-superSecretKey") && !c.includes("sk-anotherSecret")
|
||||
);
|
||||
|
||||
assert.match(contents, /\[REDACTED\]/, "redaction marker must appear in the log output");
|
||||
assert.doesNotMatch(contents, /sk-superSecretKey/, "message secret must be redacted");
|
||||
assert.doesNotMatch(contents, /sk-anotherSecret/, "error-stack secret must be redacted");
|
||||
});
|
||||
242
tests/unit/media-cost-headers-handlers.test.ts
Normal file
242
tests/unit/media-cost-headers-handlers.test.ts
Normal file
@@ -0,0 +1,242 @@
|
||||
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";
|
||||
|
||||
// Isolated DATA_DIR before any module that may open the SQLite singleton.
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-media-cost-h-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "media-cost-h-test-api-key-secret";
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const { OMNIROUTE_RESPONSE_HEADERS } = await import("../../src/shared/constants/headers.ts");
|
||||
const { saveSyncedPricing } = await import("../../src/lib/pricingSync.ts");
|
||||
const rerankHandler = await import("../../open-sse/handlers/rerank.ts");
|
||||
const moderationHandler = await import("../../open-sse/handlers/moderations.ts");
|
||||
const speechRoute = await import("../../src/app/api/v1/audio/speech/route.ts");
|
||||
const transcriptionRoute = await import("../../src/app/api/v1/audio/transcriptions/route.ts");
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
function restoreGlobals() {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
|
||||
test.afterEach(() => {
|
||||
restoreGlobals();
|
||||
});
|
||||
|
||||
test.after(() => {
|
||||
restoreGlobals();
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
// Shared assertions: every successful media Response must carry the
|
||||
// X-OmniRoute-* cost telemetry headers (parity with chat/embeddings).
|
||||
// Cost may legitimately be 0 (free / unpriced modality) — formatOmniRouteCost
|
||||
// still emits a fixed-10-decimal string ("0.0000000000"), so the format check
|
||||
// holds regardless.
|
||||
function assertCostTelemetryHeaders(response: Response) {
|
||||
assert.equal(response.status, 200);
|
||||
|
||||
const cost = response.headers.get(OMNIROUTE_RESPONSE_HEADERS.responseCost);
|
||||
assert.ok(cost, "response cost header must be present");
|
||||
assert.match(
|
||||
cost as string,
|
||||
/^\d+\.\d{10}$/,
|
||||
`cost header must be a fixed-10-decimal number, got: ${cost}`
|
||||
);
|
||||
|
||||
const version = response.headers.get(OMNIROUTE_RESPONSE_HEADERS.version);
|
||||
assert.ok(version && version.trim().length > 0, "version header must be non-empty");
|
||||
|
||||
const provider = response.headers.get(OMNIROUTE_RESPONSE_HEADERS.provider);
|
||||
assert.ok(provider && provider.trim().length > 0, "provider header must be present");
|
||||
}
|
||||
|
||||
test("rerank handler success Response carries cost telemetry headers", async () => {
|
||||
globalThis.fetch = (async (url: unknown) => {
|
||||
const stringUrl = String(url);
|
||||
if (stringUrl === "https://api.cohere.com/v2/rerank") {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
id: "rerank-1",
|
||||
results: [
|
||||
{ index: 0, relevance_score: 0.9 },
|
||||
{ index: 1, relevance_score: 0.1 },
|
||||
],
|
||||
meta: { api_version: { version: "2" }, billed_units: { search_units: 1 } },
|
||||
}),
|
||||
{ status: 200, headers: { "content-type": "application/json" } }
|
||||
);
|
||||
}
|
||||
throw new Error(`Unexpected URL: ${stringUrl}`);
|
||||
}) as typeof fetch;
|
||||
|
||||
const response = (await rerankHandler.handleRerank({
|
||||
model: "cohere/rerank-v3.5",
|
||||
query: "telemetry rerank",
|
||||
documents: ["doc a", "doc b"],
|
||||
credentials: { apiKey: "test-key" },
|
||||
})) as Response;
|
||||
|
||||
assertCostTelemetryHeaders(response);
|
||||
assert.equal(
|
||||
response.headers.get(OMNIROUTE_RESPONSE_HEADERS.provider),
|
||||
"cohere",
|
||||
"provider header must reflect the resolved rerank provider"
|
||||
);
|
||||
const body = (await response.json()) as { results?: unknown[] };
|
||||
assert.ok(
|
||||
Array.isArray(body.results) && body.results.length === 2,
|
||||
"should return rerank results"
|
||||
);
|
||||
});
|
||||
|
||||
test("rerank NVIDIA-format success Response reflects synthesized search unit in cost header", async () => {
|
||||
// NVIDIA-format rerank: transformResponseFromProvider SYNTHESIZES
|
||||
// meta.billed_units.search_units = 1 onto `result`, while the raw upstream
|
||||
// `data` has NO `meta` at all. The handler must read search units from the
|
||||
// transformed `result` (not the raw `data`), otherwise NVIDIA rerank is
|
||||
// always priced at $0 even when pricing exists.
|
||||
saveSyncedPricing({
|
||||
nvidia: {
|
||||
// calculateModalCost("rerank","nvidia","nvidia/nv-rerankqa-mistral-4b-v3")
|
||||
// first looks up the scoped id, then retries with normalizeModelName
|
||||
// (strips the "nvidia/" prefix) → this key resolves on the second try.
|
||||
"nv-rerankqa-mistral-4b-v3": { input: 0, output: 0, search_unit_cost: 0.002 },
|
||||
},
|
||||
});
|
||||
|
||||
globalThis.fetch = (async (url: unknown) => {
|
||||
const stringUrl = String(url);
|
||||
if (stringUrl === "https://integrate.api.nvidia.com/v1/ranking") {
|
||||
// NVIDIA shape — note: NO `meta` block at all.
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
rankings: [
|
||||
{ index: 0, logit: 0.9, text: "doc a" },
|
||||
{ index: 1, logit: 0.4, text: "doc b" },
|
||||
],
|
||||
}),
|
||||
{ status: 200, headers: { "content-type": "application/json" } }
|
||||
);
|
||||
}
|
||||
throw new Error(`Unexpected URL: ${stringUrl}`);
|
||||
}) as typeof fetch;
|
||||
|
||||
const response = (await rerankHandler.handleRerank({
|
||||
model: "nvidia/nv-rerankqa-mistral-4b-v3",
|
||||
query: "telemetry rerank nvidia",
|
||||
documents: ["doc a", "doc b"],
|
||||
credentials: { apiKey: "test-key" },
|
||||
})) as Response;
|
||||
|
||||
assertCostTelemetryHeaders(response);
|
||||
assert.equal(
|
||||
response.headers.get(OMNIROUTE_RESPONSE_HEADERS.provider),
|
||||
"nvidia",
|
||||
"provider header must reflect the resolved rerank provider"
|
||||
);
|
||||
// 1 synthesized search unit × $0.002 = $0.002. With the OLD `data?.meta…`
|
||||
// read this would be "0.0000000000" (raw NVIDIA data carries no meta).
|
||||
assert.equal(
|
||||
response.headers.get(OMNIROUTE_RESPONSE_HEADERS.responseCost),
|
||||
"0.0020000000",
|
||||
"NVIDIA rerank cost must reflect the synthesized 1 search unit read from result"
|
||||
);
|
||||
});
|
||||
|
||||
test("moderation handler success Response carries cost telemetry headers (cost 0)", async () => {
|
||||
globalThis.fetch = (async (url: unknown) => {
|
||||
const stringUrl = String(url);
|
||||
if (stringUrl === "https://api.openai.com/v1/moderations") {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
id: "modr-1",
|
||||
model: "omni-moderation-latest",
|
||||
results: [{ flagged: false }],
|
||||
}),
|
||||
{ status: 200, headers: { "content-type": "application/json" } }
|
||||
);
|
||||
}
|
||||
throw new Error(`Unexpected URL: ${stringUrl}`);
|
||||
}) as typeof fetch;
|
||||
|
||||
const response = (await moderationHandler.handleModeration({
|
||||
body: { model: "omni-moderation-latest", input: "telemetry moderation" },
|
||||
credentials: { apiKey: "test-key" },
|
||||
})) as Response;
|
||||
|
||||
assertCostTelemetryHeaders(response);
|
||||
assert.equal(
|
||||
response.headers.get(OMNIROUTE_RESPONSE_HEADERS.responseCost),
|
||||
"0.0000000000",
|
||||
"moderation is free → cost must be exactly 0"
|
||||
);
|
||||
});
|
||||
|
||||
test("v1 audio speech success Response carries cost telemetry headers", async () => {
|
||||
globalThis.fetch = (async (url: unknown) => {
|
||||
const stringUrl = String(url);
|
||||
if (stringUrl === "http://localhost:8000/v1/audio/speech") {
|
||||
return new Response(new Uint8Array([1, 2, 3, 4]), {
|
||||
status: 200,
|
||||
headers: { "content-type": "audio/mpeg" },
|
||||
});
|
||||
}
|
||||
throw new Error(`Unexpected URL: ${stringUrl}`);
|
||||
}) as typeof fetch;
|
||||
|
||||
const response = await speechRoute.POST(
|
||||
new Request("http://localhost/api/v1/audio/speech", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
model: "qwen/qwen3-tts",
|
||||
input: "telemetry speech text",
|
||||
}),
|
||||
}),
|
||||
{}
|
||||
);
|
||||
|
||||
assertCostTelemetryHeaders(response);
|
||||
assert.equal(
|
||||
response.headers.get("Content-Type"),
|
||||
"audio/mpeg",
|
||||
"audio Content-Type must be preserved"
|
||||
);
|
||||
});
|
||||
|
||||
test("v1 audio transcription success Response carries cost telemetry headers (cost 0)", async () => {
|
||||
globalThis.fetch = (async (url: unknown) => {
|
||||
const stringUrl = String(url);
|
||||
if (stringUrl === "http://localhost:8000/v1/audio/transcriptions") {
|
||||
return new Response(JSON.stringify({ text: "transcribed telemetry" }), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
}
|
||||
throw new Error(`Unexpected URL: ${stringUrl}`);
|
||||
}) as typeof fetch;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.set("model", "qwen/qwen3-asr");
|
||||
formData.set("file", new Blob([new Uint8Array([1, 2, 3])], { type: "audio/wav" }), "clip.wav");
|
||||
|
||||
const response = await transcriptionRoute.POST(
|
||||
new Request("http://localhost/api/v1/audio/transcriptions", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
})
|
||||
);
|
||||
|
||||
assertCostTelemetryHeaders(response);
|
||||
assert.equal(
|
||||
response.headers.get(OMNIROUTE_RESPONSE_HEADERS.responseCost),
|
||||
"0.0000000000",
|
||||
"transcription duration unavailable → cost must be 0"
|
||||
);
|
||||
});
|
||||
170
tests/unit/media-cost-headers.test.ts
Normal file
170
tests/unit/media-cost-headers.test.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
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";
|
||||
|
||||
// Isolated DATA_DIR before any module that may open the SQLite singleton.
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-media-cost-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "media-cost-test-api-key-secret";
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const { OMNIROUTE_RESPONSE_HEADERS } = await import("../../src/shared/constants/headers.ts");
|
||||
const imageRoute = await import("../../src/app/api/v1/images/generations/route.ts");
|
||||
const videoRoute = await import("../../src/app/api/v1/videos/generations/route.ts");
|
||||
const musicRoute = await import("../../src/app/api/v1/music/generations/route.ts");
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
const originalSetTimeout = globalThis.setTimeout;
|
||||
|
||||
// Skip the ComfyUI polling delays — invoke the callback synchronously.
|
||||
function immediateTimeout(callback: (...a: unknown[]) => void, _ms?: number, ...args: unknown[]) {
|
||||
if (typeof callback === "function") callback(...args);
|
||||
return 0 as unknown as ReturnType<typeof setTimeout>;
|
||||
}
|
||||
|
||||
function restoreGlobals() {
|
||||
globalThis.fetch = originalFetch;
|
||||
globalThis.setTimeout = originalSetTimeout;
|
||||
}
|
||||
|
||||
test.afterEach(() => {
|
||||
restoreGlobals();
|
||||
});
|
||||
|
||||
test.after(() => {
|
||||
restoreGlobals();
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
// Shared assertions: every successful media Response must carry the
|
||||
// X-OmniRoute-* cost telemetry headers (parity with chat/embeddings).
|
||||
function assertCostTelemetryHeaders(response: Response) {
|
||||
assert.equal(response.status, 200);
|
||||
|
||||
const cost = response.headers.get(OMNIROUTE_RESPONSE_HEADERS.responseCost);
|
||||
assert.ok(cost, "response cost header must be present");
|
||||
assert.match(
|
||||
cost as string,
|
||||
/^\d+\.\d{10}$/,
|
||||
`cost header must be a fixed-10-decimal number, got: ${cost}`
|
||||
);
|
||||
|
||||
const version = response.headers.get(OMNIROUTE_RESPONSE_HEADERS.version);
|
||||
assert.ok(version && version.trim().length > 0, "version header must be non-empty");
|
||||
|
||||
const provider = response.headers.get(OMNIROUTE_RESPONSE_HEADERS.provider);
|
||||
assert.ok(provider && provider.trim().length > 0, "provider header must be present");
|
||||
}
|
||||
|
||||
test("v1 images generation success Response carries cost telemetry headers", async () => {
|
||||
globalThis.fetch = (async (url: unknown) => {
|
||||
const stringUrl = String(url);
|
||||
if (stringUrl === "http://localhost:7860/sdapi/v1/txt2img") {
|
||||
return new Response(JSON.stringify({ images: ["YmFzZTY0LWltYWdl"] }), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
}
|
||||
throw new Error(`Unexpected URL: ${stringUrl}`);
|
||||
}) as typeof fetch;
|
||||
|
||||
const response = await imageRoute.POST(
|
||||
new Request("http://localhost/api/v1/images/generations", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
model: "sdwebui/stable-diffusion-v1-5",
|
||||
prompt: "telemetry image",
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
assertCostTelemetryHeaders(response);
|
||||
const body = (await response.json()) as { data?: unknown[] };
|
||||
assert.ok(Array.isArray(body.data) && body.data.length >= 1, "should return image data");
|
||||
});
|
||||
|
||||
test("v1 videos generation success Response carries cost telemetry headers", async () => {
|
||||
globalThis.setTimeout = immediateTimeout as typeof setTimeout;
|
||||
globalThis.fetch = (async (url: unknown) => {
|
||||
const stringUrl = String(url);
|
||||
if (stringUrl === "http://localhost:8188/prompt") {
|
||||
return new Response(JSON.stringify({ prompt_id: "video-cost-1" }), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
}
|
||||
if (stringUrl === "http://localhost:8188/history/video-cost-1") {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
"video-cost-1": {
|
||||
outputs: { 7: { gifs: [{ filename: "clip.webp", subfolder: "out", type: "output" }] } },
|
||||
},
|
||||
}),
|
||||
{ status: 200, headers: { "content-type": "application/json" } }
|
||||
);
|
||||
}
|
||||
if (stringUrl.includes("/view?")) {
|
||||
return new Response(new Uint8Array([1, 2, 3, 4]), { status: 200 });
|
||||
}
|
||||
throw new Error(`Unexpected URL: ${stringUrl}`);
|
||||
}) as typeof fetch;
|
||||
|
||||
const response = await videoRoute.POST(
|
||||
new Request("http://localhost/api/v1/videos/generations", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
model: "comfyui/animatediff",
|
||||
prompt: "telemetry video",
|
||||
duration: 6,
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
assertCostTelemetryHeaders(response);
|
||||
});
|
||||
|
||||
test("v1 music generation success Response carries cost telemetry headers", async () => {
|
||||
globalThis.setTimeout = immediateTimeout as typeof setTimeout;
|
||||
globalThis.fetch = (async (url: unknown) => {
|
||||
const stringUrl = String(url);
|
||||
if (stringUrl === "http://localhost:8188/prompt") {
|
||||
return new Response(JSON.stringify({ prompt_id: "music-cost-1" }), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
}
|
||||
if (stringUrl === "http://localhost:8188/history/music-cost-1") {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
"music-cost-1": {
|
||||
outputs: { 7: { audio: [{ filename: "track.wav", subfolder: "out", type: "output" }] } },
|
||||
},
|
||||
}),
|
||||
{ status: 200, headers: { "content-type": "application/json" } }
|
||||
);
|
||||
}
|
||||
if (stringUrl.includes("/view?")) {
|
||||
return new Response(new Uint8Array([5, 6, 7]), { status: 200 });
|
||||
}
|
||||
throw new Error(`Unexpected URL: ${stringUrl}`);
|
||||
}) as typeof fetch;
|
||||
|
||||
const response = await musicRoute.POST(
|
||||
new Request("http://localhost/api/v1/music/generations", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
model: "comfyui/musicgen-medium",
|
||||
prompt: "telemetry music",
|
||||
duration: 18,
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
assertCostTelemetryHeaders(response);
|
||||
});
|
||||
165
tests/unit/memory-vectorstore-int8-quant.test.ts
Normal file
165
tests/unit/memory-vectorstore-int8-quant.test.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* tests/unit/memory-vectorstore-int8-quant.test.ts
|
||||
*
|
||||
* F4.4 / Q2 — opt-in sqlite-vec int8 storage quantization.
|
||||
* - When MEMORY_VEC_QUANTIZATION=int8, ensureReady stores an ":int8"-suffixed
|
||||
* signature and creates the vec table with an int8[N] column (vectors stored
|
||||
* via vec_quantize_int8 'unit').
|
||||
* - Recall: int8 nearest-neighbor matches the exact float32 nearest-neighbor on
|
||||
* a deterministic fixture (top-1 identical, top-3 overlap >= 2/3).
|
||||
* - Switching mode (none -> int8) is a signature change → resetForSignature
|
||||
* recreates the table and marks all memories needs_reindex.
|
||||
*/
|
||||
|
||||
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(), "omr-vecstore-int8-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
process.env.DISABLE_SQLITE_AUTO_BACKUP = "true";
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const vsModule = await import("../../src/lib/memory/vectorStore.ts");
|
||||
const { getVectorStore, _resetVectorStoreSingleton } = vsModule;
|
||||
|
||||
import type { EmbeddingResolution } from "../../src/lib/memory/embedding/types.ts";
|
||||
|
||||
const DIM = 8;
|
||||
|
||||
function makeResolution(): EmbeddingResolution {
|
||||
return {
|
||||
source: "remote",
|
||||
model: "test/dim8",
|
||||
dimensions: DIM,
|
||||
signature: `test:dim8:${DIM}`,
|
||||
reason: "test",
|
||||
};
|
||||
}
|
||||
|
||||
function vec(values: number[]): Float32Array {
|
||||
return new Float32Array(values);
|
||||
}
|
||||
|
||||
// Deterministic, well-separated unit-ish fixture (8 dims, 6 memories).
|
||||
const FIXTURE: Array<{ id: string; v: number[] }> = [
|
||||
{ id: "m0", v: [1, 0, 0, 0, 0, 0, 0, 0] },
|
||||
{ id: "m1", v: [0, 1, 0, 0, 0, 0, 0, 0] },
|
||||
{ id: "m2", v: [0, 0, 1, 0, 0, 0, 0, 0] },
|
||||
{ id: "m3", v: [0.6, 0.8, 0, 0, 0, 0, 0, 0] },
|
||||
{ id: "m4", v: [0, 0, 0, 1, 0, 0, 0, 0] },
|
||||
{ id: "m5", v: [0, 0, 0, 0, 0.7071, 0.7071, 0, 0] },
|
||||
];
|
||||
|
||||
function l2(a: number[], b: number[]): number {
|
||||
let s = 0;
|
||||
for (let i = 0; i < a.length; i++) s += (a[i] - b[i]) ** 2;
|
||||
return Math.sqrt(s);
|
||||
}
|
||||
|
||||
function exactNearestIds(query: number[], k: number): string[] {
|
||||
return [...FIXTURE]
|
||||
.map((f) => ({ id: f.id, d: l2(f.v, query) }))
|
||||
.sort((a, b) => a.d - b.d)
|
||||
.slice(0, k)
|
||||
.map((x) => x.id);
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
_resetVectorStoreSingleton();
|
||||
core.resetDbInstance();
|
||||
if (fs.existsSync(TEST_DATA_DIR)) fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
test.afterEach(() => {
|
||||
delete process.env.MEMORY_VEC_QUANTIZATION;
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test.after(() => {
|
||||
core.resetDbInstance();
|
||||
if (fs.existsSync(TEST_DATA_DIR)) fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function getStoreOrSkip(t: { skip: (msg: string) => void }): ReturnType<typeof getVectorStore> {
|
||||
_resetVectorStoreSingleton();
|
||||
const store = getVectorStore();
|
||||
if (store === null) {
|
||||
t.skip("sqlite-vec not available in this environment — skipping");
|
||||
return null;
|
||||
}
|
||||
return store;
|
||||
}
|
||||
|
||||
function insertMemory(db: ReturnType<typeof core.getDbInstance>, id: string) {
|
||||
db.prepare(
|
||||
`INSERT INTO memories (id, api_key_id, type, key, content, created_at)
|
||||
VALUES (?, 'key1', 'factual', ?, ?, datetime('now'))`,
|
||||
).run(id, `key-${id}`, `content-${id}`);
|
||||
}
|
||||
|
||||
async function seedFixture(store: NonNullable<ReturnType<typeof getVectorStore>>) {
|
||||
const db = core.getDbInstance();
|
||||
await store.ensureReady(makeResolution());
|
||||
for (const f of FIXTURE) {
|
||||
insertMemory(db, f.id);
|
||||
await store.upsertVector(f.id, vec(f.v));
|
||||
}
|
||||
}
|
||||
|
||||
test("int8 mode: ensureReady stores an ':int8' signature", async (t) => {
|
||||
process.env.MEMORY_VEC_QUANTIZATION = "int8";
|
||||
const store = getStoreOrSkip(t);
|
||||
if (!store) return;
|
||||
|
||||
await store.ensureReady(makeResolution());
|
||||
const stats = await store.stats();
|
||||
assert.ok(
|
||||
stats.signature?.endsWith(":int8"),
|
||||
`signature must carry the int8 marker, got ${stats.signature}`,
|
||||
);
|
||||
});
|
||||
|
||||
test("int8 recall: nearest-neighbor matches exact float32 NN on the fixture", async (t) => {
|
||||
process.env.MEMORY_VEC_QUANTIZATION = "int8";
|
||||
const store = getStoreOrSkip(t);
|
||||
if (!store) return;
|
||||
|
||||
await seedFixture(store);
|
||||
|
||||
// Query close to m2 ([0,0,1,...]).
|
||||
const query = [0.05, 0.05, 0.95, 0, 0, 0, 0, 0];
|
||||
const hits = await store.searchVector(vec(query), 3);
|
||||
assert.ok(hits.length >= 1, "int8 search must return results");
|
||||
|
||||
const exact = exactNearestIds(query, 3);
|
||||
assert.equal(hits[0].memoryId, exact[0], `top-1 must match exact NN (${exact[0]})`);
|
||||
|
||||
const overlap = hits.slice(0, 3).filter((h) => exact.includes(h.memoryId)).length;
|
||||
assert.ok(overlap >= 2, `top-3 overlap must be >= 2/3 (got ${overlap}; int8=${hits
|
||||
.map((h) => h.memoryId)
|
||||
.join(",")} exact=${exact.join(",")})`);
|
||||
});
|
||||
|
||||
test("switching none → int8 is a signature change that triggers reindex", async (t) => {
|
||||
const store = getStoreOrSkip(t);
|
||||
if (!store) return;
|
||||
|
||||
// Start in float32 mode and seed.
|
||||
await seedFixture(store);
|
||||
const before = await store.stats();
|
||||
assert.ok(!before.signature?.endsWith(":int8"), "baseline should be float32 (no int8 marker)");
|
||||
|
||||
// Flip to int8 and re-run ensureReady → recreate + mark all needs_reindex.
|
||||
process.env.MEMORY_VEC_QUANTIZATION = "int8";
|
||||
const res = await store.ensureReady(makeResolution());
|
||||
assert.match(res.reason, /recreated/i, "ensureReady must recreate the table on mode switch");
|
||||
|
||||
const after = await store.stats();
|
||||
assert.ok(after.signature?.endsWith(":int8"), "signature must now carry the int8 marker");
|
||||
assert.equal(after.rowCount, 0, "table recreated empty (vectors await reindex)");
|
||||
assert.ok(after.needsReindex >= FIXTURE.length, "all memories must be marked needs_reindex");
|
||||
});
|
||||
89
tests/unit/mimo-thinking-normalize.test.ts
Normal file
89
tests/unit/mimo-thinking-normalize.test.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* Xiaomi MiMo thinking normalization — `normalizeMimoThinking`.
|
||||
*
|
||||
* MiMo controls reasoning ONLY via `thinking:{type:"enabled"|"disabled"}` and rejects
|
||||
* extra/unknown request params with a strict "400 Param Incorrect". These tests pin the
|
||||
* normalization that maps OmniRoute's internal reasoning signals onto MiMo's native shape:
|
||||
* reduce any thinking object to `{type}`, and drop `reasoning_effort` / `reasoning`.
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { normalizeMimoThinking } from "../../open-sse/services/mimoThinking.ts";
|
||||
|
||||
test("thinking:{type:'disabled'} is preserved (lets a client turn thinking OFF)", () => {
|
||||
const body = { model: "mimo-v2.5-pro", messages: [], thinking: { type: "disabled" } };
|
||||
const result = normalizeMimoThinking(body);
|
||||
assert.deepEqual(result.thinking, { type: "disabled" });
|
||||
});
|
||||
|
||||
test("Claude-shaped thinking:{type:'enabled', budget_tokens} → {type:'enabled'} (extras stripped)", () => {
|
||||
const body = {
|
||||
model: "mimo-v2.5-pro",
|
||||
messages: [],
|
||||
thinking: { type: "enabled", budget_tokens: 2048, keep: null },
|
||||
};
|
||||
const result = normalizeMimoThinking(body);
|
||||
// MiMo only accepts `type`; budget_tokens/keep would be rejected as extra params.
|
||||
assert.deepEqual(result.thinking, { type: "enabled" });
|
||||
});
|
||||
|
||||
test("thinking:{type:'adaptive'} collapses to MiMo's binary 'enabled'", () => {
|
||||
const body = { model: "mimo-v2.5", messages: [], thinking: { type: "adaptive" } };
|
||||
const result = normalizeMimoThinking(body);
|
||||
assert.deepEqual(result.thinking, { type: "enabled" });
|
||||
});
|
||||
|
||||
test("reasoning_effort is removed (MiMo does not understand it) and no thinking is synthesized", () => {
|
||||
const body = { model: "mimo-v2-flash", messages: [], reasoning_effort: "high" };
|
||||
const result = normalizeMimoThinking(body) as Record<string, unknown>;
|
||||
assert.equal(result.reasoning_effort, undefined, "reasoning_effort must be dropped");
|
||||
// Deliberately NOT synthesized — mimo-v2-omni is non-thinking; forcing it on could 400.
|
||||
assert.equal(result.thinking, undefined, "no thinking object is invented from a bare effort hint");
|
||||
});
|
||||
|
||||
test("nested `reasoning` object is removed", () => {
|
||||
const body = { model: "mimo-v2.5", messages: [], reasoning: { effort: "high", summary: "auto" } };
|
||||
const result = normalizeMimoThinking(body) as Record<string, unknown>;
|
||||
assert.equal(result.reasoning, undefined, "reasoning must be dropped");
|
||||
});
|
||||
|
||||
test("reasoning_effort alongside an explicit thinking:{type:'disabled'} → thinking wins, effort dropped", () => {
|
||||
const body = {
|
||||
model: "mimo-v2.5-pro",
|
||||
messages: [],
|
||||
reasoning_effort: "xhigh",
|
||||
thinking: { type: "disabled", budget_tokens: 1000 },
|
||||
};
|
||||
const result = normalizeMimoThinking(body) as Record<string, unknown>;
|
||||
assert.deepEqual(result.thinking, { type: "disabled" });
|
||||
assert.equal(result.reasoning_effort, undefined);
|
||||
});
|
||||
|
||||
test("body with neither thinking nor reasoning is returned UNTOUCHED (same reference)", () => {
|
||||
const body = { model: "mimo-v2.5-pro", messages: [{ role: "user", content: "hi" }] };
|
||||
const result = normalizeMimoThinking(body);
|
||||
assert.equal(result, body, "no-op must not allocate a new object");
|
||||
});
|
||||
|
||||
test("non-object body is returned unchanged", () => {
|
||||
const body = null as unknown as Record<string, unknown>;
|
||||
assert.equal(normalizeMimoThinking(body), body);
|
||||
});
|
||||
|
||||
test("unrelated fields are preserved", () => {
|
||||
const body = {
|
||||
model: "mimo-v2.5-pro",
|
||||
messages: [{ role: "user", content: "hi" }],
|
||||
temperature: 0.7,
|
||||
max_tokens: 1024,
|
||||
thinking: { type: "enabled", budget_tokens: 4096 },
|
||||
reasoning_effort: "high",
|
||||
};
|
||||
const result = normalizeMimoThinking(body) as Record<string, unknown>;
|
||||
assert.equal(result.model, "mimo-v2.5-pro");
|
||||
assert.equal(result.temperature, 0.7);
|
||||
assert.equal(result.max_tokens, 1024);
|
||||
assert.deepEqual(result.messages, [{ role: "user", content: "hi" }]);
|
||||
assert.deepEqual(result.thinking, { type: "enabled" });
|
||||
assert.equal(result.reasoning_effort, undefined);
|
||||
});
|
||||
93
tests/unit/minimax-media-servicekinds.test.ts
Normal file
93
tests/unit/minimax-media-servicekinds.test.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
// Regression guard: media providers surface in the dashboard via registry-derived
|
||||
// serviceKinds.
|
||||
//
|
||||
// Root cause this fixes: `/dashboard/media-providers/[kind]` listed a provider only
|
||||
// if it hand-declared `serviceKinds` in providers.ts. ~48 providers were wired into
|
||||
// the audio/video/music/image/embedding registries (backend works) but declared no
|
||||
// serviceKinds, so every media page was empty. The fix derives media membership from
|
||||
// the registries (single source of truth) and unions it with declared serviceKinds.
|
||||
//
|
||||
// MiniMax was the flagged case: its international endpoint serves TTS/video/music, the
|
||||
// China variant (minimax-cn) has no media registry entries.
|
||||
|
||||
const { getRegistryMediaKinds, resolveProviderServiceKinds, REGISTRY_MEDIA_KINDS } =
|
||||
await import("../../open-sse/config/mediaServiceKinds.ts");
|
||||
const { AI_PROVIDERS } = await import("../../src/shared/constants/providers.ts");
|
||||
|
||||
test("minimax (international) derives tts/video/music from the registries", () => {
|
||||
const kinds = getRegistryMediaKinds("minimax").sort();
|
||||
assert.deepEqual(kinds, ["music", "tts", "video"]);
|
||||
});
|
||||
|
||||
test("minimax-cn derives no media kinds (China endpoint has no media registry entries)", () => {
|
||||
assert.deepEqual(getRegistryMediaKinds("minimax-cn"), []);
|
||||
});
|
||||
|
||||
test("representative media providers derive the expected kinds", () => {
|
||||
// Each pair: provider id -> at least these kinds must be derived.
|
||||
const cases: Record<string, string[]> = {
|
||||
elevenlabs: ["tts"],
|
||||
deepgram: ["stt", "tts"],
|
||||
suno: ["music"],
|
||||
udio: ["music"],
|
||||
runwayml: ["video"],
|
||||
openai: ["embedding", "image", "stt", "tts"],
|
||||
cohere: ["embedding", "stt"],
|
||||
};
|
||||
for (const [id, expected] of Object.entries(cases)) {
|
||||
const derived = getRegistryMediaKinds(id);
|
||||
for (const kind of expected) {
|
||||
assert.ok(derived.includes(kind as never), `${id} should derive ${kind}; got ${derived.join(",")}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("resolveProviderServiceKinds unions declared kinds with derived media kinds", () => {
|
||||
// A provider with a declared non-media kind keeps it AND gains derived media kinds.
|
||||
const merged = resolveProviderServiceKinds("minimax", ["llm"]);
|
||||
for (const expected of ["llm", "tts", "video", "music"]) {
|
||||
assert.ok(merged.includes(expected), `expected ${expected} in ${merged.join(",")}`);
|
||||
}
|
||||
// De-dupes when declared overlaps derived.
|
||||
const veo = resolveProviderServiceKinds("veoaifree-web", ["video"]);
|
||||
assert.equal(veo.filter((k) => k === "video").length, 1);
|
||||
});
|
||||
|
||||
test("media listing filter surfaces minimax where the old declared-only filter missed it", () => {
|
||||
const cfg = AI_PROVIDERS as Record<string, { id: string; serviceKinds?: string[] }>;
|
||||
|
||||
// OLD behavior (the bug): filter by declared serviceKinds only.
|
||||
const oldListFor = (kind: string) =>
|
||||
Object.values(cfg)
|
||||
.filter((p) => (p.serviceKinds ?? []).includes(kind))
|
||||
.map((p) => p.id);
|
||||
|
||||
// NEW behavior (the fix): union declared with registry-derived kinds — mirrors page.tsx.
|
||||
const newListFor = (kind: string) =>
|
||||
Object.values(cfg)
|
||||
.filter((p) => resolveProviderServiceKinds(p.id, p.serviceKinds).includes(kind))
|
||||
.map((p) => p.id);
|
||||
|
||||
for (const kind of ["tts", "video", "music"]) {
|
||||
assert.ok(!oldListFor(kind).includes("minimax"), `precondition (bug): old filter missed minimax under ${kind}`);
|
||||
assert.ok(newListFor(kind).includes("minimax"), `fix: minimax now listed under ${kind}`);
|
||||
assert.ok(!newListFor(kind).includes("minimax-cn"), `minimax-cn must not appear under ${kind}`);
|
||||
}
|
||||
|
||||
// The fix is systemic, not minimax-only: many providers were invisible before.
|
||||
assert.ok(oldListFor("tts").length < newListFor("tts").length, "fix surfaces additional tts providers");
|
||||
});
|
||||
|
||||
test("derived kinds are always within the known media-kind set", () => {
|
||||
for (const id of Object.keys(AI_PROVIDERS)) {
|
||||
for (const kind of getRegistryMediaKinds(id)) {
|
||||
assert.ok(
|
||||
(REGISTRY_MEDIA_KINDS as readonly string[]).includes(kind),
|
||||
`${id} derived unknown kind ${kind}`
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
32
tests/unit/modalCost.test.ts
Normal file
32
tests/unit/modalCost.test.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
computeImageCost,
|
||||
computeAudioCost,
|
||||
computeRerankCost,
|
||||
computeVideoCost,
|
||||
calculateModalCost,
|
||||
} from "../../src/lib/usage/costCalculator.ts";
|
||||
|
||||
test("computeImageCost: per-image flat × n", () => {
|
||||
assert.equal(computeImageCost({ output_cost_per_image: 0.04 }, { n: 3 }), 0.12);
|
||||
assert.equal(computeImageCost({}, { n: 3 }), 0);
|
||||
assert.equal(computeImageCost({ output_cost_per_image: 0.04 }, { n: 0 }), 0);
|
||||
});
|
||||
test("computeAudioCost: per-second OR per-character, else 0", () => {
|
||||
assert.equal(computeAudioCost({ input_cost_per_second: 0.0001 }, { seconds: 30 }), 0.003);
|
||||
assert.equal(computeAudioCost({ input_cost_per_character: 0.000015 }, { characters: 1000 }), 0.015);
|
||||
assert.equal(computeAudioCost({ input_cost_per_second: 0.0001 }, {}), 0);
|
||||
});
|
||||
test("computeRerankCost: per search unit", () => {
|
||||
assert.equal(computeRerankCost({ search_unit_cost: 0.002 }, { searchUnits: 5 }), 0.01);
|
||||
assert.equal(computeRerankCost({}, { searchUnits: 5 }), 0);
|
||||
});
|
||||
test("computeVideoCost: per video-second", () => {
|
||||
assert.equal(computeVideoCost({ output_cost_per_video_per_second: 0.5 }, { seconds: 8 }), 4);
|
||||
assert.equal(computeVideoCost({}, { seconds: 8 }), 0);
|
||||
});
|
||||
test("calculateModalCost returns 0 for unknown pricing (fail-open)", async () => {
|
||||
const cost = await calculateModalCost("image", "no-such-provider", "no-such-model", { n: 2 });
|
||||
assert.equal(cost, 0);
|
||||
});
|
||||
130
tests/unit/model-capabilities-mimo-vision-override.test.ts
Normal file
130
tests/unit/model-capabilities-mimo-vision-override.test.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* Xiaomi MiMo vision capability — robust override against a wrong synced `attachment`.
|
||||
*
|
||||
* Per Xiaomi's official docs (mimo.mi.com .../multimodal-understanding/image-understanding)
|
||||
* ONLY `mimo-v2.5` and `mimo-v2-omni` accept image input. The `*-pro` chat models
|
||||
* (`mimo-v2.5-pro`, `mimo-v2-pro`) and `mimo-v2-flash` are TEXT-ONLY.
|
||||
*
|
||||
* models.dev mislabels `mimo-v2.5-pro` as attachment-capable (hermes-agent#18884),
|
||||
* and `resolveVisionCapability` lets a synced `attachment:true` win first — which would
|
||||
* route an image request to a blind model (the #4071 failure mode). A hard override
|
||||
* keyed on the documented text-only ids must beat the synced verdict.
|
||||
*
|
||||
* The discriminator is `attachment`: a synced row sets `attachment` from the seeded
|
||||
* value, while the override forces `supportsVision:false` regardless. So a row seeded
|
||||
* with `attachment:true` whose `supportsVision` still resolves `false` proves the
|
||||
* override — not the synced path — produced the verdict.
|
||||
*/
|
||||
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-mimo-vision-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const modelsDevSync = await import("../../src/lib/modelsDevSync.ts");
|
||||
const modelCapabilities = await import("../../src/lib/modelCapabilities.ts");
|
||||
|
||||
function buildCapability(overrides = {}) {
|
||||
return {
|
||||
tool_call: null,
|
||||
reasoning: null,
|
||||
attachment: null,
|
||||
structured_output: null,
|
||||
temperature: null,
|
||||
modalities_input: "[]",
|
||||
modalities_output: "[]",
|
||||
knowledge_cutoff: null,
|
||||
release_date: null,
|
||||
last_updated: null,
|
||||
status: null,
|
||||
family: null,
|
||||
open_weights: null,
|
||||
limit_context: null,
|
||||
limit_input: null,
|
||||
limit_output: null,
|
||||
interleaved_field: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function resetStorage() {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
// Mirror the WRONG models.dev `xiaomi-mimo` keying: the text-only `*-pro` chat models
|
||||
// carry attachment:true (the upstream mislabel), while the genuinely multimodal
|
||||
// `mimo-v2.5` / `mimo-v2-omni` correctly carry attachment:true too.
|
||||
function seedMimoCapabilities() {
|
||||
modelsDevSync.saveModelsDevCapabilities({
|
||||
"xiaomi-mimo": {
|
||||
"mimo-v2.5-pro": buildCapability({
|
||||
attachment: true, // upstream mislabel — must be overridden to text-only
|
||||
modalities_input: JSON.stringify(["text", "image"]),
|
||||
modalities_output: JSON.stringify(["text"]),
|
||||
status: "stable",
|
||||
}),
|
||||
"mimo-v2-pro": buildCapability({
|
||||
attachment: true, // upstream mislabel — must be overridden to text-only
|
||||
modalities_input: JSON.stringify(["text", "image"]),
|
||||
modalities_output: JSON.stringify(["text"]),
|
||||
status: "stable",
|
||||
}),
|
||||
"mimo-v2.5": buildCapability({
|
||||
attachment: true, // genuinely multimodal — must stay vision-capable
|
||||
modalities_input: JSON.stringify(["text", "image", "audio", "video"]),
|
||||
modalities_output: JSON.stringify(["text"]),
|
||||
status: "stable",
|
||||
}),
|
||||
"mimo-v2-omni": buildCapability({
|
||||
attachment: true, // genuinely multimodal — must stay vision-capable
|
||||
modalities_input: JSON.stringify(["text", "image", "audio", "video"]),
|
||||
modalities_output: JSON.stringify(["text"]),
|
||||
status: "stable",
|
||||
}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
test.beforeEach(() => {
|
||||
resetStorage();
|
||||
});
|
||||
|
||||
test.after(() => {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("mimo-v2.5-pro stays text-only even when models.dev says attachment:true", () => {
|
||||
seedMimoCapabilities();
|
||||
const pro = modelCapabilities.getResolvedModelCapabilities("xiaomi-mimo/mimo-v2.5-pro");
|
||||
// attachment is the seeded synced value, but the override forces vision false.
|
||||
assert.equal(pro.attachment, true, "synced attachment row is present (proves override, not absence)");
|
||||
assert.equal(pro.supportsVision, false, "text-only override must beat the wrong synced attachment");
|
||||
});
|
||||
|
||||
test("mimo-v2-pro stays text-only even when models.dev says attachment:true", () => {
|
||||
seedMimoCapabilities();
|
||||
const pro = modelCapabilities.getResolvedModelCapabilities("xiaomi-mimo/mimo-v2-pro");
|
||||
assert.equal(pro.supportsVision, false, "text-only override must beat the wrong synced attachment");
|
||||
});
|
||||
|
||||
test("genuinely multimodal mimo models keep vision (override is precise, not broad)", () => {
|
||||
seedMimoCapabilities();
|
||||
const v25 = modelCapabilities.getResolvedModelCapabilities("xiaomi-mimo/mimo-v2.5");
|
||||
const omni = modelCapabilities.getResolvedModelCapabilities("xiaomi-mimo/mimo-v2-omni");
|
||||
assert.equal(v25.supportsVision, true, "mimo-v2.5 is multimodal — must NOT be caught by the override");
|
||||
assert.equal(omni.supportsVision, true, "mimo-v2-omni is multimodal — must NOT be caught by the override");
|
||||
});
|
||||
|
||||
test("the bare (unqualified) text-only id is also overridden", () => {
|
||||
seedMimoCapabilities();
|
||||
// No provider prefix — exercises the `^...$` branch of the override regex.
|
||||
const bare = modelCapabilities.getResolvedModelCapabilities("mimo-v2.5-pro");
|
||||
assert.equal(bare.supportsVision, false, "bare text-only id must also be overridden");
|
||||
});
|
||||
148
tests/unit/model-capabilities-mistral-vision-sync-4073.test.ts
Normal file
148
tests/unit/model-capabilities-mistral-vision-sync-4073.test.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* #4073 — models.dev synced metadata must resolve for Mistral `-latest` aliases.
|
||||
*
|
||||
* Root cause (confirmed against the live models.dev API): models.dev catalogs
|
||||
* Mistral Pixtral 12B under the SHORT id `pixtral-12b` (with `attachment: true`,
|
||||
* `modalities.input: ["text","image"]`), while requests use the Mistral API
|
||||
* alias `pixtral-12b-latest`. The synced lookup in `getSyncedCapabilityForResolved`
|
||||
* tried the exact id, the raw id and the static-spec canonical id — all of which
|
||||
* miss for `pixtral-12b-latest` — so vision fell through to the #4071 model-id
|
||||
* heuristic and `attachment` stayed `null` (the symptom reported in #4073).
|
||||
*
|
||||
* The discriminator between "resolved via synced metadata" and "guessed via the
|
||||
* #4071 heuristic" is `attachment`: the synced path sets `attachment` from
|
||||
* `synced.attachment`; the heuristic only flips `supportsVision` and leaves
|
||||
* `attachment` null. So these tests assert on `attachment` to prove the synced
|
||||
* path — not the heuristic — produced the verdict.
|
||||
*
|
||||
* Other Mistral vision models already worked because models.dev keeps their
|
||||
* `-latest` id verbatim (e.g. `pixtral-large-latest`, `mistral-medium-latest`);
|
||||
* `pixtral-12b` is the one short-formed alias, hence the keying fix.
|
||||
*/
|
||||
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-mistral-vision-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const modelsDevSync = await import("../../src/lib/modelsDevSync.ts");
|
||||
const modelCapabilities = await import("../../src/lib/modelCapabilities.ts");
|
||||
|
||||
function buildCapability(overrides = {}) {
|
||||
return {
|
||||
tool_call: null,
|
||||
reasoning: null,
|
||||
attachment: null,
|
||||
structured_output: null,
|
||||
temperature: null,
|
||||
modalities_input: "[]",
|
||||
modalities_output: "[]",
|
||||
knowledge_cutoff: null,
|
||||
release_date: null,
|
||||
last_updated: null,
|
||||
status: null,
|
||||
family: null,
|
||||
open_weights: null,
|
||||
limit_context: null,
|
||||
limit_input: null,
|
||||
limit_output: null,
|
||||
interleaved_field: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function resetStorage() {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
// Mirrors the real models.dev `mistral` provider keying observed on the live API:
|
||||
// short-form `pixtral-12b` (vision), verbatim `pixtral-large-latest` (vision),
|
||||
// short-form `ministral-8b` (text-only).
|
||||
function seedMistralCapabilities() {
|
||||
modelsDevSync.saveModelsDevCapabilities({
|
||||
mistral: {
|
||||
"pixtral-12b": buildCapability({
|
||||
attachment: true,
|
||||
modalities_input: JSON.stringify(["text", "image"]),
|
||||
modalities_output: JSON.stringify(["text"]),
|
||||
family: "pixtral",
|
||||
status: "stable",
|
||||
}),
|
||||
"pixtral-large-latest": buildCapability({
|
||||
attachment: true,
|
||||
modalities_input: JSON.stringify(["text", "image"]),
|
||||
modalities_output: JSON.stringify(["text"]),
|
||||
family: "pixtral",
|
||||
status: "stable",
|
||||
}),
|
||||
"ministral-8b": buildCapability({
|
||||
attachment: false,
|
||||
modalities_input: JSON.stringify(["text"]),
|
||||
modalities_output: JSON.stringify(["text"]),
|
||||
family: "ministral",
|
||||
status: "stable",
|
||||
}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
test.beforeEach(() => {
|
||||
resetStorage();
|
||||
});
|
||||
|
||||
test.after(() => {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("#4073 mistral/pixtral-12b-latest resolves vision via the synced `-latest` alias (not the heuristic)", () => {
|
||||
seedMistralCapabilities();
|
||||
|
||||
const latest = modelCapabilities.getResolvedModelCapabilities("mistral/pixtral-12b-latest");
|
||||
// attachment === true can ONLY come from the synced row keyed `pixtral-12b`.
|
||||
assert.equal(latest.attachment, true, "synced attachment must resolve via the stripped `-latest` alias");
|
||||
assert.equal(latest.supportsVision, true);
|
||||
});
|
||||
|
||||
test("#4073 exact-keyed `-latest` models still resolve directly (no regression)", () => {
|
||||
seedMistralCapabilities();
|
||||
|
||||
// pixtral-large-latest is stored verbatim — the direct lookup must keep working.
|
||||
const large = modelCapabilities.getResolvedModelCapabilities("mistral/pixtral-large-latest");
|
||||
assert.equal(large.attachment, true);
|
||||
assert.equal(large.supportsVision, true);
|
||||
|
||||
// And the bare short id resolves directly too.
|
||||
const bare = modelCapabilities.getResolvedModelCapabilities("mistral/pixtral-12b");
|
||||
assert.equal(bare.attachment, true);
|
||||
assert.equal(bare.supportsVision, true);
|
||||
});
|
||||
|
||||
test("#4073 the `-latest` strip carries the synced verdict for text-only models too", () => {
|
||||
seedMistralCapabilities();
|
||||
|
||||
// ministral-8b is text-only; the heuristic does not recognise it, so the only
|
||||
// way attachment is a concrete `false` (not null) is the synced row resolving
|
||||
// through the stripped alias. This proves the strip returns the row's real
|
||||
// verdict rather than fabricating a positive.
|
||||
const ministral = modelCapabilities.getResolvedModelCapabilities("mistral/ministral-8b-latest");
|
||||
assert.equal(ministral.attachment, false, "synced false must win, resolved via stripped alias");
|
||||
assert.equal(ministral.supportsVision, false);
|
||||
});
|
||||
|
||||
test("#4073 the `-latest` strip never fabricates a match for an unknown id", () => {
|
||||
seedMistralCapabilities();
|
||||
|
||||
// No synced row for `unknown-text-model` (stripped) nor its `-latest` form, and
|
||||
// the heuristic doesn't recognise it → attachment null, vision null. The strip
|
||||
// must not invent a capability out of nothing.
|
||||
const unknown = modelCapabilities.getResolvedModelCapabilities("mistral/unknown-text-model-latest");
|
||||
assert.equal(unknown.attachment, null);
|
||||
assert.equal(unknown.supportsVision, null);
|
||||
});
|
||||
128
tests/unit/models-catalog-auto-combos-4164.test.ts
Normal file
128
tests/unit/models-catalog-auto-combos-4164.test.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* #4164 — the built-in `auto/*` combos must be advertised in `/v1/models`.
|
||||
*
|
||||
* OmniRoute ships a zero-setup `auto/*` catalog (auto/best-coding, auto/pro-
|
||||
* reasoning, …) that the dashboard advertises and that resolve on demand via
|
||||
* createBuiltinAutoCombo. But the `/v1/models` listing only emitted persisted DB
|
||||
* combos + provider models, so clients that build their model picker from
|
||||
* `/v1/models` (e.g. Hermes Agent) never saw any `auto/*` option.
|
||||
*
|
||||
* The catalog now emits every AUTO_TEMPLATE_VARIANTS id at the top of the list.
|
||||
*/
|
||||
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-catalog-auto-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "catalog-auto-test-secret";
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const v1ModelsCatalog = await import("../../src/app/api/v1/models/catalog.ts");
|
||||
const builtinCatalog = await import("../../open-sse/services/autoCombo/builtinCatalog.ts");
|
||||
|
||||
function resetStorage() {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
test.beforeEach(() => {
|
||||
resetStorage();
|
||||
});
|
||||
|
||||
test.after(() => {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("#4164 /v1/models advertises every built-in auto/* combo", async () => {
|
||||
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; owned_by?: string }> };
|
||||
|
||||
const ids = new Set(body.data.map((m) => m.id));
|
||||
const expected = Object.keys(builtinCatalog.AUTO_TEMPLATE_VARIANTS);
|
||||
assert.ok(expected.length > 0, "sanity: there are built-in auto/* variants");
|
||||
|
||||
for (const autoId of expected) {
|
||||
assert.ok(ids.has(autoId), `expected /v1/models to advertise ${autoId}`);
|
||||
}
|
||||
|
||||
// Spot-check a couple of well-known ones and their owner tag.
|
||||
const bestCoding = body.data.find((m) => m.id === "auto/best-coding");
|
||||
assert.ok(bestCoding, "auto/best-coding should be listed");
|
||||
assert.equal(bestCoding?.owned_by, "combo");
|
||||
});
|
||||
|
||||
test("#4164 auto/* combos appear at the top of the list", async () => {
|
||||
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
|
||||
new Request("http://localhost/api/v1/models")
|
||||
);
|
||||
const body = (await response.json()) as { data: Array<{ id: string }> };
|
||||
const expected = Object.keys(builtinCatalog.AUTO_TEMPLATE_VARIANTS);
|
||||
|
||||
// The first N entries (N = number of auto/* variants) should all be auto/*.
|
||||
const head = body.data.slice(0, expected.length).map((m) => m.id);
|
||||
for (const id of head) {
|
||||
assert.match(id, /^auto\//, `top-of-list entry ${id} should be an auto/* combo`);
|
||||
}
|
||||
});
|
||||
|
||||
test("#4164 no duplicate auto/* ids even if a persisted combo shadows one", async () => {
|
||||
// Defensive: even if a DB combo were named like an auto/* id, the listing must
|
||||
// not emit the id twice.
|
||||
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
|
||||
new Request("http://localhost/api/v1/models")
|
||||
);
|
||||
const body = (await response.json()) as { data: Array<{ id: string }> };
|
||||
const autoIds = body.data.map((m) => m.id).filter((id) => id.startsWith("auto/"));
|
||||
assert.equal(autoIds.length, new Set(autoIds).size, "auto/* ids must be unique");
|
||||
});
|
||||
|
||||
test("#4189 every auto/* entry exposes token limits + baseline capabilities", async () => {
|
||||
// #4164 emitted a bare auto/* entry (no token metadata). #4189 enriches each with
|
||||
// the combo's advertised context/output limits + baseline capabilities so
|
||||
// OpenAI-compatible clients that build their picker from /v1/models get a context
|
||||
// window before the first request. Without the fix these fields are absent.
|
||||
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;
|
||||
context_length?: number;
|
||||
max_input_tokens?: number;
|
||||
max_output_tokens?: number;
|
||||
capabilities?: Record<string, boolean>;
|
||||
}>;
|
||||
};
|
||||
|
||||
const autoEntries = body.data.filter((m) => m.id.startsWith("auto/"));
|
||||
assert.ok(autoEntries.length > 0, "sanity: at least one auto/* entry is listed");
|
||||
|
||||
for (const entry of autoEntries) {
|
||||
assert.equal(
|
||||
typeof entry.context_length,
|
||||
"number",
|
||||
`${entry.id} must expose a numeric context_length`
|
||||
);
|
||||
assert.ok((entry.context_length ?? 0) > 0, `${entry.id} context_length must be positive`);
|
||||
assert.equal(typeof entry.max_input_tokens, "number", `${entry.id} must expose max_input_tokens`);
|
||||
assert.equal(
|
||||
typeof entry.max_output_tokens,
|
||||
"number",
|
||||
`${entry.id} must expose max_output_tokens`
|
||||
);
|
||||
assert.ok((entry.max_output_tokens ?? 0) > 0, `${entry.id} max_output_tokens must be positive`);
|
||||
assert.ok(
|
||||
entry.capabilities && typeof entry.capabilities === "object",
|
||||
`${entry.id} must expose a capabilities map`
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -1357,7 +1357,12 @@ test("v1 models catalog includes context_length for individual chat models", asy
|
||||
new Request("http://localhost/api/v1/models")
|
||||
);
|
||||
const body = (await response.json()) as any;
|
||||
const chatModels = body.data.filter((item) => !item.type || item.type === "chat");
|
||||
// Individual chat models only — combos/routers (owned_by "combo", incl. the
|
||||
// built-in auto/* entries from #4164) resolve dynamically and have no fixed
|
||||
// context_length, so they are not "individual chat models" for this check.
|
||||
const chatModels = body.data.filter(
|
||||
(item) => (!item.type || item.type === "chat") && item.owned_by !== "combo"
|
||||
);
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.ok(chatModels.length > 0, "should have at least one chat model");
|
||||
|
||||
134
tests/unit/no-thinking-alias.test.ts
Normal file
134
tests/unit/no-thinking-alias.test.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
NO_THINKING_PREFIX,
|
||||
isNoThinkingAlias,
|
||||
stripNoThinkingAlias,
|
||||
toNoThinkingAlias,
|
||||
shouldExposeNoThinkingAlias,
|
||||
appendNoThinkingVariants,
|
||||
applyNoThinkingAlias,
|
||||
} from "../../open-sse/utils/noThinkingAlias.ts";
|
||||
|
||||
// ── prefix predicates ────────────────────────────────────────────────────────
|
||||
|
||||
test("NO_THINKING_PREFIX is the documented gateway prefix", () => {
|
||||
assert.equal(NO_THINKING_PREFIX, "claude-3-omniroute-no-thinking/");
|
||||
});
|
||||
|
||||
test("isNoThinkingAlias detects the prefix only", () => {
|
||||
assert.equal(isNoThinkingAlias("claude-3-omniroute-no-thinking/anthropic/claude-opus-4-5"), true);
|
||||
assert.equal(isNoThinkingAlias("anthropic/claude-opus-4-5"), false);
|
||||
assert.equal(isNoThinkingAlias("claude-opus-4-5"), false);
|
||||
// non-strings never match
|
||||
assert.equal(isNoThinkingAlias(undefined as unknown as string), false);
|
||||
assert.equal(isNoThinkingAlias(123 as unknown as string), false);
|
||||
});
|
||||
|
||||
test("stripNoThinkingAlias unwraps the prefix and passes plain ids through", () => {
|
||||
assert.equal(
|
||||
stripNoThinkingAlias("claude-3-omniroute-no-thinking/anthropic/claude-opus-4-5"),
|
||||
"anthropic/claude-opus-4-5"
|
||||
);
|
||||
assert.equal(stripNoThinkingAlias("claude-opus-4-5"), "claude-opus-4-5");
|
||||
});
|
||||
|
||||
test("toNoThinkingAlias round-trips with stripNoThinkingAlias", () => {
|
||||
const real = "anthropic/claude-sonnet-4-6";
|
||||
const alias = toNoThinkingAlias(real);
|
||||
assert.equal(alias, "claude-3-omniroute-no-thinking/anthropic/claude-sonnet-4-6");
|
||||
assert.equal(isNoThinkingAlias(alias), true);
|
||||
assert.equal(stripNoThinkingAlias(alias), real);
|
||||
});
|
||||
|
||||
// ── request-side suppression ─────────────────────────────────────────────────
|
||||
|
||||
test("applyNoThinkingAlias rewrites the model and disables thinking (Claude format)", () => {
|
||||
const body: Record<string, unknown> = {
|
||||
model: "claude-3-omniroute-no-thinking/anthropic/claude-opus-4-5",
|
||||
thinking: { type: "enabled", budget_tokens: 8000 },
|
||||
reasoning_effort: "high",
|
||||
messages: [],
|
||||
};
|
||||
const res = applyNoThinkingAlias(body, { claudeFormat: true });
|
||||
assert.equal(res.applied, true);
|
||||
assert.equal(res.realModel, "anthropic/claude-opus-4-5");
|
||||
assert.equal(body.model, "anthropic/claude-opus-4-5");
|
||||
assert.deepEqual(body.thinking, { type: "disabled" });
|
||||
assert.ok(!("reasoning_effort" in body), "reasoning_effort must be stripped");
|
||||
});
|
||||
|
||||
test("applyNoThinkingAlias strips reasoning fields without a thinking block (OpenAI format)", () => {
|
||||
const body: Record<string, unknown> = {
|
||||
model: "claude-3-omniroute-no-thinking/openai/gpt-5.4",
|
||||
reasoning_effort: "high",
|
||||
reasoning: { effort: "high" },
|
||||
messages: [],
|
||||
};
|
||||
const res = applyNoThinkingAlias(body, { claudeFormat: false });
|
||||
assert.equal(res.applied, true);
|
||||
assert.equal(body.model, "openai/gpt-5.4");
|
||||
assert.ok(!("thinking" in body), "no Claude thinking block on an OpenAI body");
|
||||
assert.ok(!("reasoning_effort" in body), "reasoning_effort must be stripped");
|
||||
assert.ok(!("reasoning" in body), "reasoning must be stripped");
|
||||
});
|
||||
|
||||
test("applyNoThinkingAlias is a no-op for plain models", () => {
|
||||
const body: Record<string, unknown> = { model: "anthropic/claude-opus-4-5", thinking: { type: "enabled" } };
|
||||
const res = applyNoThinkingAlias(body, { claudeFormat: true });
|
||||
assert.equal(res.applied, false);
|
||||
assert.equal(body.model, "anthropic/claude-opus-4-5");
|
||||
assert.deepEqual(body.thinking, { type: "enabled" }, "thinking is left untouched when not an alias");
|
||||
});
|
||||
|
||||
test("applyNoThinkingAlias ignores a malformed prefix-only model", () => {
|
||||
const body: Record<string, unknown> = { model: "claude-3-omniroute-no-thinking/" };
|
||||
const res = applyNoThinkingAlias(body, { claudeFormat: true });
|
||||
assert.equal(res.applied, false);
|
||||
assert.equal(body.model, "claude-3-omniroute-no-thinking/", "left untouched when nothing follows the prefix");
|
||||
});
|
||||
|
||||
// ── catalog gating ───────────────────────────────────────────────────────────
|
||||
|
||||
const entry = (id: string, owned_by = "anthropic") => ({ id, object: "model", owned_by });
|
||||
|
||||
test("shouldExposeNoThinkingAlias accepts a Claude reasoning model that honors disabled", () => {
|
||||
assert.equal(shouldExposeNoThinkingAlias(entry("claude-opus-4-5")), true);
|
||||
assert.equal(shouldExposeNoThinkingAlias(entry("anthropic/claude-sonnet-4-6")), true);
|
||||
});
|
||||
|
||||
test("shouldExposeNoThinkingAlias rejects models where suppression is meaningless", () => {
|
||||
// gpt-4o does not support thinking
|
||||
assert.equal(shouldExposeNoThinkingAlias(entry("gpt-4o", "openai")), false);
|
||||
// fable-5 rejects thinking.type:disabled — a no-thinking variant would be a lie
|
||||
assert.equal(shouldExposeNoThinkingAlias(entry("claude-fable-5")), false);
|
||||
// combos are virtual, never aliased
|
||||
assert.equal(shouldExposeNoThinkingAlias(entry("my-combo", "combo")), false);
|
||||
// never double-alias
|
||||
assert.equal(
|
||||
shouldExposeNoThinkingAlias(entry("claude-3-omniroute-no-thinking/anthropic/claude-opus-4-5")),
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
test("appendNoThinkingVariants adds one variant per eligible model and preserves the rest", () => {
|
||||
const models = [
|
||||
entry("claude-opus-4-5"),
|
||||
entry("gpt-4o", "openai"),
|
||||
entry("claude-fable-5"),
|
||||
];
|
||||
const out = appendNoThinkingVariants(models);
|
||||
const ids = out.map((m) => m.id);
|
||||
assert.ok(ids.includes("claude-3-omniroute-no-thinking/claude-opus-4-5"), "eligible model gets a variant");
|
||||
assert.ok(!ids.includes("claude-3-omniroute-no-thinking/gpt-4o"), "non-thinking model has no variant");
|
||||
assert.ok(!ids.includes("claude-3-omniroute-no-thinking/claude-fable-5"), "reject-disabled model has no variant");
|
||||
assert.equal(out.length, models.length + 1, "exactly one variant appended");
|
||||
// originals preserved up front
|
||||
assert.deepEqual(out.slice(0, 3), models);
|
||||
});
|
||||
|
||||
test("appendNoThinkingVariants returns the same array reference when nothing is eligible", () => {
|
||||
const models = [entry("gpt-4o", "openai")];
|
||||
assert.equal(appendNoThinkingVariants(models), models);
|
||||
});
|
||||
@@ -2,11 +2,14 @@ import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
attachOmniRouteMetaHeaders,
|
||||
buildOmniRouteResponseMetaHeaders,
|
||||
buildOmniRouteSseMetadataComment,
|
||||
formatOmniRouteCost,
|
||||
getOmniRouteTokenCounts,
|
||||
} from "../../src/domain/omnirouteResponseMeta.ts";
|
||||
import { APP_CONFIG } from "../../src/shared/constants/appConfig.ts";
|
||||
import { OMNIROUTE_RESPONSE_HEADERS } from "../../src/shared/constants/headers.ts";
|
||||
|
||||
test("getOmniRouteTokenCounts normalizes common usage shapes", () => {
|
||||
assert.deepEqual(
|
||||
@@ -47,6 +50,57 @@ test("buildOmniRouteResponseMetaHeaders formats provider alias, tokens, latency,
|
||||
assert.equal(headers["X-OmniRoute-Response-Cost"], "0.0012345679");
|
||||
});
|
||||
|
||||
test("buildOmniRouteResponseMetaHeaders always emits X-OmniRoute-Version", () => {
|
||||
const headers = buildOmniRouteResponseMetaHeaders({ provider: "openai", model: "gpt" });
|
||||
assert.equal(headers[OMNIROUTE_RESPONSE_HEADERS.version], APP_CONFIG.version);
|
||||
|
||||
// Even with no provider/model at all, the version is still attached.
|
||||
const bare = buildOmniRouteResponseMetaHeaders({});
|
||||
assert.equal(bare[OMNIROUTE_RESPONSE_HEADERS.version], APP_CONFIG.version);
|
||||
});
|
||||
|
||||
test("buildOmniRouteResponseMetaHeaders emits X-OmniRoute-Request-Id only when provided", () => {
|
||||
const withId = buildOmniRouteResponseMetaHeaders({ model: "gpt", requestId: "req-123" });
|
||||
assert.equal(withId[OMNIROUTE_RESPONSE_HEADERS.requestId], "req-123");
|
||||
|
||||
const noId = buildOmniRouteResponseMetaHeaders({ model: "gpt" });
|
||||
assert.equal(noId[OMNIROUTE_RESPONSE_HEADERS.requestId], undefined);
|
||||
|
||||
const nullId = buildOmniRouteResponseMetaHeaders({ model: "gpt", requestId: null });
|
||||
assert.equal(nullId[OMNIROUTE_RESPONSE_HEADERS.requestId], undefined);
|
||||
|
||||
const blankId = buildOmniRouteResponseMetaHeaders({ model: "gpt", requestId: " " });
|
||||
assert.equal(blankId[OMNIROUTE_RESPONSE_HEADERS.requestId], undefined);
|
||||
});
|
||||
|
||||
test("attachOmniRouteMetaHeaders mutates a Headers instance in place, preserving existing entries", () => {
|
||||
const headers = new Headers({ "Content-Type": "application/json" });
|
||||
attachOmniRouteMetaHeaders(headers, {
|
||||
provider: "openai",
|
||||
model: "gpt",
|
||||
requestId: "req-abc",
|
||||
});
|
||||
|
||||
assert.equal(headers.get("Content-Type"), "application/json");
|
||||
assert.equal(headers.get(OMNIROUTE_RESPONSE_HEADERS.version), APP_CONFIG.version);
|
||||
assert.equal(headers.get(OMNIROUTE_RESPONSE_HEADERS.requestId), "req-abc");
|
||||
assert.equal(headers.get(OMNIROUTE_RESPONSE_HEADERS.model), "gpt");
|
||||
});
|
||||
|
||||
test("attachOmniRouteMetaHeaders mutates a plain record in place, preserving existing entries", () => {
|
||||
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
||||
attachOmniRouteMetaHeaders(headers, {
|
||||
provider: "openai",
|
||||
model: "gpt",
|
||||
});
|
||||
|
||||
assert.equal(headers["Content-Type"], "application/json");
|
||||
assert.equal(headers[OMNIROUTE_RESPONSE_HEADERS.version], APP_CONFIG.version);
|
||||
assert.equal(headers[OMNIROUTE_RESPONSE_HEADERS.model], "gpt");
|
||||
// No requestId provided → header omitted.
|
||||
assert.equal(headers[OMNIROUTE_RESPONSE_HEADERS.requestId], undefined);
|
||||
});
|
||||
|
||||
test("buildOmniRouteSseMetadataComment emits comment lines compatible with SSE", () => {
|
||||
const comment = buildOmniRouteSseMetadataComment({
|
||||
provider: "openai",
|
||||
|
||||
68
tests/unit/openai-responses-verbosity.test.ts
Normal file
68
tests/unit/openai-responses-verbosity.test.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Verbosity mapping across the OpenAI Chat <-> Responses request translators.
|
||||
*
|
||||
* Chat Completions carries GPT-5 verbosity as top-level `verbosity`; the Responses API
|
||||
* nests it as `text.verbosity`. These tests pin both directions so the hint is not lost
|
||||
* when a request crosses formats (e.g. a Chat client routed to a Responses backend).
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
openaiToOpenAIResponsesRequest,
|
||||
openaiResponsesToOpenAIRequest,
|
||||
} from "../../open-sse/translator/request/openai-responses.ts";
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> {
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
test("Chat -> Responses maps verbosity to text.verbosity", () => {
|
||||
const out = asRecord(
|
||||
openaiToOpenAIResponsesRequest(
|
||||
"gpt-5.5",
|
||||
{ model: "gpt-5.5", messages: [{ role: "user", content: "hi" }], verbosity: "low" },
|
||||
true,
|
||||
{}
|
||||
)
|
||||
);
|
||||
assert.deepEqual(out.text, { verbosity: "low" });
|
||||
assert.equal(out.verbosity, undefined);
|
||||
});
|
||||
|
||||
test("Chat -> Responses ignores an invalid verbosity value", () => {
|
||||
const out = asRecord(
|
||||
openaiToOpenAIResponsesRequest(
|
||||
"gpt-5.5",
|
||||
{ model: "gpt-5.5", messages: [{ role: "user", content: "hi" }], verbosity: "loud" },
|
||||
true,
|
||||
{}
|
||||
)
|
||||
);
|
||||
assert.equal(out.text, undefined);
|
||||
});
|
||||
|
||||
test("Responses -> Chat maps text.verbosity to top-level verbosity and drops text", () => {
|
||||
const out = asRecord(
|
||||
openaiResponsesToOpenAIRequest(
|
||||
"gpt-5.5",
|
||||
{ model: "gpt-5.5", input: [{ role: "user", content: "hi" }], text: { verbosity: "high" } },
|
||||
false,
|
||||
{}
|
||||
)
|
||||
);
|
||||
assert.equal(out.verbosity, "high");
|
||||
assert.equal(out.text, undefined);
|
||||
});
|
||||
|
||||
test("Responses -> Chat drops a stray non-verbosity text wrapper", () => {
|
||||
const out = asRecord(
|
||||
openaiResponsesToOpenAIRequest(
|
||||
"gpt-5.5",
|
||||
{ model: "gpt-5.5", input: [{ role: "user", content: "hi" }], text: { format: { type: "json" } } },
|
||||
false,
|
||||
{}
|
||||
)
|
||||
);
|
||||
assert.equal(out.text, undefined);
|
||||
assert.equal(out.verbosity, undefined);
|
||||
});
|
||||
32
tests/unit/openapi-ws-endpoint.test.ts
Normal file
32
tests/unit/openapi-ws-endpoint.test.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import yaml from "js-yaml";
|
||||
|
||||
// Regression guard: the OpenAI-compatible chat WebSocket endpoint (/api/v1/ws)
|
||||
// must be documented in openapi.yaml so it shows up on the dashboard's
|
||||
// "API Endpoints" page (which renders /api/openapi/spec, parsed from this file).
|
||||
// The route (src/app/api/v1/ws/route.ts) shipped in v3.6.6 but was never listed
|
||||
// in the spec, so it was invisible in the endpoints reference.
|
||||
|
||||
const ROOT = fileURLToPath(new URL("../../", import.meta.url));
|
||||
const spec = yaml.load(readFileSync(ROOT + "docs/reference/openapi.yaml", "utf8")) as {
|
||||
paths: Record<string, Record<string, { tags?: string[]; security?: unknown[]; responses?: Record<string, unknown> }>>;
|
||||
};
|
||||
|
||||
test("openapi.yaml documents the /api/v1/ws chat WebSocket endpoint", () => {
|
||||
const entry = spec.paths["/api/v1/ws"];
|
||||
assert.ok(entry, "/api/v1/ws must be present in openapi.yaml paths");
|
||||
assert.ok(entry.get, "/api/v1/ws must document the GET (handshake/upgrade) operation");
|
||||
});
|
||||
|
||||
test("/api/v1/ws is tagged, authenticated and documents the WS upgrade responses", () => {
|
||||
const op = spec.paths["/api/v1/ws"].get;
|
||||
assert.ok((op.tags ?? []).length > 0, "should be tagged so it groups on the endpoints page");
|
||||
assert.ok(Array.isArray(op.security) && op.security.length > 0, "should require auth (BearerAuth)");
|
||||
const responses = op.responses ?? {};
|
||||
for (const code of ["101", "426"]) {
|
||||
assert.ok(code in responses, `should document the ${code} WebSocket response`);
|
||||
}
|
||||
});
|
||||
@@ -59,7 +59,10 @@ describe("transformToOmniRoute", () => {
|
||||
assert.strictEqual(result["gemini-cli"]["gemini-2.5-flash"].input, 0.3);
|
||||
});
|
||||
|
||||
test("skips non-chat models (embedding, image, audio)", () => {
|
||||
test("ingests non-chat models (embedding token + image per-image)", () => {
|
||||
// Phase 2 (cost-telemetry parity): non-chat modes are no longer skipped.
|
||||
// Token-priced modes (embedding) are scaled to $/1M like chat; non-token
|
||||
// modes (image) carry their per-image cost through verbatim as absolute USD.
|
||||
const raw = {
|
||||
"openai/text-embedding-3-small": {
|
||||
input_cost_per_token: 0.00000002,
|
||||
@@ -67,19 +70,43 @@ describe("transformToOmniRoute", () => {
|
||||
litellm_provider: "openai",
|
||||
mode: "embedding",
|
||||
},
|
||||
"openai/gpt-image-2": {
|
||||
"openai/dall-e-3": {
|
||||
input_cost_per_token: 0,
|
||||
output_cost_per_token: 0,
|
||||
litellm_provider: "openai",
|
||||
mode: "image_generation",
|
||||
output_cost_per_image: 0.04,
|
||||
},
|
||||
};
|
||||
|
||||
const result = transformToOmniRoute(raw);
|
||||
|
||||
const embedding = result.openai?.["text-embedding-3-small"];
|
||||
assert.ok(embedding, "Should ingest token-priced embedding model");
|
||||
assert.strictEqual(embedding.input, 0.02);
|
||||
assert.strictEqual(embedding.mode, "embedding");
|
||||
|
||||
const image = result.openai?.["dall-e-3"];
|
||||
assert.ok(image, "Should ingest image model with per-image cost");
|
||||
assert.strictEqual(image.output_cost_per_image, 0.04);
|
||||
assert.strictEqual(image.mode, "image_generation");
|
||||
});
|
||||
|
||||
test("skips models with no token AND no non-token pricing", () => {
|
||||
const raw = {
|
||||
"openai/metadata-only": {
|
||||
litellm_provider: "openai",
|
||||
mode: "image_generation",
|
||||
},
|
||||
};
|
||||
|
||||
const result = transformToOmniRoute(raw);
|
||||
|
||||
// openai key should not exist since all models were filtered
|
||||
const openaiModels = result.openai || {};
|
||||
assert.strictEqual(Object.keys(openaiModels).length, 0, "Should skip non-chat models");
|
||||
assert.strictEqual(
|
||||
Object.keys(openaiModels).length,
|
||||
0,
|
||||
"Should skip models carrying no pricing of any kind"
|
||||
);
|
||||
});
|
||||
|
||||
test("includes cache pricing when available", () => {
|
||||
|
||||
54
tests/unit/pricingSync.modal.test.ts
Normal file
54
tests/unit/pricingSync.modal.test.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { transformToOmniRoute } from "../../src/lib/pricingSync.ts";
|
||||
|
||||
test("transformToOmniRoute ingests an image model's per-image cost", () => {
|
||||
const out = transformToOmniRoute({
|
||||
"dall-e-3": {
|
||||
mode: "image_generation",
|
||||
litellm_provider: "openai",
|
||||
output_cost_per_image: 0.04,
|
||||
},
|
||||
});
|
||||
assert.equal(out.openai?.["dall-e-3"]?.output_cost_per_image, 0.04);
|
||||
});
|
||||
test("transformToOmniRoute ingests an audio (per-second) model", () => {
|
||||
const out = transformToOmniRoute({
|
||||
"whisper-1": {
|
||||
mode: "audio_transcription",
|
||||
litellm_provider: "openai",
|
||||
input_cost_per_second: 0.0001,
|
||||
},
|
||||
});
|
||||
assert.equal(out.openai?.["whisper-1"]?.input_cost_per_second, 0.0001);
|
||||
});
|
||||
test("transformToOmniRoute still ingests chat token pricing (no regression)", () => {
|
||||
const out = transformToOmniRoute({
|
||||
"gpt-4o": {
|
||||
mode: "chat",
|
||||
litellm_provider: "openai",
|
||||
input_cost_per_token: 0.0000025,
|
||||
output_cost_per_token: 0.00001,
|
||||
},
|
||||
});
|
||||
assert.equal(out.openai?.["gpt-4o"]?.input, 2.5);
|
||||
assert.equal(out.openai?.["gpt-4o"]?.output, 10);
|
||||
});
|
||||
test("transformToOmniRoute maps newly-covered providers", () => {
|
||||
const out = transformToOmniRoute({
|
||||
"mistral-large": {
|
||||
mode: "chat",
|
||||
litellm_provider: "mistral",
|
||||
input_cost_per_token: 0.000002,
|
||||
output_cost_per_token: 0.000006,
|
||||
},
|
||||
"grok-2": {
|
||||
mode: "chat",
|
||||
litellm_provider: "xai",
|
||||
input_cost_per_token: 0.000002,
|
||||
output_cost_per_token: 0.00001,
|
||||
},
|
||||
});
|
||||
assert.ok(out.mistral?.["mistral-large"]);
|
||||
assert.ok(out.xai?.["grok-2"]);
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user