mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-10 01:39:02 +00:00
291 lines
11 KiB
TypeScript
291 lines
11 KiB
TypeScript
/**
|
|
* Tests for the 3 mrmm-fork features backported to @omniroute/opencode-plugin:
|
|
*
|
|
* 1. `normaliseFreeLabel` — free-tier model display names get a consistent
|
|
* `[Free] ` prefix instead of trailing "(Free)" or ad-hoc "free" words.
|
|
*
|
|
* 2. `resolveApiBlock` — per-provider-prefix API format routing. Anthropic
|
|
* prefixes (`cc/`, `claude/`, `anthropic/`, `kiro/`, `kr/`) get the
|
|
* Anthropic SDK block; everything else gets OpenAI-compat.
|
|
*
|
|
* 3. `debugLog` — JSONL request/response capture, gated by
|
|
* `features.debugLog` and togglable at runtime via
|
|
* `debugLogEnabled/SetEnabled`.
|
|
*/
|
|
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import {
|
|
normaliseFreeLabel,
|
|
resolveApiBlock,
|
|
DEFAULT_ANTHROPIC_PREFIXES,
|
|
ensureV1Suffix,
|
|
debugLogEnabled,
|
|
debugLogSetEnabled,
|
|
debugLogClear,
|
|
debugLogRead,
|
|
debugLogAppend,
|
|
createDebugLoggingFetch,
|
|
DebugLogEntry,
|
|
} from "../src/index.js";
|
|
|
|
// ── 1. normaliseFreeLabel ────────────────────────────────────────────────────
|
|
|
|
test("normaliseFreeLabel: '(Free)' suffix becomes [Free] prefix", () => {
|
|
assert.equal(normaliseFreeLabel("GPT-4.1 (Free)"), "[Free] GPT-4.1");
|
|
});
|
|
|
|
test("normaliseFreeLabel: trailing ' Free' word becomes [Free] prefix", () => {
|
|
assert.equal(
|
|
normaliseFreeLabel("DeepSeek V4 Flash Free"),
|
|
"[Free] DeepSeek V4 Flash"
|
|
);
|
|
});
|
|
|
|
test("normaliseFreeLabel: trailing '-free' (hyphen) becomes [Free] prefix", () => {
|
|
assert.equal(normaliseFreeLabel("Llama 4 Scout-free"), "[Free] Llama 4 Scout");
|
|
});
|
|
|
|
test("normaliseFreeLabel: case-insensitive (FREE, Free, free all match)", () => {
|
|
assert.equal(normaliseFreeLabel("Model A FREE"), "[Free] Model A");
|
|
assert.equal(normaliseFreeLabel("Model A free"), "[Free] Model A");
|
|
assert.equal(normaliseFreeLabel("Model A Free"), "[Free] Model A");
|
|
});
|
|
|
|
test("normaliseFreeLabel: names without 'free' pass through unchanged", () => {
|
|
assert.equal(normaliseFreeLabel("Claude 4.7 Opus"), "Claude 4.7 Opus");
|
|
assert.equal(normaliseFreeLabel("GPT-5"), "GPT-5");
|
|
});
|
|
|
|
test("normaliseFreeLabel: 'free' in the middle of a name is NOT rewritten", () => {
|
|
// Only trailing/standalone "free" markers count; embedded "freedom" stays
|
|
assert.equal(
|
|
normaliseFreeLabel("Freedom Model"),
|
|
"Freedom Model"
|
|
);
|
|
});
|
|
|
|
test("normaliseFreeLabel: empty / whitespace-only inputs are handled", () => {
|
|
// Empty input returns empty; pure whitespace input passes through (no Free marker)
|
|
assert.equal(normaliseFreeLabel(""), "");
|
|
assert.equal(normaliseFreeLabel(" "), " ");
|
|
});
|
|
|
|
// ── 2. resolveApiBlock ───────────────────────────────────────────────────────
|
|
|
|
test("resolveApiBlock: cc/* models get the Anthropic SDK block (no /v1)", () => {
|
|
const block = resolveApiBlock("cc/claude-opus-4-7", "https://api.example.com");
|
|
assert.equal(block.id, "anthropic");
|
|
assert.equal(block.npm, "@ai-sdk/anthropic");
|
|
assert.equal(block.url, "https://api.example.com"); // NO /v1 suffix
|
|
});
|
|
|
|
test("resolveApiBlock: claude/*, anthropic/*, kiro/*, kr/* all route to Anthropic", () => {
|
|
for (const id of [
|
|
"claude/claude-opus-4-7",
|
|
"anthropic/claude-sonnet-4",
|
|
"kiro/claude-sonnet-4-5",
|
|
"kr/claude-opus-4-6",
|
|
]) {
|
|
const block = resolveApiBlock(id, "https://api.example.com");
|
|
assert.equal(block.id, "anthropic", `${id} should route to Anthropic`);
|
|
assert.equal(block.npm, "@ai-sdk/anthropic");
|
|
}
|
|
});
|
|
|
|
test("resolveApiBlock: non-Anthropic models get OpenAI-compat with /v1", () => {
|
|
const block = resolveApiBlock("gpt-4o", "https://api.example.com");
|
|
assert.equal(block.id, "openai-compatible");
|
|
assert.equal(block.npm, "@ai-sdk/openai-compatible");
|
|
assert.equal(block.url, "https://api.example.com/v1");
|
|
});
|
|
|
|
test("resolveApiBlock: user can override anthropicPrefixes to add custom prefixes", () => {
|
|
const block = resolveApiBlock("myproxy/claude-opus", "https://api.example.com", {
|
|
anthropicPrefixes: ["myproxy"],
|
|
});
|
|
assert.equal(block.id, "anthropic");
|
|
assert.equal(block.npm, "@ai-sdk/anthropic");
|
|
});
|
|
|
|
test("resolveApiBlock: empty anthropicPrefixes forces OpenAI-compat for everything", () => {
|
|
const block = resolveApiBlock("cc/claude-opus", "https://api.example.com", {
|
|
anthropicPrefixes: [],
|
|
});
|
|
assert.equal(block.id, "openai-compatible");
|
|
});
|
|
|
|
test("resolveApiBlock: baseURL that already ends in /v1 is not double-suffixed (OpenAI path)", () => {
|
|
const block = resolveApiBlock("gpt-4o", "https://api.example.com/v1");
|
|
assert.equal(block.url, "https://api.example.com/v1"); // idempotent
|
|
});
|
|
|
|
test("resolveApiBlock: model id without '/' uses the id as prefix", () => {
|
|
const block = resolveApiBlock("claude-opus-4-7", "https://api.example.com");
|
|
// The whole id is the prefix, which doesn't match "cc"/"claude" etc.
|
|
// So it falls through to OpenAI-compat.
|
|
assert.equal(block.id, "openai-compatible");
|
|
});
|
|
|
|
test("DEFAULT_ANTHROPIC_PREFIXES: contains the canonical Anthropic aliases", () => {
|
|
assert.deepEqual(DEFAULT_ANTHROPIC_PREFIXES, [
|
|
"cc",
|
|
"claude",
|
|
"anthropic",
|
|
"kiro",
|
|
"kr",
|
|
]);
|
|
});
|
|
|
|
test("ensureV1Suffix: idempotent for URLs that already end in /v1", () => {
|
|
assert.equal(ensureV1Suffix("https://api.example.com/v1"), "https://api.example.com/v1");
|
|
assert.equal(
|
|
ensureV1Suffix("https://api.example.com/v1/"),
|
|
"https://api.example.com/v1" // trailing slash is stripped
|
|
);
|
|
});
|
|
|
|
test("ensureV1Suffix: appends /v1 when missing", () => {
|
|
assert.equal(ensureV1Suffix("https://api.example.com"), "https://api.example.com/v1");
|
|
assert.equal(ensureV1Suffix("https://api.example.com/"), "https://api.example.com/v1");
|
|
});
|
|
|
|
// ── 3. debugLog ──────────────────────────────────────────────────────────────
|
|
|
|
test("debugLog: default state is disabled", () => {
|
|
debugLogClear("test-provider-disabled-default");
|
|
assert.equal(debugLogEnabled("test-provider-disabled-default"), false);
|
|
});
|
|
|
|
test("debugLogSetEnabled + debugLogEnabled: roundtrip", () => {
|
|
debugLogSetEnabled("test-provider-toggle", true);
|
|
assert.equal(debugLogEnabled("test-provider-toggle"), true);
|
|
debugLogSetEnabled("test-provider-toggle", false);
|
|
assert.equal(debugLogEnabled("test-provider-toggle"), false);
|
|
});
|
|
|
|
test("debugLogAppend + debugLogRead: roundtrip preserves entry shape", () => {
|
|
const providerId = "test-provider-readroundtrip";
|
|
debugLogClear(providerId);
|
|
const entry: DebugLogEntry = {
|
|
reqId: "req-1",
|
|
providerId,
|
|
ts: 1700000000000,
|
|
url: "https://api.example.com/v1/chat",
|
|
method: "POST",
|
|
reqHeaders: { "content-type": "application/json" },
|
|
reqBody: { model: "gpt-4o", messages: [] },
|
|
resStatus: 200,
|
|
resHeaders: { "content-type": "application/json" },
|
|
resBody: { choices: [] },
|
|
durationMs: 42,
|
|
};
|
|
debugLogAppend(entry);
|
|
const read = debugLogRead(providerId, 10);
|
|
assert.equal(read.length, 1);
|
|
assert.deepEqual(read[0], entry);
|
|
});
|
|
|
|
test("createDebugLoggingFetch: passes through when disabled", async () => {
|
|
const providerId = "test-provider-passthrough";
|
|
debugLogClear(providerId);
|
|
debugLogSetEnabled(providerId, false);
|
|
const calls: unknown[] = [];
|
|
const inner: typeof fetch = async (input) => {
|
|
calls.push(input);
|
|
return new Response("ok", { status: 200 });
|
|
};
|
|
const wrapped = createDebugLoggingFetch(inner, providerId, false);
|
|
const res = await wrapped("https://api.example.com/v1/chat");
|
|
assert.equal(res.status, 200);
|
|
assert.equal(calls.length, 1);
|
|
// No log entry should be written when disabled
|
|
assert.equal(debugLogRead(providerId).length, 0);
|
|
});
|
|
|
|
test("createDebugLoggingFetch: captures request/response when enabled", async () => {
|
|
const providerId = "test-provider-captures";
|
|
debugLogClear(providerId);
|
|
const inner: typeof fetch = async () =>
|
|
new Response(JSON.stringify({ ok: true }), {
|
|
status: 200,
|
|
headers: { "content-type": "application/json" },
|
|
});
|
|
const wrapped = createDebugLoggingFetch(inner, providerId, true);
|
|
const res = await wrapped("https://api.example.com/v1/chat", {
|
|
method: "POST",
|
|
headers: { "content-type": "application/json" },
|
|
body: JSON.stringify({ model: "gpt-4o" }),
|
|
});
|
|
assert.equal(res.status, 200);
|
|
const entries = debugLogRead(providerId);
|
|
assert.equal(entries.length, 1);
|
|
assert.equal(entries[0].method, "POST");
|
|
assert.equal(entries[0].resStatus, 200);
|
|
assert.equal(entries[0].url, "https://api.example.com/v1/chat");
|
|
assert.deepEqual(entries[0].reqBody, { model: "gpt-4o" });
|
|
});
|
|
|
|
test("createDebugLoggingFetch: records error without crashing the wrapped fetch", async () => {
|
|
const providerId = "test-provider-error";
|
|
debugLogClear(providerId);
|
|
const inner: typeof fetch = async () => {
|
|
throw new Error("network down");
|
|
};
|
|
const wrapped = createDebugLoggingFetch(inner, providerId, true);
|
|
await assert.rejects(wrapped("https://api.example.com/v1/chat"), /network down/);
|
|
const entries = debugLogRead(providerId);
|
|
assert.equal(entries.length, 1);
|
|
assert.equal(entries[0].resStatus, null);
|
|
assert.equal(entries[0].error, "network down");
|
|
});
|
|
|
|
// ── Regression tests for the 3 HIGH-priority bot review fixes ───────────────
|
|
|
|
test("createDebugLoggingFetch: URL instance input is captured (not 'undefined')", async () => {
|
|
const providerId = "test-provider-url-input";
|
|
debugLogClear(providerId);
|
|
const inner: typeof fetch = async () =>
|
|
new Response("ok", { status: 200 });
|
|
const wrapped = createDebugLoggingFetch(inner, providerId, true);
|
|
await wrapped(new URL("https://api.example.com/v1/chat"));
|
|
const entries = debugLogRead(providerId);
|
|
assert.equal(entries.length, 1);
|
|
assert.equal(entries[0].url, "https://api.example.com/v1/chat");
|
|
assert.notEqual(entries[0].url, undefined);
|
|
});
|
|
|
|
test("createDebugLoggingFetch: Request object input captures URL and headers", async () => {
|
|
const providerId = "test-provider-request-input";
|
|
debugLogClear(providerId);
|
|
const inner: typeof fetch = async () =>
|
|
new Response("ok", { status: 200 });
|
|
const wrapped = createDebugLoggingFetch(inner, providerId, true);
|
|
const req = new Request("https://api.example.com/v1/chat", {
|
|
method: "POST",
|
|
headers: { "x-test": "yes" },
|
|
});
|
|
await wrapped(req);
|
|
const entries = debugLogRead(providerId);
|
|
assert.equal(entries.length, 1);
|
|
assert.equal(entries[0].url, "https://api.example.com/v1/chat");
|
|
assert.equal(entries[0].reqHeaders["x-test"], "yes");
|
|
});
|
|
|
|
test("createDebugLoggingFetch: SSE response is NOT buffered (resBody is the stream marker)", async () => {
|
|
const providerId = "test-provider-sse";
|
|
debugLogClear(providerId);
|
|
const inner: typeof fetch = async () =>
|
|
new Response("data: hello\n\n", {
|
|
status: 200,
|
|
headers: { "content-type": "text/event-stream" },
|
|
});
|
|
const wrapped = createDebugLoggingFetch(inner, providerId, true);
|
|
const res = await wrapped("https://api.example.com/v1/stream");
|
|
// The response body must remain readable downstream
|
|
const txt = await res.text();
|
|
assert.equal(txt, "data: hello\n\n");
|
|
const entries = debugLogRead(providerId);
|
|
assert.equal(entries.length, 1);
|
|
assert.equal(entries[0].resBody, "[stream]", "SSE responses must not be buffered");
|
|
});
|