mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
This reverts commit e61b75f007.
This commit is contained in:
@@ -1,279 +0,0 @@
|
||||
/**
|
||||
* Integration tests for /api/cli-tools/codewhale-settings
|
||||
*
|
||||
* CodeWhale (https://github.com/Hmbown/CodeWhale) is the actively-maintained
|
||||
* successor to DeepSeek TUI (same author, renamed project). This route mirrors
|
||||
* deepseek-tui-settings/route.ts but writes/reads a dual config path:
|
||||
* - primary: ~/.codewhale/config.toml
|
||||
* - legacy: ~/.deepseek/config.toml (kept in sync when it already exists,
|
||||
* so users upgrading their CLI binary keep working)
|
||||
*/
|
||||
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-codewhale-settings-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
process.env.API_KEY_SECRET = "test-api-key-secret-codewhale";
|
||||
process.env.JWT_SECRET = "test-jwt-secret-codewhale";
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const localDb = await import("../../src/lib/localDb.ts");
|
||||
|
||||
const { GET, POST, DELETE } = await import("../../src/app/api/cli-tools/codewhale-settings/route.ts");
|
||||
|
||||
async function resetStorage() {
|
||||
delete process.env.INITIAL_PASSWORD;
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
async function enableAuth() {
|
||||
process.env.INITIAL_PASSWORD = "test-bootstrap";
|
||||
await localDb.updateSettings({ requireLogin: true, password: "" });
|
||||
}
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await resetStorage();
|
||||
});
|
||||
|
||||
// ── Test 1: GET without auth → 401 ──────────────────────────────────────────
|
||||
|
||||
test("codewhale-settings GET: returns 401 when auth required and no token", async () => {
|
||||
await enableAuth();
|
||||
const res = await GET(new Request("http://localhost/api/cli-tools/codewhale-settings"));
|
||||
assert.equal(res.status, 401, `Expected 401, got ${res.status}`);
|
||||
});
|
||||
|
||||
// ── Test 2: GET without auth requirement → 200 ───────────────────────────────
|
||||
|
||||
test("codewhale-settings GET: returns 200 when auth not required", async () => {
|
||||
const res = await GET(new Request("http://localhost/api/cli-tools/codewhale-settings"));
|
||||
assert.equal(res.status, 200, `Expected 200, got ${res.status}`);
|
||||
const body = await res.json();
|
||||
assert.ok(
|
||||
"installed" in body || "config" in body,
|
||||
"Response should contain installed or config field"
|
||||
);
|
||||
});
|
||||
|
||||
// ── Test 3: POST with invalid body → 400 ─────────────────────────────────────
|
||||
|
||||
test("codewhale-settings POST: 400 when baseUrl is missing", async () => {
|
||||
const res = await POST(
|
||||
new Request("http://localhost/api/cli-tools/codewhale-settings", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ apiKey: "sk-test", model: "deepseek-v4-pro" }),
|
||||
})
|
||||
);
|
||||
assert.equal(res.status, 400, `Expected 400, got ${res.status}`);
|
||||
const body = await res.json();
|
||||
assert.ok(body.error !== undefined);
|
||||
});
|
||||
|
||||
test("codewhale-settings POST: 400 when model is missing", async () => {
|
||||
const res = await POST(
|
||||
new Request("http://localhost/api/cli-tools/codewhale-settings", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ baseUrl: "http://localhost:20128", apiKey: "sk-test" }),
|
||||
})
|
||||
);
|
||||
assert.equal(res.status, 400, `Expected 400, got ${res.status}`);
|
||||
});
|
||||
|
||||
// ── Test 4: POST with valid body → writes PRIMARY config.toml only (no legacy dir) ──
|
||||
|
||||
test("codewhale-settings POST: writes primary ~/.codewhale/config.toml for a fresh install", async () => {
|
||||
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "codewhale-home-"));
|
||||
const origHome = process.env.HOME;
|
||||
process.env.HOME = tmpHome;
|
||||
|
||||
try {
|
||||
const res = await POST(
|
||||
new Request("http://localhost/api/cli-tools/codewhale-settings", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
baseUrl: "http://localhost:20128",
|
||||
apiKey: "sk-test-codewhale-key",
|
||||
model: "deepseek-v4-pro",
|
||||
}),
|
||||
})
|
||||
);
|
||||
assert.ok([200, 403, 500].includes(res.status), `Unexpected status ${res.status}`);
|
||||
if (res.status === 200) {
|
||||
const body = await res.json();
|
||||
assert.equal(body.success, true);
|
||||
|
||||
const primaryPath = path.join(tmpHome, ".codewhale", "config.toml");
|
||||
assert.ok(fs.existsSync(primaryPath), "Primary ~/.codewhale/config.toml must be written");
|
||||
const content = fs.readFileSync(primaryPath, "utf-8");
|
||||
assert.ok(content.includes("managed by OmniRoute"), "Config should have OmniRoute marker");
|
||||
assert.ok(content.includes("http://localhost:20128"), "Config should contain base URL");
|
||||
assert.ok(content.includes("[openai]"), "Config should have [openai] section");
|
||||
|
||||
// No legacy ~/.deepseek dir existed before the write — must NOT be created.
|
||||
const legacyPath = path.join(tmpHome, ".deepseek", "config.toml");
|
||||
assert.ok(
|
||||
!fs.existsSync(legacyPath),
|
||||
"Legacy ~/.deepseek/config.toml must not be created for a fresh install"
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
process.env.HOME = origHome;
|
||||
fs.rmSync(tmpHome, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
// ── Test 5: POST keeps an EXISTING legacy ~/.deepseek config in sync ────────
|
||||
|
||||
test("codewhale-settings POST: syncs an existing legacy ~/.deepseek/config.toml", async () => {
|
||||
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "codewhale-home-legacy-"));
|
||||
const origHome = process.env.HOME;
|
||||
process.env.HOME = tmpHome;
|
||||
|
||||
try {
|
||||
// Simulate an existing DeepSeek TUI install (pre-CodeWhale upgrade).
|
||||
const legacyDir = path.join(tmpHome, ".deepseek");
|
||||
fs.mkdirSync(legacyDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(legacyDir, "config.toml"), 'provider = "deepseek"\n');
|
||||
|
||||
const res = await POST(
|
||||
new Request("http://localhost/api/cli-tools/codewhale-settings", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
baseUrl: "http://localhost:20128",
|
||||
apiKey: "sk-test-codewhale-key",
|
||||
model: "deepseek-v4-flash",
|
||||
}),
|
||||
})
|
||||
);
|
||||
assert.ok([200, 403, 500].includes(res.status), `Unexpected status ${res.status}`);
|
||||
if (res.status === 200) {
|
||||
const primaryPath = path.join(tmpHome, ".codewhale", "config.toml");
|
||||
const legacyPath = path.join(tmpHome, ".deepseek", "config.toml");
|
||||
|
||||
assert.ok(fs.existsSync(primaryPath), "Primary config must be written");
|
||||
assert.ok(fs.existsSync(legacyPath), "Legacy config must still exist");
|
||||
|
||||
const primaryContent = fs.readFileSync(primaryPath, "utf-8");
|
||||
const legacyContent = fs.readFileSync(legacyPath, "utf-8");
|
||||
assert.ok(primaryContent.includes("http://localhost:20128"));
|
||||
assert.ok(
|
||||
legacyContent.includes("http://localhost:20128"),
|
||||
"Legacy config must be kept in sync with the new base URL"
|
||||
);
|
||||
assert.equal(primaryContent, legacyContent, "Primary and legacy configs should match");
|
||||
}
|
||||
} finally {
|
||||
process.env.HOME = origHome;
|
||||
fs.rmSync(tmpHome, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
// ── Test 6: GET reads from legacy path when only legacy config exists ───────
|
||||
|
||||
test("codewhale-settings GET: falls back to legacy ~/.deepseek/config.toml when primary is absent", async () => {
|
||||
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "codewhale-home-getlegacy-"));
|
||||
const origHome = process.env.HOME;
|
||||
process.env.HOME = tmpHome;
|
||||
|
||||
try {
|
||||
const legacyDir = path.join(tmpHome, ".deepseek");
|
||||
fs.mkdirSync(legacyDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(legacyDir, "config.toml"),
|
||||
'# managed by OmniRoute (plan 14)\n[openai]\nbase_url = "http://localhost:20128"\n'
|
||||
);
|
||||
|
||||
const res = await GET(new Request("http://localhost/api/cli-tools/codewhale-settings"));
|
||||
assert.equal(res.status, 200);
|
||||
const body = await res.json();
|
||||
if (body.config) {
|
||||
assert.ok(body.config.includes("managed by OmniRoute"));
|
||||
assert.equal(body.hasOmniRoute, true);
|
||||
}
|
||||
} finally {
|
||||
process.env.HOME = origHome;
|
||||
fs.rmSync(tmpHome, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
// ── Test 7: DELETE → removes both primary and legacy config files ───────────
|
||||
|
||||
test("codewhale-settings DELETE: removes primary and legacy config files", async () => {
|
||||
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "codewhale-home-del-"));
|
||||
const origHome = process.env.HOME;
|
||||
process.env.HOME = tmpHome;
|
||||
|
||||
try {
|
||||
const primaryDir = path.join(tmpHome, ".codewhale");
|
||||
const legacyDir = path.join(tmpHome, ".deepseek");
|
||||
fs.mkdirSync(primaryDir, { recursive: true });
|
||||
fs.mkdirSync(legacyDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(primaryDir, "config.toml"),
|
||||
'# managed by OmniRoute (plan 14)\n[openai]\nbase_url = "http://localhost:20128"\n'
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(legacyDir, "config.toml"),
|
||||
'# managed by OmniRoute (plan 14)\n[openai]\nbase_url = "http://localhost:20128"\n'
|
||||
);
|
||||
|
||||
const res = await DELETE(
|
||||
new Request("http://localhost/api/cli-tools/codewhale-settings", { method: "DELETE" })
|
||||
);
|
||||
assert.ok([200, 403, 500].includes(res.status), `Expected 200/403/500, got ${res.status}`);
|
||||
if (res.status === 200) {
|
||||
const body = await res.json();
|
||||
assert.equal(body.success, true);
|
||||
assert.ok(!fs.existsSync(path.join(primaryDir, "config.toml")), "Primary config removed");
|
||||
assert.ok(!fs.existsSync(path.join(legacyDir, "config.toml")), "Legacy config removed");
|
||||
}
|
||||
} finally {
|
||||
process.env.HOME = origHome;
|
||||
fs.rmSync(tmpHome, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
// ── Test 8: Error sanitization (Hard Rule #12) ───────────────────────────────
|
||||
|
||||
test("codewhale-settings: error responses do not leak stack traces", async () => {
|
||||
const badReq = new Request("http://localhost/api/cli-tools/codewhale-settings", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: "{ bad json }",
|
||||
});
|
||||
const res = await POST(badReq);
|
||||
const bodyStr = JSON.stringify(await res.json());
|
||||
assert.ok(
|
||||
!bodyStr.match(/\s+at\s+\/[^\s]/),
|
||||
"Error response must not contain absolute-path stack traces"
|
||||
);
|
||||
});
|
||||
|
||||
// ── Test 9: Hard Rule #13 (no exec/spawn) ────────────────────────────────────
|
||||
|
||||
test("codewhale-settings route.ts: does not call exec() or spawn() directly", () => {
|
||||
const routePath = path.resolve(
|
||||
import.meta.dirname,
|
||||
"../../src/app/api/cli-tools/codewhale-settings/route.ts"
|
||||
);
|
||||
const content = fs.readFileSync(routePath, "utf-8");
|
||||
assert.ok(!content.match(/\bexec\s*\(/), "Handler must not use exec()");
|
||||
assert.ok(!content.match(/\bspawn\s*\(/), "Handler must not use spawn()");
|
||||
});
|
||||
|
||||
test.after(async () => {
|
||||
await resetStorage();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
delete process.env.DATA_DIR;
|
||||
delete process.env.API_KEY_SECRET;
|
||||
delete process.env.JWT_SECRET;
|
||||
});
|
||||
@@ -13,10 +13,8 @@
|
||||
* Prerequisites when running for real:
|
||||
* - npm is in PATH
|
||||
* - Network access to registry.npmjs.org
|
||||
* - Ports 20130 (9router), 8317 (cliproxy), and 8080 (bifrost) are available
|
||||
* - Ports 20130 (9router) and 8317 (cliproxy) are available
|
||||
* - DATA_DIR is writable
|
||||
* - Bifrost lazily downloads its Go binary from downloads.getmaxim.ai on first
|
||||
* start, so the bifrost block additionally needs network access to that host.
|
||||
*/
|
||||
|
||||
import { describe, it } from "node:test";
|
||||
@@ -247,86 +245,6 @@ describe("cliproxy — full lifecycle (opt-in, RUN_SERVICES_INT=1)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// bifrost lifecycle
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("bifrost — full lifecycle (opt-in, RUN_SERVICES_INT=1)", () => {
|
||||
it("STEP 1: install bifrost (latest)", async (t) => {
|
||||
if (maybeSkip(t)) return;
|
||||
const { status, body } = await apiPost("/api/services/bifrost/install", {
|
||||
version: "latest",
|
||||
});
|
||||
assert.ok(
|
||||
status === 200,
|
||||
`Expected 200 from install, got ${status}: ${JSON.stringify(body).slice(0, 300)}`
|
||||
);
|
||||
const b = body as Record<string, unknown>;
|
||||
assert.ok(b.ok === true, "install response must have ok:true");
|
||||
assert.ok(
|
||||
typeof b.installedVersion === "string",
|
||||
"install response must have installedVersion"
|
||||
);
|
||||
assert.ok(typeof b.durationMs === "number", "install response must have durationMs");
|
||||
});
|
||||
|
||||
it("STEP 2: verify status is stopped after install", async (t) => {
|
||||
if (maybeSkip(t)) return;
|
||||
const { status, body } = await apiGet("/api/services/bifrost/status");
|
||||
assert.equal(status, 200);
|
||||
const b = body as Record<string, unknown>;
|
||||
assert.equal(b.state, "stopped", `Expected stopped state after install, got: ${b.state}`);
|
||||
assert.ok(
|
||||
typeof b.installedVersion === "string",
|
||||
"installedVersion should be set after install"
|
||||
);
|
||||
});
|
||||
|
||||
it("STEP 3: start bifrost", async (t) => {
|
||||
if (maybeSkip(t)) return;
|
||||
const { status, body } = await apiPost("/api/services/bifrost/start");
|
||||
assert.ok(
|
||||
status === 200,
|
||||
`Expected 200 from start, got ${status}: ${JSON.stringify(body).slice(0, 300)}`
|
||||
);
|
||||
const b = body as Record<string, unknown>;
|
||||
assert.ok(
|
||||
["starting", "running"].includes(b.state as string),
|
||||
`Expected starting or running, got: ${b.state}`
|
||||
);
|
||||
});
|
||||
|
||||
it("STEP 4: wait for bifrost to become healthy (≤60s — Go binary download on first start)", async (t) => {
|
||||
if (maybeSkip(t)) return;
|
||||
// First start lazily downloads the Go binary, so allow a longer window than
|
||||
// the 9router/cliproxy blocks (30s). Confirms open question §2b/§6 (Windows +
|
||||
// headless boot) against a real download.
|
||||
const finalStatus = await waitForState("/api/services/bifrost/status", "running", 60_000);
|
||||
const b = finalStatus as Record<string, unknown>;
|
||||
assert.equal(b.state, "running");
|
||||
assert.equal(b.health, "healthy");
|
||||
assert.ok(typeof b.pid === "number", "pid must be a number when running");
|
||||
assert.equal(b.port, 8080, "bifrost must report its default port 8080");
|
||||
});
|
||||
|
||||
it("STEP 5: stop bifrost", async (t) => {
|
||||
if (maybeSkip(t)) return;
|
||||
const { status, body } = await apiPost("/api/services/bifrost/stop");
|
||||
assert.equal(status, 200);
|
||||
const b = body as Record<string, unknown>;
|
||||
assert.ok(
|
||||
["stopping", "stopped"].includes(b.state as string),
|
||||
`Expected stopping or stopped, got: ${b.state}`
|
||||
);
|
||||
});
|
||||
|
||||
it("STEP 6: status returns stopped after stop", async (t) => {
|
||||
if (maybeSkip(t)) return;
|
||||
const final = await waitForState("/api/services/bifrost/status", "stopped", 15_000);
|
||||
assert.equal((final as Record<string, unknown>).state, "stopped");
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Security smoke (requires running server)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -52,18 +52,6 @@ describe("isLocalOnlyPath — /api/services/* and /dashboard/providers/services/
|
||||
assert.equal(isLocalOnlyPath("/api/services/cliproxy/status"), true);
|
||||
});
|
||||
|
||||
it("returns true for /api/services/bifrost/start", () => {
|
||||
assert.equal(isLocalOnlyPath("/api/services/bifrost/start"), true);
|
||||
});
|
||||
|
||||
it("returns true for /api/services/bifrost/install", () => {
|
||||
assert.equal(isLocalOnlyPath("/api/services/bifrost/install"), true);
|
||||
});
|
||||
|
||||
it("returns true for /api/services/bifrost/status", () => {
|
||||
assert.equal(isLocalOnlyPath("/api/services/bifrost/status"), true);
|
||||
});
|
||||
|
||||
it("returns true for /api/services/ (root prefix)", () => {
|
||||
assert.equal(isLocalOnlyPath("/api/services/"), true);
|
||||
});
|
||||
@@ -122,10 +110,6 @@ describe("isLocalOnlyBypassableByManageScope — /api/services/* is NOT bypassab
|
||||
it("returns false for /api/services/cliproxy/install", () => {
|
||||
assert.equal(isLocalOnlyBypassableByManageScope("/api/services/cliproxy/install"), false);
|
||||
});
|
||||
|
||||
it("returns false for /api/services/bifrost/install (spawn-capable)", () => {
|
||||
assert.equal(isLocalOnlyBypassableByManageScope("/api/services/bifrost/install"), false);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -33,10 +33,10 @@
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "claude-cli/2.1.195 (external, cli)",
|
||||
"X-App": "cli",
|
||||
"X-Stainless-Arch": "<ARCH>",
|
||||
"X-Stainless-Arch": "x64",
|
||||
"X-Stainless-Helper-Method": "stream",
|
||||
"X-Stainless-Lang": "js",
|
||||
"X-Stainless-Os": "<OS>",
|
||||
"X-Stainless-Os": "Linux",
|
||||
"X-Stainless-Package-Version": "0.94.0",
|
||||
"X-Stainless-Retry-Count": "0",
|
||||
"X-Stainless-Runtime": "node",
|
||||
@@ -51,10 +51,10 @@
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "claude-cli/2.1.195 (external, cli)",
|
||||
"X-App": "cli",
|
||||
"X-Stainless-Arch": "<ARCH>",
|
||||
"X-Stainless-Arch": "x64",
|
||||
"X-Stainless-Helper-Method": "stream",
|
||||
"X-Stainless-Lang": "js",
|
||||
"X-Stainless-Os": "<OS>",
|
||||
"X-Stainless-Os": "Linux",
|
||||
"X-Stainless-Package-Version": "0.94.0",
|
||||
"X-Stainless-Retry-Count": "0",
|
||||
"X-Stainless-Runtime": "node",
|
||||
@@ -70,10 +70,10 @@
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "claude-cli/2.1.195 (external, cli)",
|
||||
"X-App": "cli",
|
||||
"X-Stainless-Arch": "<ARCH>",
|
||||
"X-Stainless-Arch": "x64",
|
||||
"X-Stainless-Helper-Method": "stream",
|
||||
"X-Stainless-Lang": "js",
|
||||
"X-Stainless-Os": "<OS>",
|
||||
"X-Stainless-Os": "Linux",
|
||||
"X-Stainless-Package-Version": "0.94.0",
|
||||
"X-Stainless-Retry-Count": "0",
|
||||
"X-Stainless-Runtime": "node",
|
||||
@@ -94,18 +94,18 @@
|
||||
"Accept": "text/event-stream",
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "Antigravity/4.2.0 (<PLATFORM>) Chrome/142.0.7444.175 Electron/39.2.3"
|
||||
"User-Agent": "Antigravity/4.2.0 (X11; Linux x86_64) Chrome/142.0.7444.175 Electron/39.2.3"
|
||||
},
|
||||
"nonStream": {
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "Antigravity/4.2.0 (<PLATFORM>) Chrome/142.0.7444.175 Electron/39.2.3"
|
||||
"User-Agent": "Antigravity/4.2.0 (X11; Linux x86_64) Chrome/142.0.7444.175 Electron/39.2.3"
|
||||
},
|
||||
"oauth": {
|
||||
"Accept": "text/event-stream",
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "Antigravity/4.2.0 (<PLATFORM>) Chrome/142.0.7444.175 Electron/39.2.3"
|
||||
"User-Agent": "Antigravity/4.2.0 (X11; Linux x86_64) Chrome/142.0.7444.175 Electron/39.2.3"
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
@@ -241,18 +241,18 @@
|
||||
"Accept": "text/event-stream",
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "Antigravity/4.2.0 (<PLATFORM>) Chrome/142.0.7444.175 Electron/39.2.3"
|
||||
"User-Agent": "Antigravity/4.2.0 (X11; Linux x86_64) Chrome/142.0.7444.175 Electron/39.2.3"
|
||||
},
|
||||
"nonStream": {
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "Antigravity/4.2.0 (<PLATFORM>) Chrome/142.0.7444.175 Electron/39.2.3"
|
||||
"User-Agent": "Antigravity/4.2.0 (X11; Linux x86_64) Chrome/142.0.7444.175 Electron/39.2.3"
|
||||
},
|
||||
"oauth": {
|
||||
"Accept": "text/event-stream",
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "Antigravity/4.2.0 (<PLATFORM>) Chrome/142.0.7444.175 Electron/39.2.3"
|
||||
"User-Agent": "Antigravity/4.2.0 (X11; Linux x86_64) Chrome/142.0.7444.175 Electron/39.2.3"
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
@@ -289,52 +289,6 @@
|
||||
"stream": "https://api.airforce/v1/chat/completions"
|
||||
}
|
||||
},
|
||||
"auggie": {
|
||||
"format": "openai",
|
||||
"headers": {
|
||||
"apiKey": {
|
||||
"Accept": "text/event-stream",
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"nonStream": {
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"oauth": {
|
||||
"Accept": "text/event-stream",
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"nonStream": "auggie://cli/stdio",
|
||||
"stream": "auggie://cli/stdio"
|
||||
}
|
||||
},
|
||||
"bai": {
|
||||
"format": "openai",
|
||||
"headers": {
|
||||
"apiKey": {
|
||||
"Accept": "text/event-stream",
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"nonStream": {
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"oauth": {
|
||||
"Accept": "text/event-stream",
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"nonStream": "https://api.b.ai/v1/chat/completions",
|
||||
"stream": "https://api.b.ai/v1/chat/completions"
|
||||
}
|
||||
},
|
||||
"baichuan": {
|
||||
"format": "openai",
|
||||
"headers": {
|
||||
@@ -611,29 +565,6 @@
|
||||
"stream": "https://api.cerebras.ai/v1/chat/completions"
|
||||
}
|
||||
},
|
||||
"charm-hyper": {
|
||||
"format": "openai",
|
||||
"headers": {
|
||||
"apiKey": {
|
||||
"Accept": "text/event-stream",
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"nonStream": {
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"oauth": {
|
||||
"Accept": "text/event-stream",
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"nonStream": "https://hyper.charm.land/v1/chat/completions",
|
||||
"stream": "https://hyper.charm.land/v1/chat/completions"
|
||||
}
|
||||
},
|
||||
"chatgpt-web": {
|
||||
"format": "openai",
|
||||
"headers": {
|
||||
@@ -714,10 +645,10 @@
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "claude-cli/2.1.195 (external, cli)",
|
||||
"X-App": "cli",
|
||||
"X-Stainless-Arch": "<ARCH>",
|
||||
"X-Stainless-Arch": "x64",
|
||||
"X-Stainless-Helper-Method": "stream",
|
||||
"X-Stainless-Lang": "js",
|
||||
"X-Stainless-Os": "<OS>",
|
||||
"X-Stainless-Os": "Linux",
|
||||
"X-Stainless-Package-Version": "0.94.0",
|
||||
"X-Stainless-Retry-Count": "0",
|
||||
"X-Stainless-Runtime": "node",
|
||||
@@ -732,10 +663,10 @@
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "claude-cli/2.1.195 (external, cli)",
|
||||
"X-App": "cli",
|
||||
"X-Stainless-Arch": "<ARCH>",
|
||||
"X-Stainless-Arch": "x64",
|
||||
"X-Stainless-Helper-Method": "stream",
|
||||
"X-Stainless-Lang": "js",
|
||||
"X-Stainless-Os": "<OS>",
|
||||
"X-Stainless-Os": "Linux",
|
||||
"X-Stainless-Package-Version": "0.94.0",
|
||||
"X-Stainless-Retry-Count": "0",
|
||||
"X-Stainless-Runtime": "node",
|
||||
@@ -751,10 +682,10 @@
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "claude-cli/2.1.195 (external, cli)",
|
||||
"X-App": "cli",
|
||||
"X-Stainless-Arch": "<ARCH>",
|
||||
"X-Stainless-Arch": "x64",
|
||||
"X-Stainless-Helper-Method": "stream",
|
||||
"X-Stainless-Lang": "js",
|
||||
"X-Stainless-Os": "<OS>",
|
||||
"X-Stainless-Os": "Linux",
|
||||
"X-Stainless-Package-Version": "0.94.0",
|
||||
"X-Stainless-Retry-Count": "0",
|
||||
"X-Stainless-Runtime": "node",
|
||||
@@ -841,35 +772,6 @@
|
||||
"stream": "https://api.cline.bot/api/v1/chat/completions"
|
||||
}
|
||||
},
|
||||
"clinepass": {
|
||||
"format": "openai",
|
||||
"headers": {
|
||||
"apiKey": {
|
||||
"Accept": "text/event-stream",
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json",
|
||||
"HTTP-Referer": "https://cline.bot",
|
||||
"X-Title": "Cline"
|
||||
},
|
||||
"nonStream": {
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json",
|
||||
"HTTP-Referer": "https://cline.bot",
|
||||
"X-Title": "Cline"
|
||||
},
|
||||
"oauth": {
|
||||
"Accept": "text/event-stream",
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json",
|
||||
"HTTP-Referer": "https://cline.bot",
|
||||
"X-Title": "Cline"
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"nonStream": "https://api.cline.bot/api/v1/chat/completions",
|
||||
"stream": "https://api.cline.bot/api/v1/chat/completions"
|
||||
}
|
||||
},
|
||||
"cloudflare-ai": {
|
||||
"format": "openai",
|
||||
"headers": {
|
||||
@@ -965,7 +867,7 @@
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json",
|
||||
"Openai-Beta": "responses=experimental",
|
||||
"User-Agent": "codex-cli/0.142.0 (<OS>; <ARCH>)",
|
||||
"User-Agent": "codex-cli/0.142.0 (Windows 10.0.26200; x64)",
|
||||
"Version": "0.142.0",
|
||||
"X-Codex-Beta-Features": "responses_websockets"
|
||||
},
|
||||
@@ -973,7 +875,7 @@
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json",
|
||||
"Openai-Beta": "responses=experimental",
|
||||
"User-Agent": "codex-cli/0.142.0 (<OS>; <ARCH>)",
|
||||
"User-Agent": "codex-cli/0.142.0 (Windows 10.0.26200; x64)",
|
||||
"Version": "0.142.0",
|
||||
"X-Codex-Beta-Features": "responses_websockets"
|
||||
},
|
||||
@@ -982,7 +884,7 @@
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json",
|
||||
"Openai-Beta": "responses=experimental",
|
||||
"User-Agent": "codex-cli/0.142.0 (<OS>; <ARCH>)",
|
||||
"User-Agent": "codex-cli/0.142.0 (Windows 10.0.26200; x64)",
|
||||
"Version": "0.142.0",
|
||||
"X-Codex-Beta-Features": "responses_websockets"
|
||||
}
|
||||
@@ -1754,14 +1656,14 @@
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json",
|
||||
"HTTP-Referer": "https://github.com/Gitlawb/openclaude",
|
||||
"User-Agent": "OpenClaude/1.0 (<OS>; <ARCH>)",
|
||||
"User-Agent": "OpenClaude/1.0 (linux; x86_64)",
|
||||
"X-Title": "OpenClaude CLI"
|
||||
},
|
||||
"nonStream": {
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json",
|
||||
"HTTP-Referer": "https://github.com/Gitlawb/openclaude",
|
||||
"User-Agent": "OpenClaude/1.0 (<OS>; <ARCH>)",
|
||||
"User-Agent": "OpenClaude/1.0 (linux; x86_64)",
|
||||
"X-Title": "OpenClaude CLI"
|
||||
},
|
||||
"oauth": {
|
||||
@@ -1769,7 +1671,7 @@
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json",
|
||||
"HTTP-Referer": "https://github.com/Gitlawb/openclaude",
|
||||
"User-Agent": "OpenClaude/1.0 (<OS>; <ARCH>)",
|
||||
"User-Agent": "OpenClaude/1.0 (linux; x86_64)",
|
||||
"X-Title": "OpenClaude CLI"
|
||||
}
|
||||
},
|
||||
@@ -1786,14 +1688,14 @@
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json",
|
||||
"HTTP-Referer": "https://github.com/Gitlawb/openclaude",
|
||||
"User-Agent": "OpenClaude/1.0 (<OS>; <ARCH>)",
|
||||
"User-Agent": "OpenClaude/1.0 (linux; x86_64)",
|
||||
"X-Title": "OpenClaude CLI"
|
||||
},
|
||||
"nonStream": {
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json",
|
||||
"HTTP-Referer": "https://github.com/Gitlawb/openclaude",
|
||||
"User-Agent": "OpenClaude/1.0 (<OS>; <ARCH>)",
|
||||
"User-Agent": "OpenClaude/1.0 (linux; x86_64)",
|
||||
"X-Title": "OpenClaude CLI"
|
||||
},
|
||||
"oauth": {
|
||||
@@ -1801,7 +1703,7 @@
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json",
|
||||
"HTTP-Referer": "https://github.com/Gitlawb/openclaude",
|
||||
"User-Agent": "OpenClaude/1.0 (<OS>; <ARCH>)",
|
||||
"User-Agent": "OpenClaude/1.0 (linux; x86_64)",
|
||||
"X-Title": "OpenClaude CLI"
|
||||
}
|
||||
},
|
||||
@@ -2755,29 +2657,6 @@
|
||||
"stream": "https://api.modal.ai/v1/chat/completions"
|
||||
}
|
||||
},
|
||||
"modelscope": {
|
||||
"format": "openai",
|
||||
"headers": {
|
||||
"apiKey": {
|
||||
"Accept": "text/event-stream",
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"nonStream": {
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"oauth": {
|
||||
"Accept": "text/event-stream",
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"nonStream": "https://api-inference.modelscope.cn/v1/chat/completions",
|
||||
"stream": "https://api-inference.modelscope.cn/v1/chat/completions"
|
||||
}
|
||||
},
|
||||
"monsterapi": {
|
||||
"format": "openai",
|
||||
"headers": {
|
||||
@@ -3008,29 +2887,6 @@
|
||||
"stream": "https://inference.api.nscale.com/v1/chat/completions"
|
||||
}
|
||||
},
|
||||
"nube": {
|
||||
"format": "openai",
|
||||
"headers": {
|
||||
"apiKey": {
|
||||
"Accept": "text/event-stream",
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"nonStream": {
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"oauth": {
|
||||
"Accept": "text/event-stream",
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"nonStream": "https://ai.nube.sh/api/v1/chat/completions",
|
||||
"stream": "https://ai.nube.sh/api/v1/chat/completions"
|
||||
}
|
||||
},
|
||||
"nvidia": {
|
||||
"format": "openai",
|
||||
"headers": {
|
||||
@@ -3457,29 +3313,6 @@
|
||||
"stream": "https://qianfan.baidubce.com/v2/chat/completions"
|
||||
}
|
||||
},
|
||||
"qiniu": {
|
||||
"format": "openai",
|
||||
"headers": {
|
||||
"apiKey": {
|
||||
"Accept": "text/event-stream",
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"nonStream": {
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"oauth": {
|
||||
"Accept": "text/event-stream",
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"nonStream": "https://api.qnaigc.com/v1/chat/completions",
|
||||
"stream": "https://api.qnaigc.com/v1/chat/completions"
|
||||
}
|
||||
},
|
||||
"qoder": {
|
||||
"format": "openai",
|
||||
"headers": {
|
||||
@@ -3516,13 +3349,13 @@
|
||||
"Connection": "keep-alive",
|
||||
"Content-Type": "application/json",
|
||||
"Sec-Fetch-Mode": "cors",
|
||||
"User-Agent": "QwenCode/0.19.3 (<OS>; <ARCH>)",
|
||||
"User-Agent": "QwenCode/0.19.3 (linux; x64)",
|
||||
"X-Dashscope-AuthType": "qwen-oauth",
|
||||
"X-Dashscope-CacheControl": "enable",
|
||||
"X-Dashscope-UserAgent": "QwenCode/0.19.3 (<OS>; <ARCH>)",
|
||||
"X-Stainless-Arch": "<ARCH>",
|
||||
"X-Dashscope-UserAgent": "QwenCode/0.19.3 (linux; x64)",
|
||||
"X-Stainless-Arch": "x64",
|
||||
"X-Stainless-Lang": "js",
|
||||
"X-Stainless-Os": "<OS>",
|
||||
"X-Stainless-Os": "Linux",
|
||||
"X-Stainless-Package-Version": "5.11.0",
|
||||
"X-Stainless-Retry-Count": "1",
|
||||
"X-Stainless-Runtime": "node",
|
||||
@@ -3534,13 +3367,13 @@
|
||||
"Connection": "keep-alive",
|
||||
"Content-Type": "application/json",
|
||||
"Sec-Fetch-Mode": "cors",
|
||||
"User-Agent": "QwenCode/0.19.3 (<OS>; <ARCH>)",
|
||||
"User-Agent": "QwenCode/0.19.3 (linux; x64)",
|
||||
"X-Dashscope-AuthType": "qwen-oauth",
|
||||
"X-Dashscope-CacheControl": "enable",
|
||||
"X-Dashscope-UserAgent": "QwenCode/0.19.3 (<OS>; <ARCH>)",
|
||||
"X-Stainless-Arch": "<ARCH>",
|
||||
"X-Dashscope-UserAgent": "QwenCode/0.19.3 (linux; x64)",
|
||||
"X-Stainless-Arch": "x64",
|
||||
"X-Stainless-Lang": "js",
|
||||
"X-Stainless-Os": "<OS>",
|
||||
"X-Stainless-Os": "Linux",
|
||||
"X-Stainless-Package-Version": "5.11.0",
|
||||
"X-Stainless-Retry-Count": "1",
|
||||
"X-Stainless-Runtime": "node",
|
||||
@@ -3553,13 +3386,13 @@
|
||||
"Connection": "keep-alive",
|
||||
"Content-Type": "application/json",
|
||||
"Sec-Fetch-Mode": "cors",
|
||||
"User-Agent": "QwenCode/0.19.3 (<OS>; <ARCH>)",
|
||||
"User-Agent": "QwenCode/0.19.3 (linux; x64)",
|
||||
"X-Dashscope-AuthType": "qwen-oauth",
|
||||
"X-Dashscope-CacheControl": "enable",
|
||||
"X-Dashscope-UserAgent": "QwenCode/0.19.3 (<OS>; <ARCH>)",
|
||||
"X-Stainless-Arch": "<ARCH>",
|
||||
"X-Dashscope-UserAgent": "QwenCode/0.19.3 (linux; x64)",
|
||||
"X-Stainless-Arch": "x64",
|
||||
"X-Stainless-Lang": "js",
|
||||
"X-Stainless-Os": "<OS>",
|
||||
"X-Stainless-Os": "Linux",
|
||||
"X-Stainless-Package-Version": "5.11.0",
|
||||
"X-Stainless-Retry-Count": "1",
|
||||
"X-Stainless-Runtime": "node",
|
||||
@@ -3778,29 +3611,6 @@
|
||||
"stream": "https://api.stepfun.com/v1/chat/completions"
|
||||
}
|
||||
},
|
||||
"sumopod": {
|
||||
"format": "openai",
|
||||
"headers": {
|
||||
"apiKey": {
|
||||
"Accept": "text/event-stream",
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"nonStream": {
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"oauth": {
|
||||
"Accept": "text/event-stream",
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"nonStream": "https://ai.sumopod.com/v1/chat/completions",
|
||||
"stream": "https://ai.sumopod.com/v1/chat/completions"
|
||||
}
|
||||
},
|
||||
"suno": {
|
||||
"format": "openai",
|
||||
"headers": {
|
||||
@@ -4287,29 +4097,6 @@
|
||||
"stream": "https://server.self-serve.windsurf.com"
|
||||
}
|
||||
},
|
||||
"x5lab": {
|
||||
"format": "openai",
|
||||
"headers": {
|
||||
"apiKey": {
|
||||
"Accept": "text/event-stream",
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"nonStream": {
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"oauth": {
|
||||
"Accept": "text/event-stream",
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"nonStream": "https://api.x5lab.dev/v1/chat/completions",
|
||||
"stream": "https://api.x5lab.dev/v1/chat/completions"
|
||||
}
|
||||
},
|
||||
"xai": {
|
||||
"format": "openai",
|
||||
"headers": {
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const { parseRetryFromErrorText } = await import("../../open-sse/services/accountFallback.ts");
|
||||
|
||||
test("parseRetryFromErrorText reads nested ISO retryAfter from a 429 JSON body", () => {
|
||||
const futureIso = new Date(Date.now() + 120_000).toISOString();
|
||||
const waitMs = parseRetryFromErrorText(JSON.stringify({ error: { retryAfter: futureIso } }));
|
||||
assert.ok(waitMs !== null, "expected a parsed wait time, got null");
|
||||
assert.ok(Math.abs((waitMs as number) - 120_000) <= 2_000, `expected ~120000ms, got ${waitMs}`);
|
||||
});
|
||||
|
||||
test("parseRetryFromErrorText reads top-level retryAfter when nested field is absent", () => {
|
||||
const futureIso = new Date(Date.now() + 60_000).toISOString();
|
||||
const waitMs = parseRetryFromErrorText(JSON.stringify({ retryAfter: futureIso }));
|
||||
assert.ok(waitMs !== null, "expected a parsed wait time, got null");
|
||||
assert.ok(Math.abs((waitMs as number) - 60_000) <= 2_000, `expected ~60000ms, got ${waitMs}`);
|
||||
});
|
||||
|
||||
test("parseRetryFromErrorText reads millisecond retry hints from 429 JSON bodies", () => {
|
||||
assert.equal(parseRetryFromErrorText(JSON.stringify({ retry_after_ms: 45_000 })), 45_000);
|
||||
assert.equal(
|
||||
parseRetryFromErrorText(JSON.stringify({ error: { retry_after_ms: 12_000 } })),
|
||||
12_000
|
||||
);
|
||||
assert.equal(parseRetryFromErrorText(JSON.stringify({ retryAfterMs: 8_000 })), 8_000);
|
||||
});
|
||||
|
||||
test("parseRetryFromErrorText ignores past ISO retryAfter values in JSON bodies", () => {
|
||||
const pastIso = new Date(Date.now() - 60_000).toISOString();
|
||||
assert.equal(parseRetryFromErrorText(JSON.stringify({ error: { retryAfter: pastIso } })), null);
|
||||
});
|
||||
@@ -1,44 +0,0 @@
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
const modalPath =
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditConnectionModal.tsx";
|
||||
const source = readFileSync(modalPath, "utf8");
|
||||
|
||||
describe("agy Project ID UI support", () => {
|
||||
it("declares a single Antigravity-family provider gate", () => {
|
||||
assert.ok(
|
||||
source.includes(
|
||||
'const isAntigravityFamily = provider === "antigravity" || provider === "agy";'
|
||||
),
|
||||
"isAntigravityFamily must include antigravity and agy without a separate Google Project ID gate"
|
||||
);
|
||||
});
|
||||
|
||||
it("does not keep the old supportsGoogleProjectId alias", () => {
|
||||
assert.equal(source.includes("supportsGoogleProjectId"), false);
|
||||
});
|
||||
|
||||
it("uses antigravityProjectIdLabel for Antigravity-family providers", () => {
|
||||
assert.ok(
|
||||
source.includes('label={t("antigravityProjectIdLabel")}'),
|
||||
"projectId label must use Antigravity-family copy"
|
||||
);
|
||||
});
|
||||
|
||||
it("uses isAntigravityFamily for antigravityClientProfile UI", () => {
|
||||
assert.ok(
|
||||
source.includes("{isAntigravityFamily && (\n <div") &&
|
||||
source.includes('label={t("antigravityClientProfileLabel")}'),
|
||||
"client profile Select must render for isAntigravityFamily"
|
||||
);
|
||||
});
|
||||
|
||||
it("uses isAntigravityFamily for client profile save", () => {
|
||||
assert.ok(
|
||||
source.includes("if (isAntigravityFamily) {"),
|
||||
"client profile save must use isAntigravityFamily"
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,71 +0,0 @@
|
||||
/**
|
||||
* Regression for #5899 (PR #5920): the OpenAI-compatible models-discovery URL
|
||||
* builder must strip a trailing `/v1` UNCONDITIONALLY before appending
|
||||
* `/v1/models`. A gateway baseUrl like ".../v1/chat/completions" was reduced to
|
||||
* ".../v1" (the old `else if` skipped the /v1 strip once `/chat/completions`
|
||||
* matched) and then produced ".../v1/v1/models" — a 308 redirect that blocked
|
||||
* model discovery. The fix converts the `/v1` strip to an independent `if`
|
||||
* (guarding against a literal "scheme://v1" authority) in BOTH the general
|
||||
* discovery path and the `provider === "openai"` custom-base-URL path.
|
||||
*/
|
||||
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-5899-"));
|
||||
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 modelsRoute = await import("../../src/app/api/providers/[id]/models/route.ts");
|
||||
|
||||
test.after(() => {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("#5899 openai gateway baseUrl ending in /v1/chat/completions never probes /v1/v1/models", async () => {
|
||||
const connection = await providersDb.createProviderConnection({
|
||||
provider: "openai",
|
||||
authType: "apikey",
|
||||
name: "airforce-gateway",
|
||||
apiKey: "sk-airforce",
|
||||
providerSpecificData: { baseUrl: "https://api.airforce/v1/chat/completions" },
|
||||
});
|
||||
|
||||
const requestedUrls: string[] = [];
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = async (url) => {
|
||||
const u = String(url);
|
||||
requestedUrls.push(u);
|
||||
// The correctly-stripped candidate must be the one that serves models.
|
||||
if (u === "https://api.airforce/v1/models") {
|
||||
return Response.json({ object: "list", data: [{ id: "gpt-4o" }, { id: "gpt-5" }] });
|
||||
}
|
||||
// The double-prefixed URL upstream answered with a 308 redirect (#5899).
|
||||
if (u === "https://api.airforce/v1/v1/models") {
|
||||
return new Response(null, { status: 308, headers: { location: u } });
|
||||
}
|
||||
return new Response("not found", { status: 404 });
|
||||
};
|
||||
|
||||
try {
|
||||
await modelsRoute.GET(
|
||||
new Request(`http://localhost/api/providers/${connection.id}/models?refresh=true`),
|
||||
{ params: { id: connection.id } }
|
||||
);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
|
||||
assert.ok(
|
||||
requestedUrls.includes("https://api.airforce/v1/models"),
|
||||
`expected a request to the correctly-stripped /v1/models URL; got: ${JSON.stringify(requestedUrls)}`
|
||||
);
|
||||
assert.ok(
|
||||
!requestedUrls.includes("https://api.airforce/v1/v1/models"),
|
||||
`must never probe the double-prefixed /v1/v1/models URL; got: ${JSON.stringify(requestedUrls)}`
|
||||
);
|
||||
});
|
||||
@@ -1,48 +0,0 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
// Split-guard for the antigravity executor SSE-collect extraction.
|
||||
// The pure SSE-payload -> collected-stream parser lives in antigravity/sseCollect.ts
|
||||
// (no host state, no fetch/auth). Host imports the helpers it uses and re-exports
|
||||
// processAntigravitySSEPayload for external importers (tests).
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const EXE = join(HERE, "../../open-sse/executors");
|
||||
const HOST = join(EXE, "antigravity.ts");
|
||||
const LEAF = join(EXE, "antigravity/sseCollect.ts");
|
||||
|
||||
test("leaf hosts the SSE-collect helpers and does not import the host", () => {
|
||||
const src = readFileSync(LEAF, "utf8");
|
||||
for (const sym of [
|
||||
"processAntigravitySSEPayload",
|
||||
"processAntigravitySSEText",
|
||||
"flushAntigravitySSEText",
|
||||
"stripZeroWidth",
|
||||
]) {
|
||||
assert.match(src, new RegExp(`export (function|type) ${sym}\\b`));
|
||||
}
|
||||
assert.doesNotMatch(src, /from "\.\.\/antigravity\.ts"/);
|
||||
});
|
||||
|
||||
test("host re-exports processAntigravitySSEPayload", () => {
|
||||
const host = readFileSync(HOST, "utf8");
|
||||
assert.match(
|
||||
host,
|
||||
/export \{ processAntigravitySSEPayload \} from "\.\/antigravity\/sseCollect\.ts"/
|
||||
);
|
||||
assert.match(host, /from "\.\/antigravity\/sseCollect\.ts"/);
|
||||
});
|
||||
|
||||
test("SSE-collect helpers are callable and tolerate empty/garbage input", async () => {
|
||||
const { processAntigravitySSEPayload, stripZeroWidth } =
|
||||
await import("../../open-sse/executors/antigravity/sseCollect.ts");
|
||||
assert.equal(typeof processAntigravitySSEPayload, "function");
|
||||
// stripZeroWidth removes zero-width markers from strings, passes through non-strings.
|
||||
assert.equal(stripZeroWidth("ab"), "ab");
|
||||
assert.deepEqual(stripZeroWidth(42), 42);
|
||||
// A malformed payload must not throw (defensive parse).
|
||||
const collected = { textContent: "" };
|
||||
assert.doesNotThrow(() => processAntigravitySSEPayload("not-json", collected));
|
||||
});
|
||||
@@ -1,127 +0,0 @@
|
||||
/**
|
||||
* Regression test for #6026.
|
||||
*
|
||||
* Antigravity IDE (via AgentBridge/MITM → `/v1/antigravity` → translator) can ship a
|
||||
* truncated history whose FIRST turn is a tool result (`functionResponse`) with no
|
||||
* preceding tool call. When that survives the antigravity→openai→claude chain, Anthropic
|
||||
* (Vertex `claude-opus-4.6`) rejects it with HTTP 400:
|
||||
*
|
||||
* messages.0.content.1: unexpected tool_use_id found in tool_result blocks:
|
||||
* toolu_vrtx_...: Each tool_result block must have a corresponding tool_use block in
|
||||
* the previous message.
|
||||
*
|
||||
* The fix strips orphan tool_results at the antigravity message-assembly point
|
||||
* (`antigravityToOpenAIRequest`) by reusing `fixToolPairs`, so the orphan never reaches
|
||||
* the upstream Claude request.
|
||||
*
|
||||
* PURE-FUNCTION ONLY — this test imports the translator + sanitizer functions directly.
|
||||
* It must NEVER start the MITM proxy, bind :443/:80, touch /etc/hosts, or install a CA.
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const { antigravityToOpenAIRequest } = await import(
|
||||
"../../open-sse/translator/request/antigravity-to-openai.ts"
|
||||
);
|
||||
const { fixToolPairs } = await import("../../open-sse/services/contextManager.ts");
|
||||
|
||||
test("#6026: antigravityToOpenAIRequest strips an orphan functionResponse (no preceding functionCall)", () => {
|
||||
const result = antigravityToOpenAIRequest(
|
||||
"ag/claude-opus-4-6",
|
||||
{
|
||||
request: {
|
||||
contents: [
|
||||
{
|
||||
// First (and only) turn is a tool result with NO preceding tool call.
|
||||
role: "user",
|
||||
parts: [
|
||||
{
|
||||
functionResponse: {
|
||||
id: "toolu_vrtx_test",
|
||||
name: "read_file",
|
||||
response: { result: { ok: true } },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
// The orphan tool message must be gone — otherwise the openai→claude step would emit an
|
||||
// orphan tool_result block and Anthropic would 400.
|
||||
const orphan = result.messages.find(
|
||||
(m: Record<string, unknown>) => m.role === "tool" && m.tool_call_id === "toolu_vrtx_test"
|
||||
);
|
||||
assert.equal(orphan, undefined, "orphan tool_result message must be stripped");
|
||||
assert.equal(
|
||||
result.messages.some((m: Record<string, unknown>) => m.role === "tool"),
|
||||
false,
|
||||
"no orphan tool messages should remain"
|
||||
);
|
||||
});
|
||||
|
||||
test("#6026: well-formed functionCall/functionResponse pair is preserved (no regression)", () => {
|
||||
const result = antigravityToOpenAIRequest(
|
||||
"ag/claude-opus-4-6",
|
||||
{
|
||||
request: {
|
||||
contents: [
|
||||
{
|
||||
role: "model",
|
||||
parts: [{ functionCall: { id: "toolu_vrtx_ok", name: "read_file", args: {} } }],
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
parts: [
|
||||
{
|
||||
functionResponse: {
|
||||
id: "toolu_vrtx_ok",
|
||||
name: "read_file",
|
||||
response: { result: { ok: true } },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
const assistant = result.messages.find(
|
||||
(m: Record<string, unknown>) => m.role === "assistant"
|
||||
);
|
||||
const tool = result.messages.find((m: Record<string, unknown>) => m.role === "tool");
|
||||
assert.ok(assistant, "assistant tool_call message must survive");
|
||||
assert.ok(tool, "matched tool_result message must survive");
|
||||
assert.equal((tool as Record<string, unknown>).tool_call_id, "toolu_vrtx_ok");
|
||||
});
|
||||
|
||||
test("#6026: fixToolPairs removes the exact Anthropic-shape orphan tool_result block", () => {
|
||||
// Mirrors the reporter's failing body: messages[0] is a user message whose content array
|
||||
// holds a tool_result block with no matching tool_use anywhere in the request.
|
||||
const messages: Record<string, unknown>[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: "continue" },
|
||||
{ type: "tool_result", tool_use_id: "toolu_vrtx_test", content: "stale" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const fixed = fixToolPairs(messages);
|
||||
|
||||
const stillHasOrphan = fixed.some(
|
||||
(m) =>
|
||||
m.role === "user" &&
|
||||
Array.isArray(m.content) &&
|
||||
(m.content as Record<string, unknown>[]).some(
|
||||
(b) => b.type === "tool_result" && b.tool_use_id === "toolu_vrtx_test"
|
||||
)
|
||||
);
|
||||
assert.equal(stillHasOrphan, false, "orphan tool_result block must be stripped");
|
||||
});
|
||||
@@ -27,12 +27,7 @@ test("chat handler maps API key provider quota bypass scope to auth bypass optio
|
||||
});
|
||||
|
||||
test("auto combo disables hard provider quota cutoffs when relay requests bypass", () => {
|
||||
// The auto-strategy bypass logic was extracted verbatim from combo.ts into the
|
||||
// resolveAutoStrategy leaf (Block J Task 2); the source scan follows the code.
|
||||
const source = fs.readFileSync(
|
||||
path.join(repoRoot, "open-sse/services/combo/resolveAutoStrategy.ts"),
|
||||
"utf8"
|
||||
);
|
||||
const source = fs.readFileSync(path.join(repoRoot, "open-sse/services/combo.ts"), "utf8");
|
||||
|
||||
assert.match(source, /relayOptions\?\.bypassProviderQuotaPolicy === true/);
|
||||
assert.match(source, /quotaPreflight:[\s\S]*enabled: false/);
|
||||
|
||||
@@ -19,7 +19,6 @@ process.env.OMNIROUTE_DISABLE_REDIS_AUTH_CACHE = "1";
|
||||
|
||||
const ORIGINAL_JWT_SECRET = process.env.JWT_SECRET;
|
||||
const ORIGINAL_INITIAL_PASSWORD = process.env.INITIAL_PASSWORD;
|
||||
const ORIGINAL_CORS_ALLOW_ALL = process.env.CORS_ALLOW_ALL;
|
||||
|
||||
const core = await import("../../../src/lib/db/core.ts");
|
||||
const settingsDb = await import("../../../src/lib/db/settings.ts");
|
||||
@@ -40,8 +39,6 @@ test.after(() => {
|
||||
else process.env.JWT_SECRET = ORIGINAL_JWT_SECRET;
|
||||
if (ORIGINAL_INITIAL_PASSWORD === undefined) delete process.env.INITIAL_PASSWORD;
|
||||
else process.env.INITIAL_PASSWORD = ORIGINAL_INITIAL_PASSWORD;
|
||||
if (ORIGINAL_CORS_ALLOW_ALL === undefined) delete process.env.CORS_ALLOW_ALL;
|
||||
else process.env.CORS_ALLOW_ALL = ORIGINAL_CORS_ALLOW_ALL;
|
||||
});
|
||||
|
||||
// ─── AC-1 — shape ─────────────────────────────────────────────────────────
|
||||
@@ -156,44 +153,6 @@ test("AC-2: bypassPrefixes additions land in the inventory", async () => {
|
||||
assert.deepEqual(body.bypassPrefixes, ["/api/mcp/", "/api/mcp/v2/"]);
|
||||
});
|
||||
|
||||
// ─── #5602 — CORS status surfaced for the dashboard wildcard warning ──────
|
||||
|
||||
test("#5602: cors.allowAll is false by default (no CORS_ALLOW_ALL)", async () => {
|
||||
process.env.JWT_SECRET = "test-jwt-secret-authz-inventory";
|
||||
process.env.INITIAL_PASSWORD = "initial-pass-cors-a";
|
||||
delete process.env.CORS_ALLOW_ALL;
|
||||
await settingsDb.updateSettings({ requireLogin: true });
|
||||
|
||||
const response = await inventoryRoute.GET(
|
||||
await makeManagementSessionRequest("http://localhost/api/settings/authz-inventory", {
|
||||
method: "GET",
|
||||
})
|
||||
);
|
||||
assert.equal(response.status, 200);
|
||||
const body = (await response.json()) as {
|
||||
cors: { allowAll: boolean; allowedOrigins: string[] };
|
||||
};
|
||||
assert.ok(body.cors, "response should carry a cors envelope");
|
||||
assert.equal(body.cors.allowAll, false);
|
||||
assert.deepEqual(body.cors.allowedOrigins, []);
|
||||
});
|
||||
|
||||
test("#5602: cors.allowAll reflects CORS_ALLOW_ALL=true", async () => {
|
||||
process.env.JWT_SECRET = "test-jwt-secret-authz-inventory";
|
||||
process.env.INITIAL_PASSWORD = "initial-pass-cors-b";
|
||||
process.env.CORS_ALLOW_ALL = "true";
|
||||
await settingsDb.updateSettings({ requireLogin: true });
|
||||
|
||||
const response = await inventoryRoute.GET(
|
||||
await makeManagementSessionRequest("http://localhost/api/settings/authz-inventory", {
|
||||
method: "GET",
|
||||
})
|
||||
);
|
||||
assert.equal(response.status, 200);
|
||||
const body = (await response.json()) as { cors: { allowAll: boolean } };
|
||||
assert.equal(body.cors.allowAll, true);
|
||||
});
|
||||
|
||||
// ─── AC-12 — anonymous request rejected (no inventory leak) ───────────────
|
||||
|
||||
test("AC-12: anonymous request (no cookie, no Bearer) → 401", async () => {
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
import { describe, test, before, after } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
// Isolated DB + auth disabled so requireManagementAuth passes (no key configured).
|
||||
let tmpDataDir: string;
|
||||
let core: typeof import("@/lib/db/core");
|
||||
let db: typeof import("@/lib/db/discoveryResults");
|
||||
let resultsRoute: typeof import("@/app/api/discovery/results/route");
|
||||
let resultByIdRoute: typeof import("@/app/api/discovery/results/[id]/route");
|
||||
let scanRoute: typeof import("@/app/api/discovery/scan/route");
|
||||
let verifyRoute: typeof import("@/app/api/discovery/verify/[id]/route");
|
||||
|
||||
function req(method: string, url: string, body?: unknown): Request {
|
||||
return new Request(`http://localhost${url}`, {
|
||||
method,
|
||||
headers: { "content-type": "application/json" },
|
||||
body: body === undefined ? undefined : JSON.stringify(body),
|
||||
});
|
||||
}
|
||||
|
||||
before(async () => {
|
||||
tmpDataDir = mkdtempSync(join(tmpdir(), "omniroute-discovery-routes-"));
|
||||
process.env.DATA_DIR = tmpDataDir;
|
||||
delete process.env.REQUIRE_API_KEY;
|
||||
process.env.OMNIROUTE_DISABLE_AUTH = "1";
|
||||
core = await import("@/lib/db/core");
|
||||
core.resetDbInstance();
|
||||
core.getDbInstance();
|
||||
db = await import("@/lib/db/discoveryResults");
|
||||
resultsRoute = await import("@/app/api/discovery/results/route");
|
||||
resultByIdRoute = await import("@/app/api/discovery/results/[id]/route");
|
||||
scanRoute = await import("@/app/api/discovery/scan/route");
|
||||
verifyRoute = await import("@/app/api/discovery/verify/[id]/route");
|
||||
});
|
||||
|
||||
after(() => {
|
||||
core.resetDbInstance();
|
||||
if (tmpDataDir) rmSync(tmpDataDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe("discovery API routes", () => {
|
||||
test("GET /results lists persisted findings (and filters by providerId)", async () => {
|
||||
db.upsertDiscoveryResult({
|
||||
providerId: "acme",
|
||||
method: "free_tier",
|
||||
authType: "none",
|
||||
feasibility: 3,
|
||||
riskLevel: "none",
|
||||
status: "pending",
|
||||
});
|
||||
const res = await resultsRoute.GET(req("GET", "/api/discovery/results?providerId=acme"));
|
||||
assert.equal(res.status, 200);
|
||||
const body = await res.json();
|
||||
assert.ok(Array.isArray(body.results));
|
||||
assert.ok(body.results.some((r: { providerId: string }) => r.providerId === "acme"));
|
||||
});
|
||||
|
||||
test("GET /results/:id returns the row, 404 when missing, 400 on bad id", async () => {
|
||||
const created = db.upsertDiscoveryResult({
|
||||
providerId: "beta",
|
||||
method: "trial",
|
||||
authType: "api_key",
|
||||
feasibility: 2,
|
||||
riskLevel: "low",
|
||||
status: "pending",
|
||||
});
|
||||
const ok = await resultByIdRoute.GET(req("GET", `/api/discovery/results/${created.id}`), {
|
||||
params: Promise.resolve({ id: String(created.id) }),
|
||||
});
|
||||
assert.equal(ok.status, 200);
|
||||
|
||||
const missing = await resultByIdRoute.GET(req("GET", "/api/discovery/results/999999"), {
|
||||
params: Promise.resolve({ id: "999999" }),
|
||||
});
|
||||
assert.equal(missing.status, 404);
|
||||
|
||||
const bad = await resultByIdRoute.GET(req("GET", "/api/discovery/results/abc"), {
|
||||
params: Promise.resolve({ id: "abc" }),
|
||||
});
|
||||
assert.equal(bad.status, 400);
|
||||
});
|
||||
|
||||
test("POST /scan persists findings; rejects an empty providerId with 400", async () => {
|
||||
const res = await scanRoute.POST(req("POST", "/api/discovery/scan", { providerId: "gamma" }));
|
||||
assert.equal(res.status, 200);
|
||||
const body = await res.json();
|
||||
assert.ok(Array.isArray(body.results) && body.results.length > 0);
|
||||
assert.ok(body.results[0].id > 0);
|
||||
// the persisted row is now queryable
|
||||
assert.ok(db.getDiscoveryResults("gamma").length > 0);
|
||||
|
||||
const invalid = await scanRoute.POST(req("POST", "/api/discovery/scan", { providerId: "" }));
|
||||
assert.equal(invalid.status, 400);
|
||||
|
||||
const malformed = new Request("http://localhost/api/discovery/scan", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: "{not json",
|
||||
});
|
||||
const malformedRes = await scanRoute.POST(malformed);
|
||||
assert.equal(malformedRes.status, 400);
|
||||
});
|
||||
|
||||
test("POST /verify/:id marks verified, 404 when missing", async () => {
|
||||
const created = db.upsertDiscoveryResult({
|
||||
providerId: "delta",
|
||||
method: "public_api",
|
||||
authType: "api_key",
|
||||
feasibility: 5,
|
||||
riskLevel: "none",
|
||||
status: "pending",
|
||||
});
|
||||
const res = await verifyRoute.POST(req("POST", `/api/discovery/verify/${created.id}`), {
|
||||
params: Promise.resolve({ id: String(created.id) }),
|
||||
});
|
||||
assert.equal(res.status, 200);
|
||||
const body = await res.json();
|
||||
assert.equal(body.result.status, "verified");
|
||||
|
||||
const missing = await verifyRoute.POST(req("POST", "/api/discovery/verify/999999"), {
|
||||
params: Promise.resolve({ id: "999999" }),
|
||||
});
|
||||
assert.equal(missing.status, 404);
|
||||
});
|
||||
|
||||
test("DELETE /results/:id removes the row, 404 on second delete", async () => {
|
||||
const created = db.upsertDiscoveryResult({
|
||||
providerId: "epsilon",
|
||||
method: "free_tier",
|
||||
authType: "none",
|
||||
feasibility: 1,
|
||||
riskLevel: "none",
|
||||
status: "pending",
|
||||
});
|
||||
const first = await resultByIdRoute.DELETE(req("DELETE", `/api/discovery/results/${created.id}`), {
|
||||
params: Promise.resolve({ id: String(created.id) }),
|
||||
});
|
||||
assert.equal(first.status, 200);
|
||||
const second = await resultByIdRoute.DELETE(req("DELETE", `/api/discovery/results/${created.id}`), {
|
||||
params: Promise.resolve({ id: String(created.id) }),
|
||||
});
|
||||
assert.equal(second.status, 404);
|
||||
});
|
||||
|
||||
test("error responses do not leak stack traces", async () => {
|
||||
const missing = await resultByIdRoute.GET(req("GET", "/api/discovery/results/424242"), {
|
||||
params: Promise.resolve({ id: "424242" }),
|
||||
});
|
||||
const body = await missing.json();
|
||||
assert.ok(!String(body.error?.message ?? "").includes("at /"));
|
||||
});
|
||||
});
|
||||
@@ -1,30 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
GET,
|
||||
OPTIONS,
|
||||
} from "../../../../src/app/api/v1/provider-plugin-manifest/route.ts";
|
||||
|
||||
test("provider plugin manifest route returns JSON-safe manifest", async () => {
|
||||
const response = await GET();
|
||||
const body = await response.json();
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(response.headers.get("Content-Type"), "application/json");
|
||||
assert.equal(body.schemaVersion, 1);
|
||||
assert.equal(body.generatedFrom, "open-sse/config/providers");
|
||||
assert.ok(body.providers.length > 100);
|
||||
assert.ok(body.providers.some((provider: { id: string }) => provider.id === "openai"));
|
||||
|
||||
const serialized = JSON.stringify(body);
|
||||
assert.equal(serialized.includes("clientSecret"), false);
|
||||
});
|
||||
|
||||
test("provider plugin manifest route handles CORS preflight", async () => {
|
||||
const response = await OPTIONS();
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(response.headers.get("Access-Control-Allow-Methods"), "GET, OPTIONS");
|
||||
assert.equal(response.headers.get("Access-Control-Allow-Headers"), "*");
|
||||
});
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
getRoutingFallbackHeader,
|
||||
resolveRelayRoutingBackend,
|
||||
shouldTryBifrost,
|
||||
shouldTryBifrostForRequest,
|
||||
} from "../../../../src/app/api/v1/relay/chat/completions/routingBackend.ts";
|
||||
|
||||
test("relay routing backend defaults to TypeScript without bifrost", () => {
|
||||
@@ -99,56 +98,3 @@ test("relay routing backend keeps strict bifrost failures out of auto fallback a
|
||||
assert.equal(getRoutingFallbackHeader("bifrost", config), undefined);
|
||||
assert.equal(resolveRelayRoutingBackend({ OMNIROUTE_RELAY_BACKEND: "bifrost" }), "bifrost");
|
||||
});
|
||||
|
||||
test("relay routing backend auto mode tries bifrost for manifest-eligible providers", () => {
|
||||
const config = getBifrostRoutingConfig({
|
||||
BIFROST_BASE_URL: "http://127.0.0.1:8080",
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
shouldTryBifrostForRequest("auto", config, { model: "openai/gpt-4.1" }, () => ({
|
||||
eligible: true,
|
||||
reasons: [],
|
||||
})),
|
||||
{ tryBifrost: true }
|
||||
);
|
||||
});
|
||||
|
||||
test("relay routing backend auto mode skips bifrost for manifest-ineligible providers", () => {
|
||||
const config = getBifrostRoutingConfig({
|
||||
BIFROST_BASE_URL: "http://127.0.0.1:8080",
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
shouldTryBifrostForRequest("auto", config, { model: "cw/claude-sonnet-4.6" }, () => ({
|
||||
eligible: false,
|
||||
reasons: ["custom executor: claude-web"],
|
||||
})),
|
||||
{ tryBifrost: false, fallbackReason: "bifrost-ineligible" }
|
||||
);
|
||||
});
|
||||
|
||||
test("relay routing backend auto mode keeps unknown providers on TS fallback", () => {
|
||||
const config = getBifrostRoutingConfig({
|
||||
BIFROST_BASE_URL: "http://127.0.0.1:8080",
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
shouldTryBifrostForRequest("auto", config, { model: "unknown/model" }, () => null),
|
||||
{ tryBifrost: false, fallbackReason: "bifrost-provider-unknown" }
|
||||
);
|
||||
});
|
||||
|
||||
test("relay routing backend strict bifrost bypasses manifest eligibility", () => {
|
||||
const config = getBifrostRoutingConfig({
|
||||
BIFROST_BASE_URL: "http://127.0.0.1:8080",
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
shouldTryBifrostForRequest("bifrost", config, { model: "cw/claude-sonnet-4.6" }, () => ({
|
||||
eligible: false,
|
||||
reasons: ["custom executor: claude-web"],
|
||||
})),
|
||||
{ tryBifrost: true }
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
import { describe, test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { z } from "zod";
|
||||
import { validatedJsonBody } from "@/shared/validation/helpers";
|
||||
|
||||
function makeRequest(body: string, contentType = "application/json"): Request {
|
||||
return new Request("http://localhost/test", {
|
||||
method: "POST",
|
||||
headers: { "content-type": contentType },
|
||||
body,
|
||||
});
|
||||
}
|
||||
|
||||
describe("validatedJsonBody", () => {
|
||||
const schema = z.object({
|
||||
name: z.string().min(1),
|
||||
count: z.number().int().nonnegative(),
|
||||
});
|
||||
|
||||
test("returns the parsed and validated data on success", async () => {
|
||||
const result = await validatedJsonBody(makeRequest('{"name":"hello","count":3}'), schema);
|
||||
assert.equal(result.success, true);
|
||||
if (result.success) {
|
||||
assert.deepEqual(result.data, { name: "hello", count: 3 });
|
||||
}
|
||||
});
|
||||
|
||||
test("returns a 400 with structured details when the body fails Zod validation", async () => {
|
||||
const result = await validatedJsonBody(makeRequest('{"name":"","count":-1}'), schema);
|
||||
assert.equal(result.success, false);
|
||||
if (!result.success) {
|
||||
assert.equal(result.response.status, 400);
|
||||
const body = await result.response.json();
|
||||
assert.equal(body.error.message, "Invalid request");
|
||||
assert.ok(Array.isArray(body.error.details));
|
||||
const fields = body.error.details.map((d: { field: string }) => d.field);
|
||||
assert.ok(fields.includes("name"));
|
||||
assert.ok(fields.includes("count"));
|
||||
}
|
||||
});
|
||||
|
||||
test("returns a 400 with a body-parse failure for malformed JSON", async () => {
|
||||
const result = await validatedJsonBody(makeRequest("not json at all"), schema);
|
||||
assert.equal(result.success, false);
|
||||
if (!result.success) {
|
||||
assert.equal(result.response.status, 400);
|
||||
const body = await result.response.json();
|
||||
assert.deepEqual(body, {
|
||||
error: {
|
||||
message: "Invalid request",
|
||||
details: [{ field: "body", message: "Invalid JSON body" }],
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test("returns a 400 for an empty body", async () => {
|
||||
const result = await validatedJsonBody(makeRequest(""), schema);
|
||||
assert.equal(result.success, false);
|
||||
if (!result.success) {
|
||||
assert.equal(result.response.status, 400);
|
||||
}
|
||||
});
|
||||
|
||||
test("returns a 400 when required fields are missing entirely", async () => {
|
||||
const result = await validatedJsonBody(makeRequest("{}"), schema);
|
||||
assert.equal(result.success, false);
|
||||
if (!result.success) {
|
||||
assert.equal(result.response.status, 400);
|
||||
const body = await result.response.json();
|
||||
const fields = body.error.details.map((d: { field: string }) => d.field);
|
||||
assert.ok(fields.includes("name"));
|
||||
assert.ok(fields.includes("count"));
|
||||
}
|
||||
});
|
||||
|
||||
test("preserves the same envelope shape between parse and validate failure", async () => {
|
||||
const parseFailure = await validatedJsonBody(makeRequest("nope"), schema);
|
||||
const validateFailure = await validatedJsonBody(makeRequest("{}"), schema);
|
||||
assert.equal(parseFailure.success, false);
|
||||
assert.equal(validateFailure.success, false);
|
||||
if (!parseFailure.success && !validateFailure.success) {
|
||||
const parseBody = await parseFailure.response.json();
|
||||
const validateBody = await validateFailure.response.json();
|
||||
assert.equal(typeof parseBody.error.message, "string");
|
||||
assert.equal(typeof validateBody.error.message, "string");
|
||||
assert.ok(Array.isArray(parseBody.error.details));
|
||||
assert.ok(Array.isArray(validateBody.error.details));
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,199 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const { handleAudioTranslation } = await import("../../open-sse/handlers/audioTranslation.ts");
|
||||
|
||||
function buildFile(contents, name, type) {
|
||||
return new File([Buffer.from(contents)], name, { type });
|
||||
}
|
||||
|
||||
test("handleAudioTranslation requires model", async () => {
|
||||
const formData = new FormData();
|
||||
formData.append("file", buildFile("abc", "audio.wav", "audio/wav"));
|
||||
|
||||
const response = await handleAudioTranslation({ formData, credentials: { apiKey: "x" } });
|
||||
const payload = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 400);
|
||||
assert.equal(payload.error.message, "model is required");
|
||||
});
|
||||
|
||||
test("handleAudioTranslation requires a file upload", async () => {
|
||||
const formData = new FormData();
|
||||
formData.append("model", "openai/whisper-1");
|
||||
|
||||
const response = await handleAudioTranslation({ formData, credentials: { apiKey: "x" } });
|
||||
const payload = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 400);
|
||||
assert.equal(payload.error.message, "file is required");
|
||||
});
|
||||
|
||||
test("handleAudioTranslation requires credentials for authenticated providers", async () => {
|
||||
const formData = new FormData();
|
||||
formData.append("model", "openai/whisper-1");
|
||||
formData.append("file", buildFile("abc", "clip.wav", "audio/wav"));
|
||||
|
||||
const response = await handleAudioTranslation({ formData, credentials: null });
|
||||
const payload = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 401);
|
||||
assert.equal(payload.error.message, "No credentials for translation provider: openai");
|
||||
});
|
||||
|
||||
test("handleAudioTranslation rejects unsupported providers", async () => {
|
||||
const formData = new FormData();
|
||||
formData.append("model", "unknown/provider");
|
||||
formData.append("file", buildFile("abc", "clip.wav", "audio/wav"));
|
||||
|
||||
const response = await handleAudioTranslation({
|
||||
formData,
|
||||
credentials: { apiKey: "x" },
|
||||
});
|
||||
const payload = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 400);
|
||||
assert.match(payload.error.message, /No translation provider found for model "unknown\/provider"/);
|
||||
});
|
||||
|
||||
test("handleAudioTranslation dispatches OpenAI-compatible multipart requests and returns { text }", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
let captured;
|
||||
|
||||
globalThis.fetch = async (url, options = {}) => {
|
||||
captured = {
|
||||
url: String(url),
|
||||
headers: options.headers,
|
||||
body: options.body,
|
||||
};
|
||||
|
||||
return new Response(JSON.stringify({ text: "hello world" }), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("model", "openai/whisper-1");
|
||||
formData.append("file", buildFile("abc", "clip.webm", "audio/webm"));
|
||||
formData.append("prompt", "greeting");
|
||||
formData.append("response_format", "json");
|
||||
formData.append("temperature", "0.2");
|
||||
// Translation always outputs English — a caller-supplied `language` must
|
||||
// NOT be forwarded upstream (differs from /v1/audio/transcriptions).
|
||||
formData.append("language", "pt");
|
||||
|
||||
const response = await handleAudioTranslation({
|
||||
formData,
|
||||
credentials: { apiKey: "openai-key" },
|
||||
});
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(captured.url, "https://api.openai.com/v1/audio/translations");
|
||||
assert.equal(captured.headers.Authorization, "Bearer openai-key");
|
||||
assert.match(captured.headers["Content-Type"], /^multipart\/form-data; boundary=/);
|
||||
|
||||
const bodyText = new TextDecoder().decode(captured.body);
|
||||
assert.ok(bodyText.includes('name="model"'));
|
||||
assert.ok(bodyText.includes("whisper-1"));
|
||||
assert.ok(bodyText.includes('name="prompt"'));
|
||||
assert.ok(bodyText.includes("greeting"));
|
||||
assert.ok(bodyText.includes('name="response_format"'));
|
||||
assert.ok(bodyText.includes('name="temperature"'));
|
||||
assert.ok(bodyText.includes("0.2"));
|
||||
assert.ok(bodyText.includes('name="file"'));
|
||||
assert.ok(bodyText.includes('filename="clip.webm"'));
|
||||
assert.ok(!bodyText.includes('name="language"'));
|
||||
|
||||
assert.deepEqual(await response.json(), { text: "hello world" });
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("handleAudioTranslation dispatches Groq-compatible multipart requests", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
let captured;
|
||||
|
||||
globalThis.fetch = async (url, options = {}) => {
|
||||
captured = { url: String(url), headers: options.headers };
|
||||
return new Response(JSON.stringify({ text: "bonjour" }), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("model", "groq/whisper-large-v3");
|
||||
formData.append("file", buildFile("abc", "clip.wav", "audio/wav"));
|
||||
|
||||
const response = await handleAudioTranslation({
|
||||
formData,
|
||||
credentials: { apiKey: "groq-key" },
|
||||
});
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(captured.url, "https://api.groq.com/openai/v1/audio/translations");
|
||||
assert.equal(captured.headers.Authorization, "Bearer groq-key");
|
||||
assert.deepEqual(await response.json(), { text: "bonjour" });
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("handleAudioTranslation surfaces parsed upstream errors without leaking internals", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
globalThis.fetch = async () =>
|
||||
new Response(JSON.stringify({ error: { message: "too many requests" } }), {
|
||||
status: 429,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("model", "openai/whisper-1");
|
||||
formData.append("file", buildFile("abc", "clip.wav", "audio/wav"));
|
||||
|
||||
const response = await handleAudioTranslation({
|
||||
formData,
|
||||
credentials: { apiKey: "openai-key" },
|
||||
});
|
||||
const payload = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 429);
|
||||
assert.equal(payload.error.message, "too many requests");
|
||||
assert.ok(!payload.error.message.includes("at /"));
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("handleAudioTranslation sanitizes stack-trace-shaped messages on fetch failure", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
globalThis.fetch = async () => {
|
||||
throw new Error("Fetch failed at /home/user/project/src/secure.ts:10:5");
|
||||
};
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("model", "openai/whisper-1");
|
||||
formData.append("file", buildFile("abc", "clip.wav", "audio/wav"));
|
||||
|
||||
const response = await handleAudioTranslation({
|
||||
formData,
|
||||
credentials: { apiKey: "openai-key" },
|
||||
});
|
||||
const payload = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 500);
|
||||
// Rule: error responses must never leak absolute source paths / stack traces.
|
||||
assert.ok(!payload.error.message.includes("at /"));
|
||||
assert.ok(!payload.error.message.includes("secure.ts"));
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
@@ -1,380 +0,0 @@
|
||||
/**
|
||||
* AuggieExecutor unit tests.
|
||||
*
|
||||
* Rather than mocking node:child_process (fragile under ESM without
|
||||
* --experimental-vm-modules — see tests/unit/dns-config-generic.test.ts for the
|
||||
* same tradeoff), these tests point `AUGGIE_BIN` at small real, disposable shell
|
||||
* scripts that stand in for the `auggie` CLI. No live `auggie` binary is required
|
||||
* or touched — CI never needs the real Augment CLI installed.
|
||||
*/
|
||||
|
||||
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 { AuggieExecutor, buildAuggiePrompt, resolveAuggieBin, resolveAuggieModel } = await import(
|
||||
"@omniroute/open-sse/executors/auggie"
|
||||
);
|
||||
|
||||
const TMP_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-auggie-test-"));
|
||||
|
||||
/** Write an executable shell script and return its absolute path. */
|
||||
function writeFakeBin(name: string, script: string): string {
|
||||
const p = path.join(TMP_DIR, name);
|
||||
fs.writeFileSync(p, `#!/bin/sh\n${script}\n`, { mode: 0o755 });
|
||||
return p;
|
||||
}
|
||||
|
||||
|
||||
async function readSseEvents(response: Response): Promise<Record<string, unknown>[]> {
|
||||
const text = await response.text();
|
||||
const events: Record<string, unknown>[] = [];
|
||||
for (const line of text.split("\n")) {
|
||||
if (!line.startsWith("data: ")) continue;
|
||||
const payload = line.slice("data: ".length).trim();
|
||||
if (payload === "[DONE]") continue;
|
||||
events.push(JSON.parse(payload));
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
test.after(() => {
|
||||
fs.rmSync(TMP_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
// ─── buildAuggiePrompt ────────────────────────────────────────────────────
|
||||
|
||||
test("buildAuggiePrompt flattens system/user/assistant turns with role tags", () => {
|
||||
const prompt = buildAuggiePrompt([
|
||||
{ role: "system", content: "Be terse." },
|
||||
{ role: "user", content: "hi" },
|
||||
{ role: "assistant", content: "hello" },
|
||||
{ role: "user", content: "how are you?" },
|
||||
]);
|
||||
assert.equal(
|
||||
prompt,
|
||||
"[System]\nBe terse.\n\n[User]\nhi\n\n[Assistant]\nhello\n\n[User]\nhow are you?"
|
||||
);
|
||||
});
|
||||
|
||||
test("buildAuggiePrompt flattens array-shaped content blocks and skips empty turns", () => {
|
||||
const prompt = buildAuggiePrompt([
|
||||
{ role: "user", content: [{ type: "text", text: "part1 " }, { type: "text", text: "part2" }] },
|
||||
{ role: "user", content: "" },
|
||||
{ role: "user", content: [] },
|
||||
]);
|
||||
assert.equal(prompt, "[User]\npart1 part2");
|
||||
});
|
||||
|
||||
test("buildAuggiePrompt returns a placeholder for an empty conversation", () => {
|
||||
assert.equal(buildAuggiePrompt([]), "(empty)");
|
||||
});
|
||||
|
||||
// ─── resolveAuggieBin ─────────────────────────────────────────────────────
|
||||
|
||||
test("resolveAuggieBin honors AUGGIE_BIN env override", () => {
|
||||
const prev = process.env.AUGGIE_BIN;
|
||||
try {
|
||||
process.env.AUGGIE_BIN = "/custom/path/to/auggie";
|
||||
assert.equal(resolveAuggieBin(), "/custom/path/to/auggie");
|
||||
} finally {
|
||||
if (prev === undefined) delete process.env.AUGGIE_BIN;
|
||||
else process.env.AUGGIE_BIN = prev;
|
||||
}
|
||||
});
|
||||
|
||||
// ─── execute(): ENOENT → CLI not found ─────────────────────────────────────
|
||||
|
||||
test("execute() surfaces a sanitized 'CLI not found' error on ENOENT (streaming)", async () => {
|
||||
const prevBin = process.env.AUGGIE_BIN;
|
||||
process.env.AUGGIE_BIN = path.join(TMP_DIR, "does-not-exist-binary");
|
||||
try {
|
||||
const executor = new AuggieExecutor();
|
||||
const { response } = await executor.execute({
|
||||
model: "claude-sonnet-4.6",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: true,
|
||||
credentials: {} as never,
|
||||
});
|
||||
assert.equal(response.headers.get("Content-Type"), "text/event-stream");
|
||||
const events = await readSseEvents(response);
|
||||
const errorEvent = events.find((e) => (e as any).error);
|
||||
assert.ok(errorEvent, "expected an error SSE event");
|
||||
const message = String((errorEvent as any).error.message);
|
||||
// The configured bin path is intentionally included (actionable for the
|
||||
// operator) — sanitizeErrorMessage only strips stack-trace-shaped source
|
||||
// paths (`*.ts`/`*.js` + line:col), not arbitrary CLI binary paths.
|
||||
assert.match(message, /Auggie CLI not found/);
|
||||
assert.equal(message.includes("\n"), false, "message must not contain a stack trace");
|
||||
} finally {
|
||||
if (prevBin === undefined) delete process.env.AUGGIE_BIN;
|
||||
else process.env.AUGGIE_BIN = prevBin;
|
||||
}
|
||||
});
|
||||
|
||||
test("execute() surfaces a sanitized 'CLI not found' error on ENOENT (non-streaming)", async () => {
|
||||
const prevBin = process.env.AUGGIE_BIN;
|
||||
process.env.AUGGIE_BIN = path.join(TMP_DIR, "does-not-exist-binary-2");
|
||||
try {
|
||||
const executor = new AuggieExecutor();
|
||||
const { response } = await executor.execute({
|
||||
model: "claude-sonnet-4.6",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: false,
|
||||
credentials: {} as never,
|
||||
});
|
||||
assert.equal(response.headers.get("Content-Type"), "application/json");
|
||||
assert.equal(response.status, 502);
|
||||
const body = await response.json();
|
||||
assert.match(String(body.error.message), /Auggie CLI not found/);
|
||||
} finally {
|
||||
if (prevBin === undefined) delete process.env.AUGGIE_BIN;
|
||||
else process.env.AUGGIE_BIN = prevBin;
|
||||
}
|
||||
});
|
||||
|
||||
// ─── execute(): non-zero exit → sanitized error ────────────────────────────
|
||||
|
||||
test("execute() surfaces a sanitized error when the CLI exits non-zero (streaming)", async () => {
|
||||
const bin = writeFakeBin("fake-auggie-fail.sh", 'echo "boom at /home/attacker/secret.ts:42" 1>&2\nexit 3');
|
||||
const prevBin = process.env.AUGGIE_BIN;
|
||||
process.env.AUGGIE_BIN = bin;
|
||||
try {
|
||||
const executor = new AuggieExecutor();
|
||||
const { response } = await executor.execute({
|
||||
model: "claude-sonnet-4.6",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: true,
|
||||
credentials: {} as never,
|
||||
});
|
||||
const events = await readSseEvents(response);
|
||||
const errorEvent = events.find((e) => (e as any).error);
|
||||
assert.ok(errorEvent, "expected an error SSE event");
|
||||
const message = String((errorEvent as any).error.message);
|
||||
assert.match(message, /exited with code 3/);
|
||||
assert.equal(message.includes("/home/attacker/secret.ts"), false, "path must be sanitized");
|
||||
} finally {
|
||||
if (prevBin === undefined) delete process.env.AUGGIE_BIN;
|
||||
else process.env.AUGGIE_BIN = prevBin;
|
||||
}
|
||||
});
|
||||
|
||||
test("execute() surfaces a sanitized error when the CLI exits non-zero (non-streaming)", async () => {
|
||||
const bin = writeFakeBin("fake-auggie-fail2.sh", "exit 1");
|
||||
const prevBin = process.env.AUGGIE_BIN;
|
||||
process.env.AUGGIE_BIN = bin;
|
||||
try {
|
||||
const executor = new AuggieExecutor();
|
||||
const { response } = await executor.execute({
|
||||
model: "claude-sonnet-4.6",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: false,
|
||||
credentials: {} as never,
|
||||
});
|
||||
assert.equal(response.status, 502);
|
||||
const body = await response.json();
|
||||
assert.match(String(body.error.message), /exited with code 1/);
|
||||
} finally {
|
||||
if (prevBin === undefined) delete process.env.AUGGIE_BIN;
|
||||
else process.env.AUGGIE_BIN = prevBin;
|
||||
}
|
||||
});
|
||||
|
||||
// ─── execute(): stream vs non-stream shape ─────────────────────────────────
|
||||
|
||||
test("execute() with stream=true returns SSE deltas + [DONE]", async () => {
|
||||
const bin = writeFakeBin("fake-auggie-echo.sh", 'printf "hello world"');
|
||||
const prevBin = process.env.AUGGIE_BIN;
|
||||
process.env.AUGGIE_BIN = bin;
|
||||
try {
|
||||
const executor = new AuggieExecutor();
|
||||
const { response } = await executor.execute({
|
||||
model: "claude-sonnet-4.6",
|
||||
body: { messages: [{ role: "user", content: "say hi" }] },
|
||||
stream: true,
|
||||
credentials: {} as never,
|
||||
});
|
||||
assert.equal(response.headers.get("Content-Type"), "text/event-stream");
|
||||
const events = await readSseEvents(response);
|
||||
// First chunk announces the assistant role.
|
||||
assert.equal((events[0] as any).choices[0].delta.role, "assistant");
|
||||
// Content is streamed as delta chunks that concatenate to the full text.
|
||||
const contentDeltas = events
|
||||
.map((e) => (e as any).choices?.[0]?.delta?.content)
|
||||
.filter((c) => typeof c === "string");
|
||||
assert.equal(contentDeltas.join(""), "hello world");
|
||||
// Final chunk signals completion.
|
||||
const last = events[events.length - 1] as any;
|
||||
assert.equal(last.choices[0].finish_reason, "stop");
|
||||
} finally {
|
||||
if (prevBin === undefined) delete process.env.AUGGIE_BIN;
|
||||
else process.env.AUGGIE_BIN = prevBin;
|
||||
}
|
||||
});
|
||||
|
||||
test("execute() with stream=false returns a single chat.completion JSON body", async () => {
|
||||
const bin = writeFakeBin("fake-auggie-echo2.sh", 'printf "hello world"');
|
||||
const prevBin = process.env.AUGGIE_BIN;
|
||||
process.env.AUGGIE_BIN = bin;
|
||||
try {
|
||||
const executor = new AuggieExecutor();
|
||||
const { response } = await executor.execute({
|
||||
model: "claude-sonnet-4.6",
|
||||
body: { messages: [{ role: "user", content: "say hi" }] },
|
||||
stream: false,
|
||||
credentials: {} as never,
|
||||
});
|
||||
assert.equal(response.headers.get("Content-Type"), "application/json");
|
||||
assert.equal(response.status, 200);
|
||||
const body = await response.json();
|
||||
assert.equal(body.object, "chat.completion");
|
||||
assert.equal(body.choices[0].message.role, "assistant");
|
||||
assert.equal(body.choices[0].message.content, "hello world");
|
||||
assert.equal(body.choices[0].finish_reason, "stop");
|
||||
assert.ok(body.usage);
|
||||
} finally {
|
||||
if (prevBin === undefined) delete process.env.AUGGIE_BIN;
|
||||
else process.env.AUGGIE_BIN = prevBin;
|
||||
}
|
||||
});
|
||||
|
||||
// ─── resolveAuggieModel ─────────────────────────────────────────────────────
|
||||
|
||||
test("resolveAuggieModel defaults to a real allowlisted model when unset", () => {
|
||||
const result = resolveAuggieModel(undefined);
|
||||
assert.equal(result.ok, true);
|
||||
if (result.ok) assert.equal(typeof result.model, "string");
|
||||
});
|
||||
|
||||
test("resolveAuggieModel accepts a known registry model id verbatim", () => {
|
||||
const result = resolveAuggieModel("claude-haiku-4.5");
|
||||
assert.deepEqual(result, { ok: true, model: "claude-haiku-4.5" });
|
||||
});
|
||||
|
||||
// ─── execute(): model allowlist (argument-injection defense) ──────────────
|
||||
|
||||
test("execute() rejects a model not in the registry allowlist and never spawns", async () => {
|
||||
const marker = path.join(TMP_DIR, "spawned-unknown-model.marker");
|
||||
const bin = writeFakeBin("fake-auggie-unknown.sh", `touch "${marker}"\nprintf "hi"`);
|
||||
const prevBin = process.env.AUGGIE_BIN;
|
||||
process.env.AUGGIE_BIN = bin;
|
||||
try {
|
||||
const executor = new AuggieExecutor();
|
||||
const { response } = await executor.execute({
|
||||
model: "totally-not-a-real-model",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: false,
|
||||
credentials: {} as never,
|
||||
});
|
||||
assert.equal(response.status, 400);
|
||||
const body = await response.json();
|
||||
assert.match(String(body.error.message), /Unknown Auggie model/);
|
||||
assert.equal(fs.existsSync(marker), false, "subprocess must never be spawned");
|
||||
} finally {
|
||||
if (prevBin === undefined) delete process.env.AUGGIE_BIN;
|
||||
else process.env.AUGGIE_BIN = prevBin;
|
||||
}
|
||||
});
|
||||
|
||||
test("execute() rejects a model starting with '-' (flag smuggling) and never spawns (streaming)", async () => {
|
||||
const marker = path.join(TMP_DIR, "spawned-flag-smuggle.marker");
|
||||
const bin = writeFakeBin("fake-auggie-flag.sh", `touch "${marker}"\nprintf "hi"`);
|
||||
const prevBin = process.env.AUGGIE_BIN;
|
||||
process.env.AUGGIE_BIN = bin;
|
||||
try {
|
||||
const executor = new AuggieExecutor();
|
||||
const { response } = await executor.execute({
|
||||
model: "-rf",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: true,
|
||||
credentials: {} as never,
|
||||
});
|
||||
assert.equal(response.headers.get("Content-Type"), "text/event-stream");
|
||||
const events = await readSseEvents(response);
|
||||
const errorEvent = events.find((e) => (e as any).error);
|
||||
assert.ok(errorEvent, "expected an error SSE event");
|
||||
assert.match(String((errorEvent as any).error.message), /must not start with "-"/);
|
||||
assert.equal(fs.existsSync(marker), false, "subprocess must never be spawned");
|
||||
} finally {
|
||||
if (prevBin === undefined) delete process.env.AUGGIE_BIN;
|
||||
else process.env.AUGGIE_BIN = prevBin;
|
||||
}
|
||||
});
|
||||
|
||||
test("execute() spawns with a valid allowlisted model, no shell, and a '--' argv separator", async () => {
|
||||
const argvFile = path.join(TMP_DIR, "captured-argv.txt");
|
||||
const bin = writeFakeBin(
|
||||
"fake-auggie-argv.sh",
|
||||
`for a in "$@"; do printf '%s\\n' "$a" >> "${argvFile}"; done\nprintf "ok"`
|
||||
);
|
||||
const prevBin = process.env.AUGGIE_BIN;
|
||||
process.env.AUGGIE_BIN = bin;
|
||||
try {
|
||||
const executor = new AuggieExecutor();
|
||||
const { response } = await executor.execute({
|
||||
model: "claude-opus-4.6",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: false,
|
||||
credentials: {} as never,
|
||||
});
|
||||
assert.equal(response.status, 200);
|
||||
const body = await response.json();
|
||||
assert.equal(body.choices[0].message.content, "ok");
|
||||
const argv = fs.readFileSync(argvFile, "utf8").trim().split("\n");
|
||||
assert.deepEqual(argv, ["--print", "--quiet", "--model", "claude-opus-4.6", "--"]);
|
||||
} finally {
|
||||
if (prevBin === undefined) delete process.env.AUGGIE_BIN;
|
||||
else process.env.AUGGIE_BIN = prevBin;
|
||||
fs.rmSync(argvFile, { force: true });
|
||||
}
|
||||
});
|
||||
|
||||
// ─── execute(): abort kills the subprocess promptly ────────────────────────
|
||||
|
||||
test("execute() aborts a long-running CLI process instead of hanging (streaming)", async () => {
|
||||
// Sleeps far longer than the test timeout unless killed on abort.
|
||||
const bin = writeFakeBin("fake-auggie-sleep.sh", "sleep 30");
|
||||
const prevBin = process.env.AUGGIE_BIN;
|
||||
process.env.AUGGIE_BIN = bin;
|
||||
try {
|
||||
const executor = new AuggieExecutor();
|
||||
const controller = new AbortController();
|
||||
const { response } = await executor.execute({
|
||||
model: "claude-sonnet-4.6",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: true,
|
||||
credentials: {} as never,
|
||||
signal: controller.signal,
|
||||
});
|
||||
// Give the child process a beat to spawn, then abort.
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
controller.abort();
|
||||
|
||||
// Reading the stream must resolve promptly (i.e. the stream closes) rather
|
||||
// than hanging for the full 30s sleep.
|
||||
const start = Date.now();
|
||||
await response.text();
|
||||
const elapsed = Date.now() - start;
|
||||
assert.ok(elapsed < 5000, `expected abort to close the stream quickly, took ${elapsed}ms`);
|
||||
} finally {
|
||||
if (prevBin === undefined) delete process.env.AUGGIE_BIN;
|
||||
else process.env.AUGGIE_BIN = prevBin;
|
||||
}
|
||||
});
|
||||
|
||||
// ─── refreshCredentials() is a no-op ────────────────────────────────────────
|
||||
|
||||
test("refreshCredentials() is a no-op (auggie has no OmniRoute-managed credentials)", async () => {
|
||||
const executor = new AuggieExecutor();
|
||||
const result = await executor.refreshCredentials({} as never);
|
||||
assert.equal(result, null);
|
||||
});
|
||||
|
||||
test("buildUrl/buildHeaders/transformRequest match the CLI-passthrough shape", () => {
|
||||
const executor = new AuggieExecutor();
|
||||
assert.equal(executor.buildUrl(), "auggie://cli/stdio");
|
||||
assert.deepEqual(executor.buildHeaders(), {});
|
||||
assert.equal(executor.transformRequest(), null);
|
||||
});
|
||||
@@ -1,27 +0,0 @@
|
||||
import { describe, test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
isLocalOnlyPath,
|
||||
isLocalOnlyBypassableByManageScope,
|
||||
} from "@/server/authz/routeGuard";
|
||||
|
||||
// Security guard: the discovery surface must be strict-loopback only. If someone
|
||||
// removes "/api/discovery/" from LOCAL_ONLY_API_PREFIXES, or adds it to the
|
||||
// manage-scope bypass list, these assertions fail — the scan route makes
|
||||
// outbound probes (SSRF-adjacent) and must never be tunnel-reachable.
|
||||
describe("discovery routes are strict local-only", () => {
|
||||
for (const path of [
|
||||
"/api/discovery/results",
|
||||
"/api/discovery/results/42",
|
||||
"/api/discovery/scan",
|
||||
"/api/discovery/verify/42",
|
||||
]) {
|
||||
test(`isLocalOnlyPath("${path}") === true`, () => {
|
||||
assert.equal(isLocalOnlyPath(path), true);
|
||||
});
|
||||
|
||||
test(`"${path}" is NOT bypassable by manage-scope (strict loopback)`, () => {
|
||||
assert.equal(isLocalOnlyBypassableByManageScope(path), false);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -138,50 +138,6 @@ test("runAuthzPipeline redirects unauthenticated /home/* nested paths to login (
|
||||
assert.equal(response.headers.get("x-omniroute-route-class"), "MANAGEMENT");
|
||||
});
|
||||
|
||||
// PR #1810 (upstream 9router): reverse-proxy subpath deployment via
|
||||
// OMNIROUTE_BASE_PATH. Next.js strips the basePath from nextUrl.pathname
|
||||
// before route classification, so the redirect targets must re-add it via
|
||||
// request.nextUrl.basePath to stay inside the deployed subpath.
|
||||
test("runAuthzPipeline prefixes the root-to-dashboard redirect with basePath when set", async () => {
|
||||
await forceAuthRequired();
|
||||
|
||||
const req = new NextRequest("http://localhost/omniroute/", {
|
||||
nextConfig: { basePath: "/omniroute" },
|
||||
});
|
||||
|
||||
const response = await pipeline.runAuthzPipeline(req, { enforce: true });
|
||||
|
||||
assert.equal(response.status, 307);
|
||||
assert.equal(response.headers.get("location"), "http://localhost/omniroute/dashboard");
|
||||
});
|
||||
|
||||
test("runAuthzPipeline prefixes the dashboard login redirect with basePath when set", async () => {
|
||||
await forceAuthRequired();
|
||||
|
||||
const req = new NextRequest("http://localhost/omniroute/dashboard", {
|
||||
nextConfig: { basePath: "/omniroute" },
|
||||
});
|
||||
|
||||
const response = await pipeline.runAuthzPipeline(req, { enforce: true });
|
||||
|
||||
assert.equal(response.status, 307);
|
||||
assert.equal(response.headers.get("location"), "http://localhost/omniroute/login");
|
||||
assert.equal(response.headers.get("x-omniroute-route-class"), "MANAGEMENT");
|
||||
});
|
||||
|
||||
test("runAuthzPipeline leaves redirect targets unprefixed when basePath is empty", async () => {
|
||||
await forceAuthRequired();
|
||||
|
||||
const req = new NextRequest("http://localhost/dashboard", {
|
||||
nextConfig: { basePath: "" },
|
||||
});
|
||||
|
||||
const response = await pipeline.runAuthzPipeline(req, { enforce: true });
|
||||
|
||||
assert.equal(response.status, 307);
|
||||
assert.equal(response.headers.get("location"), "http://localhost/login");
|
||||
});
|
||||
|
||||
test("runAuthzPipeline allows onboarding when login is required but no password exists", async () => {
|
||||
delete process.env.INITIAL_PASSWORD;
|
||||
await settingsDb.updateSettings({
|
||||
|
||||
@@ -189,25 +189,6 @@ test("isLocalOnlyBypassableByManageScope: /api/services/* is NOT bypassable (spa
|
||||
assert.equal(isLocalOnlyBypassableByManageScope("/api/services/"), false);
|
||||
});
|
||||
|
||||
// Hard Rule #17 — Mux (coder/mux) embedded service (spawns child processes via
|
||||
// runNpm install + node server spawn): every /api/services/mux/* route MUST be
|
||||
// classified local-only, same as every other embedded service under this prefix.
|
||||
test("isLocalOnlyPath: /api/services/mux/* is local-only (Hard Rule #17)", () => {
|
||||
assert.equal(isLocalOnlyPath("/api/services/mux/install"), true);
|
||||
assert.equal(isLocalOnlyPath("/api/services/mux/start"), true);
|
||||
assert.equal(isLocalOnlyPath("/api/services/mux/stop"), true);
|
||||
assert.equal(isLocalOnlyPath("/api/services/mux/restart"), true);
|
||||
assert.equal(isLocalOnlyPath("/api/services/mux/update"), true);
|
||||
assert.equal(isLocalOnlyPath("/api/services/mux/status"), true);
|
||||
assert.equal(isLocalOnlyPath("/api/services/mux/auto-start"), true);
|
||||
assert.equal(isLocalOnlyPath("/api/services/mux/logs"), true);
|
||||
});
|
||||
|
||||
test("isLocalOnlyBypassableByManageScope: /api/services/mux/* is NOT bypassable (spawn-capable)", () => {
|
||||
assert.equal(isLocalOnlyBypassableByManageScope("/api/services/mux/start"), false);
|
||||
assert.equal(isLocalOnlyBypassableByManageScope("/api/services/mux/install"), false);
|
||||
});
|
||||
|
||||
test("management policy rejects /api/services/ from non-localhost (status 403)", async () => {
|
||||
const ctx = makeCtx("/api/services/9router/start", { host: "evil.tunnel.io" });
|
||||
const outcome = await managementPolicy.evaluate(ctx);
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
const { APIKEY_PROVIDERS } = await import("../../src/shared/constants/providers.ts");
|
||||
const { PROVIDER_ENDPOINTS } = await import("../../src/shared/constants/config.ts");
|
||||
const { REGISTRY: providerRegistry } = await import("../../open-sse/config/providerRegistry.ts");
|
||||
const { isValidModel } = await import("../../src/shared/constants/models.ts");
|
||||
|
||||
const BAI_CHAT_URL = "https://api.b.ai/v1/chat/completions";
|
||||
const BAI_MODELS_URL = "https://api.b.ai/v1/models";
|
||||
|
||||
test("b.ai is registered as an API-key provider", () => {
|
||||
const entry = APIKEY_PROVIDERS.bai;
|
||||
assert.ok(entry, "APIKEY_PROVIDERS.bai must be defined");
|
||||
assert.equal(entry.id, "bai");
|
||||
assert.equal(entry.alias, "bai");
|
||||
assert.equal(entry.name, "b.ai");
|
||||
assert.equal(entry.website, "https://b.ai");
|
||||
assert.equal(entry.passthroughModels, true);
|
||||
});
|
||||
|
||||
test("b.ai is distinct from the existing thebai (TheB.AI) provider", () => {
|
||||
const bai = APIKEY_PROVIDERS.bai;
|
||||
const thebai = APIKEY_PROVIDERS.thebai;
|
||||
assert.ok(bai, "APIKEY_PROVIDERS.bai must be defined");
|
||||
assert.ok(thebai, "APIKEY_PROVIDERS.thebai must be defined");
|
||||
assert.notEqual(bai.id, thebai.id);
|
||||
assert.notEqual(bai.website, thebai.website);
|
||||
assert.equal(bai.website, "https://b.ai");
|
||||
assert.equal(thebai.website, "https://theb.ai");
|
||||
});
|
||||
|
||||
test("b.ai exposes the OpenAI-compatible chat completions endpoint", () => {
|
||||
assert.equal(PROVIDER_ENDPOINTS.bai, BAI_CHAT_URL);
|
||||
});
|
||||
|
||||
test("b.ai registry entry uses OpenAI format with bearer API-key auth", () => {
|
||||
const entry = providerRegistry.bai;
|
||||
assert.ok(entry, "providerRegistry.bai must be defined");
|
||||
assert.equal(entry.id, "bai");
|
||||
assert.equal(entry.alias, "bai");
|
||||
assert.equal(entry.format, "openai");
|
||||
assert.equal(entry.executor, "default");
|
||||
assert.equal(entry.authType, "apikey");
|
||||
assert.equal(entry.authHeader, "bearer");
|
||||
assert.equal(entry.baseUrl, BAI_CHAT_URL);
|
||||
assert.equal(entry.modelsUrl, BAI_MODELS_URL);
|
||||
assert.equal(entry.passthroughModels, true);
|
||||
});
|
||||
|
||||
test("b.ai ships no static model seed — relies fully on passthrough + live catalog", () => {
|
||||
const models = providerRegistry.bai.models;
|
||||
assert.deepEqual(models, []);
|
||||
});
|
||||
|
||||
test("b.ai accepts any model id via passthrough models (GPT/Claude/Gemini/Kimi/GLM behind one key)", () => {
|
||||
assert.equal(isValidModel("bai", "gpt-5.2"), true);
|
||||
assert.equal(isValidModel("bai", "claude-opus-4-5"), true);
|
||||
assert.equal(isValidModel("bai", "gemini-3-pro"), true);
|
||||
assert.equal(isValidModel("bai", "kimi-k2.5"), true);
|
||||
});
|
||||
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-bai-"));
|
||||
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 modelsRoute = await import("../../src/app/api/providers/[id]/models/route.ts");
|
||||
|
||||
async function resetStorage() {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
test.after(() => {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
interface ModelsBody {
|
||||
provider: string;
|
||||
connectionId: string;
|
||||
models: Array<{ id: string }>;
|
||||
source?: string;
|
||||
}
|
||||
|
||||
test("b.ai import fetches the live /v1/models catalog", async () => {
|
||||
await resetStorage();
|
||||
const connection = await providersDb.createProviderConnection({
|
||||
provider: "bai",
|
||||
authType: "apikey",
|
||||
name: "bai-live",
|
||||
apiKey: "bai-key",
|
||||
});
|
||||
|
||||
let fetched = false;
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = async (url) => {
|
||||
if (String(url) === BAI_MODELS_URL) {
|
||||
fetched = true;
|
||||
return Response.json({
|
||||
object: "list",
|
||||
data: [{ id: "gpt-5.2" }, { id: "claude-opus-4-5" }, { id: "kimi-k2.5" }],
|
||||
});
|
||||
}
|
||||
return new Response("not found", { status: 404 });
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await modelsRoute.GET(
|
||||
new Request(`http://localhost/api/providers/${connection.id}/models?refresh=true`),
|
||||
{ params: { id: connection.id } }
|
||||
);
|
||||
assert.equal(response.status, 200);
|
||||
const body = (await response.json()) as ModelsBody;
|
||||
assert.equal(body.provider, "bai");
|
||||
assert.equal(body.source, "api", "should serve the live upstream catalog");
|
||||
assert.ok(fetched, `should have probed ${BAI_MODELS_URL}`);
|
||||
const ids = body.models.map((model) => model.id);
|
||||
assert.ok(ids.includes("gpt-5.2"), `live catalog model missing: ${ids.join(",")}`);
|
||||
assert.ok(ids.includes("kimi-k2.5"), `live catalog model missing: ${ids.join(",")}`);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("b.ai import falls back to an empty local catalog when live fetch fails", async () => {
|
||||
await resetStorage();
|
||||
const connection = await providersDb.createProviderConnection({
|
||||
provider: "bai",
|
||||
authType: "apikey",
|
||||
name: "bai-fallback",
|
||||
apiKey: "bai-key-2",
|
||||
});
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = async () => new Response("bad gateway", { status: 502 });
|
||||
|
||||
try {
|
||||
const response = await modelsRoute.GET(
|
||||
new Request(`http://localhost/api/providers/${connection.id}/models?refresh=true`),
|
||||
{ params: { id: connection.id } }
|
||||
);
|
||||
assert.equal(response.status, 200);
|
||||
const body = (await response.json()) as ModelsBody;
|
||||
assert.equal(body.provider, "bai");
|
||||
assert.equal(body.source, "local_catalog", "import must not break when upstream is down");
|
||||
assert.deepEqual(
|
||||
body.models.map((model) => model.id),
|
||||
[]
|
||||
);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
@@ -1,55 +0,0 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
// Split-guard for the base executor header-helper extraction.
|
||||
// The pure upstream-header helpers live in base/headers.ts (no host state, no fetch).
|
||||
// base.ts re-exports all 6 so the ~18 executors + tests that import them from "./base.ts"
|
||||
// keep resolving unchanged.
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const EXE = join(HERE, "../../open-sse/executors");
|
||||
const HOST = join(EXE, "base.ts");
|
||||
const LEAF = join(EXE, "base/headers.ts");
|
||||
|
||||
test("leaf hosts the header helpers and does not import the host", () => {
|
||||
const src = readFileSync(LEAF, "utf8");
|
||||
for (const sym of [
|
||||
"mergeUpstreamExtraHeaders",
|
||||
"setUserAgentHeader",
|
||||
"isOpenAICompatibleEndpoint",
|
||||
"stripStainlessHeadersForOpenAICompat",
|
||||
]) {
|
||||
assert.match(src, new RegExp(`export function ${sym}\\b`));
|
||||
}
|
||||
assert.doesNotMatch(src, /from "\.\.\/base\.ts"/);
|
||||
});
|
||||
|
||||
test("host re-exports all 6 header helpers for external importers", () => {
|
||||
const host = readFileSync(HOST, "utf8");
|
||||
for (const sym of [
|
||||
"mergeUpstreamExtraHeaders",
|
||||
"getCustomUserAgent",
|
||||
"setUserAgentHeader",
|
||||
"applyConfiguredUserAgent",
|
||||
"isOpenAICompatibleEndpoint",
|
||||
"stripStainlessHeadersForOpenAICompat",
|
||||
]) {
|
||||
assert.match(host, new RegExp(`\\b${sym}\\b`));
|
||||
}
|
||||
assert.match(host, /from "\.\/base\/headers\.ts"/);
|
||||
});
|
||||
|
||||
test("header helpers behave via base.ts (the public import path)", async () => {
|
||||
const mod = await import("../../open-sse/executors/base.ts");
|
||||
assert.equal(mod.isOpenAICompatibleEndpoint("openai-compatible-x", "https://x/y"), true);
|
||||
const headers: Record<string, string> = { "x-stainless-lang": "js", "User-Agent": "openai-node" };
|
||||
const stripped = mod.stripStainlessHeadersForOpenAICompat(
|
||||
headers,
|
||||
"openai-compatible-x",
|
||||
"https://x/v1/chat/completions"
|
||||
);
|
||||
assert.ok(stripped.includes("x-stainless-lang"));
|
||||
assert.equal(headers["x-stainless-lang"], undefined);
|
||||
});
|
||||
@@ -1,34 +0,0 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
// Split-guard for the base executor reasoning-effort extraction.
|
||||
// The provider-aware reasoning_effort sanitation lives in base/reasoningEffort.ts
|
||||
// (deps are config/services only — no host import, no cycle). base.ts re-exports it
|
||||
// so external importers (mimoThinking + tests) keep the "./base.ts" path.
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const EXE = join(HERE, "../../open-sse/executors");
|
||||
|
||||
test("leaf hosts sanitizeReasoningEffortForProvider and does not import the host", () => {
|
||||
const src = readFileSync(join(EXE, "base/reasoningEffort.ts"), "utf8");
|
||||
assert.match(src, /export function sanitizeReasoningEffortForProvider\b/);
|
||||
assert.doesNotMatch(src, /from "\.\.\/base\.ts"/);
|
||||
});
|
||||
|
||||
test("base.ts re-exports it for external importers", () => {
|
||||
const host = readFileSync(join(EXE, "base.ts"), "utf8");
|
||||
assert.match(
|
||||
host,
|
||||
/export \{[^}]*sanitizeReasoningEffortForProvider[^}]*\} from "\.\/base\/reasoningEffort\.ts"/s
|
||||
);
|
||||
});
|
||||
|
||||
test("both import paths resolve to the same function", async () => {
|
||||
const viaHost = (await import("../../open-sse/executors/base.ts"))
|
||||
.sanitizeReasoningEffortForProvider;
|
||||
const viaLeaf = (await import("../../open-sse/executors/base/reasoningEffort.ts"))
|
||||
.sanitizeReasoningEffortForProvider;
|
||||
assert.equal(viaHost, viaLeaf);
|
||||
});
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
requiresWebSessionCredential,
|
||||
getWebSessionCredentialRequirement,
|
||||
hasUsableWebSessionCredential,
|
||||
resolveWebSessionImportApiKey,
|
||||
} from "../../src/shared/providers/webSessionCredentials.ts";
|
||||
|
||||
describe("bulkWebSessionImportSchema", () => {
|
||||
@@ -150,63 +149,24 @@ describe("web-session credential helpers", () => {
|
||||
}),
|
||||
true
|
||||
);
|
||||
assert.equal(hasUsableWebSessionCredential("chatgpt-web", { cookie: "" }), false);
|
||||
assert.equal(hasUsableWebSessionCredential("chatgpt-web", {}), false);
|
||||
assert.equal(
|
||||
hasUsableWebSessionCredential("chatgpt-web", { cookie: "" }),
|
||||
false
|
||||
);
|
||||
assert.equal(
|
||||
hasUsableWebSessionCredential("chatgpt-web", {}),
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
it("hasUsableWebSessionCredential validates token data correctly", () => {
|
||||
assert.equal(hasUsableWebSessionCredential("deepseek-web", { token: "my-token" }), true);
|
||||
assert.equal(hasUsableWebSessionCredential("deepseek-web", { token: " " }), false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveWebSessionImportApiKey (token-kind imports must populate apiKey)", () => {
|
||||
// Regression: the bulk web-session import stored token-kind credentials
|
||||
// (deepseek-web, copilot-web, t3-chat-web, …) only in providerSpecificData and
|
||||
// left apiKey null. Both the connection validator (validateDeepSeekWebProvider)
|
||||
// and the executor (extractUserToken → credentials.apiKey) read the token from
|
||||
// apiKey, so imported token-kind connections were never recognized. Token-kind
|
||||
// must resolve the credential into apiKey; cookie-kind keeps apiKey null (those
|
||||
// executors read providerSpecificData.cookie).
|
||||
it("returns the credential for a token-kind provider (deepseek-web)", () => {
|
||||
const req = getWebSessionCredentialRequirement("deepseek-web");
|
||||
assert.equal(
|
||||
resolveWebSessionImportApiKey(req, "j9CVFGvd8Y/deadbeeftoken"),
|
||||
"j9CVFGvd8Y/deadbeeftoken"
|
||||
hasUsableWebSessionCredential("deepseek-web", { token: "my-token" }),
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves a JSON-wrapped userToken verbatim (extractUserToken unwraps it later)", () => {
|
||||
const req = getWebSessionCredentialRequirement("deepseek-web");
|
||||
const blob = '{"value":"abc123","__version":"0"}';
|
||||
assert.equal(resolveWebSessionImportApiKey(req, blob), blob);
|
||||
});
|
||||
|
||||
it("returns null for cookie-kind providers (they read providerSpecificData.cookie)", () => {
|
||||
assert.equal(
|
||||
resolveWebSessionImportApiKey(
|
||||
getWebSessionCredentialRequirement("claude-web"),
|
||||
"sessionKey=abc"
|
||||
),
|
||||
null
|
||||
hasUsableWebSessionCredential("deepseek-web", { token: " " }),
|
||||
false
|
||||
);
|
||||
assert.equal(
|
||||
resolveWebSessionImportApiKey(
|
||||
getWebSessionCredentialRequirement("chatgpt-web"),
|
||||
"__Secure-next-auth.session-token=abc"
|
||||
),
|
||||
null
|
||||
);
|
||||
});
|
||||
|
||||
it("returns null for a whitespace-only or missing credential", () => {
|
||||
const req = getWebSessionCredentialRequirement("deepseek-web");
|
||||
assert.equal(resolveWebSessionImportApiKey(req, " "), null);
|
||||
assert.equal(resolveWebSessionImportApiKey(null, "anything"), null);
|
||||
});
|
||||
|
||||
it("trims surrounding whitespace from the stored token", () => {
|
||||
const req = getWebSessionCredentialRequirement("copilot-web");
|
||||
assert.equal(resolveWebSessionImportApiKey(req, " tok-123 "), "tok-123");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -66,29 +66,6 @@ test("Fable 5 catalog exposes claude-fable-5 in cc and kiro providers with match
|
||||
assert.ok(kiroPricing["claude-fable-5"], "kiro pricing must include claude-fable-5");
|
||||
});
|
||||
|
||||
test("Sonnet 5 catalog exposes claude-sonnet-5 across cc/kiro/anthropic/blackbox with Sonnet-tier pricing", () => {
|
||||
// Sonnet 5 must be wired everywhere the last flagship (Fable 5) was — but as a
|
||||
// Sonnet-tier model: $3/$15 pricing (NOT the Opus/Fable $15/$75), 1M ctx / 128K out.
|
||||
for (const providerId of ["cc", "kiro", "anthropic", "blackbox"]) {
|
||||
const ids = new Set(getModelsByProviderId(providerId).map((m) => m.id));
|
||||
assert.ok(ids.has("claude-sonnet-5"), `${providerId} must expose claude-sonnet-5`);
|
||||
}
|
||||
|
||||
const kiroSonnet5 = getModelsByProviderId("kiro").find((m) => m.id === "claude-sonnet-5");
|
||||
assert.equal(kiroSonnet5?.contextLength, 1000000);
|
||||
assert.equal(kiroSonnet5?.maxOutputTokens, 128000);
|
||||
|
||||
const ccPricing = (DEFAULT_PRICING as Record<string, Record<string, unknown>>).cc;
|
||||
assert.ok(ccPricing["claude-sonnet-5"], "cc pricing must include claude-sonnet-5");
|
||||
|
||||
const kiroPricing = (DEFAULT_PRICING as Record<string, Record<string, unknown>>).kiro;
|
||||
const kiroSonnet5Price = kiroPricing["claude-sonnet-5"] as { input: number; output: number };
|
||||
assert.ok(kiroSonnet5Price, "kiro pricing must include claude-sonnet-5");
|
||||
// Sonnet-tier, not Opus-tier — guards against copying Fable 5's $15/$75.
|
||||
assert.equal(kiroSonnet5Price.input, 3.0);
|
||||
assert.equal(kiroSonnet5Price.output, 15.0);
|
||||
});
|
||||
|
||||
test("Kiro catalog exposes Claude Opus 4.8 alongside 4.7 with matching pricing", () => {
|
||||
const models = getModelsByProviderId("kiro");
|
||||
const ids = new Set(models.map((model) => model.id));
|
||||
|
||||
@@ -452,7 +452,7 @@ test("DefaultExecutor uses CC-compatible path and headers", () => {
|
||||
assert.equal(headers.Authorization, "Bearer sk-test");
|
||||
assert.equal(headers["x-api-key"], undefined);
|
||||
assert.equal(headers["X-Claude-Code-Session-Id"], "session-3");
|
||||
assert.equal(headers.Accept, "text/event-stream");
|
||||
assert.equal(headers.Accept, "application/json");
|
||||
});
|
||||
|
||||
test("validateProviderApiKey uses CC skeleton request after /models fallback", async () => {
|
||||
@@ -493,7 +493,7 @@ test("validateProviderApiKey uses CC skeleton request after /models fallback", a
|
||||
assert.equal(calls[1].body.stream, true);
|
||||
assert.equal(calls[1].headers.Authorization, "Bearer sk-test");
|
||||
assert.equal(calls[1].headers["x-api-key"], undefined);
|
||||
assert.equal(calls[1].headers.Accept, "text/event-stream");
|
||||
assert.equal(calls[1].headers.Accept, "application/json");
|
||||
});
|
||||
|
||||
test("handleChatCore forces SSE upstream for CC compatible providers while returning JSON to non-stream clients", async () => {
|
||||
@@ -571,7 +571,7 @@ test("handleChatCore forces SSE upstream for CC compatible providers while retur
|
||||
|
||||
assert.equal(result.success, true);
|
||||
assert.equal(calls.length, 1);
|
||||
assert.equal(calls[0].headers.Accept, "text/event-stream");
|
||||
assert.equal(calls[0].headers.Accept, "application/json");
|
||||
assert.equal(calls[0].body.stream, true);
|
||||
assert.equal(calls[0].body.stream_options, undefined);
|
||||
assert.equal(JSON.stringify(calls[0].body).includes('"cache_control"'), false);
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const { APIKEY_PROVIDERS } = await import("../../src/shared/constants/providers.ts");
|
||||
const { REGISTRY: providerRegistry } = await import("../../open-sse/config/providerRegistry.ts");
|
||||
|
||||
const CHARM_HYPER_CHAT_URL = "https://hyper.charm.land/v1/chat/completions";
|
||||
const CHARM_HYPER_MODELS_URL = "https://hyper.charm.land/v1/models";
|
||||
|
||||
test("Charm Hyper is registered as a free API-key provider", () => {
|
||||
const entry = APIKEY_PROVIDERS["charm-hyper"];
|
||||
assert.ok(entry, "APIKEY_PROVIDERS['charm-hyper'] must be defined");
|
||||
assert.equal(entry.id, "charm-hyper");
|
||||
assert.equal(entry.alias, "charm-hyper");
|
||||
assert.equal(entry.name, "Charm Hyper");
|
||||
assert.equal(entry.website, "https://hyper.charm.land");
|
||||
assert.equal(entry.hasFree, true);
|
||||
assert.equal(entry.passthroughModels, true);
|
||||
});
|
||||
|
||||
test("Charm Hyper registry entry uses OpenAI format with bearer API-key auth", () => {
|
||||
const entry = providerRegistry["charm-hyper"];
|
||||
assert.ok(entry, "providerRegistry['charm-hyper'] must be defined");
|
||||
assert.equal(entry.id, "charm-hyper");
|
||||
assert.equal(entry.alias, "charm-hyper");
|
||||
assert.equal(entry.format, "openai");
|
||||
assert.equal(entry.executor, "default");
|
||||
assert.equal(entry.authType, "apikey");
|
||||
assert.equal(entry.authHeader, "bearer");
|
||||
assert.equal(entry.baseUrl, CHARM_HYPER_CHAT_URL);
|
||||
assert.equal(entry.modelsUrl, CHARM_HYPER_MODELS_URL);
|
||||
assert.equal(entry.passthroughModels, true);
|
||||
});
|
||||
@@ -499,9 +499,7 @@ test("chatCore keeps Responses-native Codex payloads in native passthrough mode"
|
||||
|
||||
assert.equal(result.success, true);
|
||||
assert.match(call.url, /\/responses$/);
|
||||
assert.deepEqual(call.body.input, [
|
||||
{ type: "message", role: "user", content: [{ type: "input_text", text: "ship it" }] },
|
||||
]);
|
||||
assert.deepEqual(call.body.input, [{ type: "message", role: "user", content: [{ type: "input_text", text: "ship it" }] }]);
|
||||
assert.equal(call.body.instructions, "custom system prompt");
|
||||
assert.equal(call.body.store, false);
|
||||
assert.deepEqual(call.body.metadata, { source: "codex-client" });
|
||||
@@ -673,8 +671,9 @@ test("chatCore builds Claude Code-compatible upstream requests for CC providers"
|
||||
});
|
||||
|
||||
assert.equal(result.success, true);
|
||||
assert.equal(call.headers.Accept ?? call.headers.accept, "text/event-stream");
|
||||
assert.deepEqual([call.body.stream, call.body.context_management], [true, undefined]);
|
||||
assert.equal(call.headers.Accept ?? call.headers.accept, "application/json");
|
||||
assert.equal(call.body.stream, true);
|
||||
assert.equal(call.body.context_management, undefined);
|
||||
assert.equal(call.body.system.length, 1);
|
||||
assert.match(call.body.system[0].text, /Claude Agent SDK/);
|
||||
assert.equal(typeof call.body.metadata.user_id, "string");
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
// Split-guard for the chatgpt-web model-mapping extraction.
|
||||
// The static model maps + pure thinking-effort resolvers live in the pure leaf
|
||||
// chatgpt-web/models.ts (no module state). Host imports the two it uses back.
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const EXE = join(HERE, "../../open-sse/executors");
|
||||
const HOST = join(EXE, "chatgpt-web.ts");
|
||||
const LEAF = join(EXE, "chatgpt-web/models.ts");
|
||||
|
||||
test("leaf hosts the model maps + resolvers and does not import the host", () => {
|
||||
const src = readFileSync(LEAF, "utf8");
|
||||
for (const sym of ["MODEL_MAP", "resolveChatGptModel", "resolveThinkingEffort"]) {
|
||||
assert.match(src, new RegExp(`export (const|function) ${sym}\\b`));
|
||||
}
|
||||
assert.doesNotMatch(src, /from "\.\.\/chatgpt-web\.ts"/);
|
||||
});
|
||||
|
||||
test("host imports the resolvers back from the leaf", () => {
|
||||
const host = readFileSync(HOST, "utf8");
|
||||
assert.match(host, /from "\.\/chatgpt-web\/models\.ts"/);
|
||||
});
|
||||
|
||||
test("resolveChatGptModel maps a dot-form model id to a chatgpt slug", async () => {
|
||||
const { resolveChatGptModel, MODEL_MAP } =
|
||||
await import("../../open-sse/executors/chatgpt-web/models.ts");
|
||||
const firstKey = Object.keys(MODEL_MAP)[0];
|
||||
const resolved = resolveChatGptModel(firstKey);
|
||||
assert.equal(typeof resolved.slug, "string");
|
||||
assert.ok(resolved.slug.length > 0);
|
||||
});
|
||||
@@ -1,55 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const mod = await import("../../scripts/check/check-pr-evidence.mjs");
|
||||
const { evaluatePrBody } = mod;
|
||||
const SCRIPT = path.resolve(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
"../../scripts/check/check-pr-evidence.mjs"
|
||||
);
|
||||
|
||||
function run(body) {
|
||||
try {
|
||||
const out = execFileSync("node", [SCRIPT], {
|
||||
encoding: "utf8",
|
||||
env: { ...process.env, PR_BODY: body },
|
||||
});
|
||||
return { code: 0, out };
|
||||
} catch (err) {
|
||||
return { code: err.status ?? 1, out: `${err.stdout || ""}${err.stderr || ""}` };
|
||||
}
|
||||
}
|
||||
|
||||
test("evaluatePrBody: no outcome claim → pass (no evidence required)", () => {
|
||||
const r = evaluatePrBody("Adds a helper module.");
|
||||
assert.equal(r.result, "pass");
|
||||
assert.match(r.reason, /no evidence required/i);
|
||||
});
|
||||
|
||||
test("evaluatePrBody: outcome claim + evidence block → pass", () => {
|
||||
const r = evaluatePrBody("Tests pass.\n\n## Evidence\n```\ntests 20 / pass 20 / fail 0\n```");
|
||||
assert.equal(r.result, "pass");
|
||||
});
|
||||
|
||||
test("evaluatePrBody: outcome claim without evidence → fail", () => {
|
||||
const r = evaluatePrBody("All 20 tests pass and 0 errors.");
|
||||
assert.equal(r.result, "fail");
|
||||
});
|
||||
|
||||
test("the FAIL report explains that editing the body does not re-run the gate (push instead)", () => {
|
||||
const { code, out } = run("All 20 tests pass and 0 errors."); // claim, no evidence
|
||||
assert.equal(code, 1, "gate fails on a claim with no evidence");
|
||||
assert.match(out, /Result: FAIL/);
|
||||
assert.match(out, /does NOT re-run this gate/);
|
||||
assert.match(out, /push a commit/i);
|
||||
});
|
||||
|
||||
test("the hint does NOT appear when the gate passes", () => {
|
||||
const { code, out } = run("Tests pass.\n\n## Evidence\n```\ntests 20 / pass 20 / fail 0\n```");
|
||||
assert.equal(code, 0);
|
||||
assert.match(out, /Result: PASS/);
|
||||
assert.doesNotMatch(out, /does NOT re-run this gate/);
|
||||
});
|
||||
@@ -1,178 +0,0 @@
|
||||
/**
|
||||
* TDD test for the Claude Code auto-mode classifier compat mode (opt-in, default off).
|
||||
*
|
||||
* Claude Code's `--permission-mode auto` sends an internal `/v1/messages` security-classifier
|
||||
* request and requires the response to START with the literal token `<block>no</block>` (ALLOW)
|
||||
* or `<block>yes</block>` (BLOCK) — anything else is unparseable and Claude Code fails closed
|
||||
* with "Auto mode could not evaluate this action and is blocking it for safety".
|
||||
*
|
||||
* When a combo/fallback route sends the classifier call to a cheap model that returns 200 with
|
||||
* empty content, the well-formed-but-empty Claude message OmniRoute produces still fails that
|
||||
* parser. With `claudeClassifierCompat` set to "auto" (or "always"), handleChatCore detects the
|
||||
* classifier request and short-circuits with a synthetic ALLOW response — WITHOUT ever calling
|
||||
* the upstream provider. Default is "off": nothing changes unless an operator explicitly opts in.
|
||||
*/
|
||||
|
||||
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-claude-classifier-compat-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const { updateSettings } = await import("../../src/lib/db/settings.ts");
|
||||
const { handleChatCore } = await import("../../open-sse/handlers/chatCore.ts");
|
||||
const { shouldDefaultAllowClassifier, buildDefaultAllowClaudeMessage } = await import(
|
||||
"../../open-sse/handlers/chatCore/claudeClassifierCompat.ts"
|
||||
);
|
||||
const { FORMATS } = await import("../../open-sse/translator/formats.ts");
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
function noopLog() {
|
||||
return { debug() {}, info() {}, warn() {}, error() {} };
|
||||
}
|
||||
|
||||
// Shape of the classifier request Claude Code's `--permission-mode auto` sends internally:
|
||||
// a Claude Messages request carrying the security-monitor system prompt AND `</block>` as a
|
||||
// stop sequence — the two independent signals the compat detector relies on.
|
||||
const CLASSIFIER_BODY = {
|
||||
model: "claude-3-5-haiku-20241022",
|
||||
stream: false,
|
||||
system: [
|
||||
{
|
||||
type: "text",
|
||||
text: "You are a security monitor for autonomous AI coding agents. Evaluate the following action and respond with <block>yes</block> or <block>no</block>.",
|
||||
},
|
||||
],
|
||||
stop_sequences: ["</block>"],
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "<transcript>WebFetch https://example.com</transcript>" }],
|
||||
},
|
||||
],
|
||||
max_tokens: 8,
|
||||
};
|
||||
|
||||
test.after(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
// ─── Settings default is opt-in (off) ────────────────────────────────────────
|
||||
// Runs FIRST, before any updateSettings() write, so it reads the pristine DB.
|
||||
// (DATA_DIR freezes at the first DB open, so a later "fresh dir" swap would still
|
||||
// read this same DB — hence assert the default up front.)
|
||||
|
||||
test("settings default: claudeClassifierCompat is 'off' (opt-in)", async () => {
|
||||
const { getSettings } = await import("../../src/lib/db/settings.ts");
|
||||
const settings = await getSettings();
|
||||
assert.equal(settings.claudeClassifierCompat, "off", "claudeClassifierCompat defaults to off");
|
||||
});
|
||||
|
||||
// ─── Pure detector: shouldDefaultAllowClassifier ─────────────────────────────
|
||||
|
||||
test("detector: off never short-circuits (pass-through preserved by default)", () => {
|
||||
assert.equal(shouldDefaultAllowClassifier(FORMATS.CLAUDE, CLASSIFIER_BODY, "off"), false);
|
||||
assert.equal(shouldDefaultAllowClassifier(FORMATS.CLAUDE, CLASSIFIER_BODY, undefined), false);
|
||||
});
|
||||
|
||||
test("detector: auto fires on the security-monitor system-prompt marker", () => {
|
||||
const body = {
|
||||
system: [{ type: "text", text: "You are a security monitor for autonomous AI coding agents." }],
|
||||
stop_sequences: [],
|
||||
};
|
||||
assert.equal(shouldDefaultAllowClassifier(FORMATS.CLAUDE, body, "auto"), true);
|
||||
});
|
||||
|
||||
test("detector: auto fires on the </block> stop_sequence token", () => {
|
||||
const body = { system: [{ type: "text", text: "unrelated" }], stop_sequences: ["</block>"] };
|
||||
assert.equal(shouldDefaultAllowClassifier(FORMATS.CLAUDE, body, "auto"), true);
|
||||
});
|
||||
|
||||
test("detector: auto does NOT fire on a regular Claude request (no marker, no </block>)", () => {
|
||||
const body = {
|
||||
system: [{ type: "text", text: "You are a helpful coding assistant." }],
|
||||
stop_sequences: [],
|
||||
messages: [{ role: "user", content: "hello" }],
|
||||
};
|
||||
assert.equal(shouldDefaultAllowClassifier(FORMATS.CLAUDE, body, "auto"), false);
|
||||
});
|
||||
|
||||
test("detector: never fires for non-Claude source formats even in always mode", () => {
|
||||
assert.equal(shouldDefaultAllowClassifier(FORMATS.OPENAI, CLASSIFIER_BODY, "always"), false);
|
||||
});
|
||||
|
||||
test("detector: always fires for every Claude-format request", () => {
|
||||
const plain = { system: [{ type: "text", text: "hi" }], stop_sequences: [] };
|
||||
assert.equal(shouldDefaultAllowClassifier(FORMATS.CLAUDE, plain, "always"), true);
|
||||
});
|
||||
|
||||
// ─── Pure builder: buildDefaultAllowClaudeMessage ────────────────────────────
|
||||
|
||||
test("builder: synthetic message text STARTS WITH <block>no</block>", async () => {
|
||||
const built = buildDefaultAllowClaudeMessage("claude-3-5-haiku-20241022");
|
||||
assert.equal(built.success, true);
|
||||
const payload = (await built.response.json()) as {
|
||||
type: string;
|
||||
role: string;
|
||||
stop_reason: string;
|
||||
content: Array<{ type: string; text?: string }>;
|
||||
};
|
||||
assert.equal(payload.type, "message");
|
||||
assert.equal(payload.role, "assistant");
|
||||
assert.equal(payload.stop_reason, "end_turn");
|
||||
const text = payload.content.find((b) => b.type === "text")?.text ?? "";
|
||||
assert.ok(
|
||||
text.startsWith("<block>no</block>"),
|
||||
`expected synthetic text to start with <block>no</block>, got: ${text}`
|
||||
);
|
||||
assert.ok(!text.includes("<block>yes"), "must not signal BLOCK");
|
||||
});
|
||||
|
||||
// ─── Handler-level: end-to-end short-circuit through handleChatCore ──────────
|
||||
|
||||
test("handler: claudeClassifierCompat=auto short-circuits WITHOUT calling upstream, text starts with <block>no</block>", async () => {
|
||||
await updateSettings({ claudeClassifierCompat: "auto" });
|
||||
|
||||
let fetchCalls = 0;
|
||||
globalThis.fetch = (async () => {
|
||||
fetchCalls++;
|
||||
throw new Error("upstream fetch should NOT be called when the classifier short-circuits");
|
||||
}) as typeof fetch;
|
||||
|
||||
try {
|
||||
const result = await handleChatCore({
|
||||
body: structuredClone(CLASSIFIER_BODY),
|
||||
modelInfo: { provider: "openai", model: "gpt-4o-mini", extendedContext: false },
|
||||
credentials: { apiKey: "sk-test", providerSpecificData: {} },
|
||||
log: noopLog(),
|
||||
clientRawRequest: {
|
||||
endpoint: "/v1/messages",
|
||||
body: structuredClone(CLASSIFIER_BODY),
|
||||
headers: new Headers({ accept: "application/json" }),
|
||||
},
|
||||
userAgent: "unit-test",
|
||||
});
|
||||
|
||||
assert.equal(fetchCalls, 0, "upstream fetch must NOT be called");
|
||||
assert.equal(result.success, true, "handleChatCore must report success");
|
||||
const payload = (await (result as { response: Response }).response.json()) as {
|
||||
type: string;
|
||||
content: Array<{ type: string; text?: string }>;
|
||||
};
|
||||
assert.equal(payload.type, "message");
|
||||
const text = payload.content.find((b) => b.type === "text")?.text ?? "";
|
||||
assert.ok(
|
||||
text.startsWith("<block>no</block>"),
|
||||
`expected classifier response to start with <block>no</block>, got: ${text}`
|
||||
);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
@@ -47,7 +47,7 @@ test("buildClaudeCodeCompatibleHeaders emits bearer auth headers and session id"
|
||||
const streamHeaders = buildClaudeCodeCompatibleHeaders("sk-demo", true, "session-123");
|
||||
const jsonHeaders = buildClaudeCodeCompatibleHeaders("sk-demo", false);
|
||||
|
||||
assert.equal(streamHeaders.Accept, "text/event-stream");
|
||||
assert.equal(streamHeaders.Accept, "application/json");
|
||||
assert.equal(streamHeaders.Authorization, "Bearer sk-demo");
|
||||
assert.equal(streamHeaders["x-api-key"], undefined);
|
||||
assert.equal(streamHeaders["X-Claude-Code-Session-Id"], "session-123");
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
// Split-guard for the claude-web executor payload extraction.
|
||||
// The pure payload types + transforms + default tools/style live in the leaf
|
||||
// claude-web/payload.ts (no host state, no fetch/auth). Host imports back the
|
||||
// symbols it uses (ClaudeWebRequestPayload, transformToClaude, transformFromClaude).
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const EXE = join(HERE, "../../open-sse/executors");
|
||||
const HOST = join(EXE, "claude-web.ts");
|
||||
const LEAF = join(EXE, "claude-web/payload.ts");
|
||||
|
||||
test("leaf hosts the payload builders/transforms and does not import the host", () => {
|
||||
const src = readFileSync(LEAF, "utf8");
|
||||
for (const sym of ["transformToClaude", "transformFromClaude", "getDefaultTools"]) {
|
||||
assert.match(src, new RegExp(`export function ${sym}\\b`));
|
||||
}
|
||||
assert.match(src, /export interface ClaudeWebRequestPayload\b/);
|
||||
assert.doesNotMatch(src, /from "\.\.\/claude-web\.ts"/);
|
||||
});
|
||||
|
||||
test("host imports the transforms back from the leaf", () => {
|
||||
const host = readFileSync(HOST, "utf8");
|
||||
assert.match(host, /from "\.\/claude-web\/payload\.ts"/);
|
||||
});
|
||||
|
||||
test("transformToClaude builds a Claude-web payload with model + tools", async () => {
|
||||
const { transformToClaude } = await import("../../open-sse/executors/claude-web/payload.ts");
|
||||
const payload = transformToClaude(
|
||||
{ messages: [{ role: "user", content: "hi" }] },
|
||||
"claude-sonnet-4-6"
|
||||
);
|
||||
assert.equal(typeof payload, "object");
|
||||
assert.ok(Array.isArray(payload.tools));
|
||||
});
|
||||
@@ -44,7 +44,6 @@ const NOT_ACP_SPAWNABLE_IDS = [
|
||||
"roo",
|
||||
"jcode",
|
||||
"deepseek-tui",
|
||||
"codewhale",
|
||||
"smelt",
|
||||
"pi",
|
||||
"hermes-agent",
|
||||
|
||||
@@ -30,7 +30,7 @@ test(`CLI_TOOLS has exactly ${EXPECTED_AGENT_COUNT} agent entries`, () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("CLI_TOOLS total code entries (including none) equals 24 (20 visible + 4 none)", () => {
|
||||
test("CLI_TOOLS total code entries (including none) equals 22 (18 visible + 4 none)", () => {
|
||||
// code-none entries: antigravity, kiro, cursor (app), hermes (simple guide)
|
||||
const codeNone = codeAll.filter((t) => t.baseUrlSupport === "none");
|
||||
assert.equal(
|
||||
@@ -38,11 +38,11 @@ test("CLI_TOOLS total code entries (including none) equals 24 (20 visible + 4 no
|
||||
4,
|
||||
`Expected 4 code entries with baseUrlSupport='none', got ${codeNone.length}: ${codeNone.map((t) => t.id).join(", ")}`
|
||||
);
|
||||
assert.equal(codeAll.length, 24, `Expected 24 total code entries, got ${codeAll.length}`);
|
||||
assert.equal(codeAll.length, 22, `Expected 22 total code entries, got ${codeAll.length}`);
|
||||
});
|
||||
|
||||
test("CLI_TOOLS total (code + agent) = 30", () => {
|
||||
assert.equal(all.length, 30, `Expected 30 total entries, got ${all.length}`);
|
||||
test("CLI_TOOLS total (code + agent) = 28", () => {
|
||||
assert.equal(all.length, 28, `Expected 28 total entries, got ${all.length}`);
|
||||
});
|
||||
|
||||
test("All code-none entries have configType mitm OR are legacy excluded entries", () => {
|
||||
@@ -66,7 +66,7 @@ test("All agent entries have baseUrlSupport 'full' or 'partial' (no agent is 'no
|
||||
}
|
||||
});
|
||||
|
||||
test("The 20 visible code entries match D15 list exactly (+ crush + codewhale)", () => {
|
||||
test("The 18 visible code entries match D15 list exactly", () => {
|
||||
const d15List = new Set([
|
||||
"claude",
|
||||
"codex",
|
||||
@@ -79,7 +79,6 @@ test("The 20 visible code entries match D15 list exactly (+ crush + codewhale)",
|
||||
"forge",
|
||||
"jcode",
|
||||
"deepseek-tui",
|
||||
"codewhale",
|
||||
"opencode",
|
||||
"droid",
|
||||
"copilot",
|
||||
@@ -87,7 +86,6 @@ test("The 20 visible code entries match D15 list exactly (+ crush + codewhale)",
|
||||
"smelt",
|
||||
"pi",
|
||||
"custom",
|
||||
"crush",
|
||||
]);
|
||||
const visibleIds = new Set(codeVisible.map((t) => t.id));
|
||||
for (const id of d15List) {
|
||||
|
||||
@@ -40,27 +40,12 @@ function readConfig() {
|
||||
return JSON.parse(fs.readFileSync(path.join(CONFIG_DIR, "opencode.json"), "utf8"));
|
||||
}
|
||||
|
||||
// #5959-class deflake: the command under test prints CLI progress with multi-byte
|
||||
// glyphs (printInfo/printSuccess "✔"/printError "✖" via console.log). Under the
|
||||
// node:test runner those stdout writes interleave with the child's V8-serialized
|
||||
// report frames and can corrupt the stream ("Unable to deserialize cloned data
|
||||
// due to invalid or unsupported version"). No test here asserts on stdout, so
|
||||
// silence the stdout-writing console methods for the duration of this file
|
||||
// (same pattern as tests/unit/cli/setup-claude.test.ts, #6019/#6021).
|
||||
const _console = { log: console.log, info: console.info, warn: console.warn };
|
||||
|
||||
describe("omniroute setup opencode", () => {
|
||||
before(() => {
|
||||
console.log = () => {};
|
||||
console.info = () => {};
|
||||
console.warn = () => {};
|
||||
makeFakePluginDist();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
console.log = _console.log;
|
||||
console.info = _console.info;
|
||||
console.warn = _console.warn;
|
||||
try {
|
||||
fs.rmSync(FIXTURE_ROOT, { recursive: true, force: true });
|
||||
} catch {
|
||||
@@ -87,11 +72,7 @@ describe("omniroute setup opencode", () => {
|
||||
const [modulePath, options] = cfg.plugin[0];
|
||||
assert.equal(modulePath, "./plugins/omniroute/dist/index.js");
|
||||
assert.equal(options.providerId, "omniroute");
|
||||
assert.equal(
|
||||
options.baseURL,
|
||||
"http://10.0.0.5:20128",
|
||||
"--base-url flag must reach the registered entry"
|
||||
);
|
||||
assert.equal(options.baseURL, "http://10.0.0.5:20128", "--base-url flag must reach the registered entry");
|
||||
});
|
||||
|
||||
it("is idempotent: re-running updates the entry in place instead of duplicating it", async () => {
|
||||
@@ -104,15 +85,10 @@ describe("omniroute setup opencode", () => {
|
||||
|
||||
const cfg = readConfig();
|
||||
const omniEntries = cfg.plugin.filter(
|
||||
(p: unknown) =>
|
||||
Array.isArray(p) && (p[1] as { providerId?: string })?.providerId === "omniroute"
|
||||
(p: unknown) => Array.isArray(p) && (p[1] as { providerId?: string })?.providerId === "omniroute"
|
||||
);
|
||||
assert.equal(omniEntries.length, 1, "re-run must not duplicate the entry");
|
||||
assert.equal(
|
||||
omniEntries[0][1].baseURL,
|
||||
"http://10.0.0.9:20128",
|
||||
"re-run updates baseURL in place"
|
||||
);
|
||||
assert.equal(omniEntries[0][1].baseURL, "http://10.0.0.9:20128", "re-run updates baseURL in place");
|
||||
});
|
||||
|
||||
it("removes the legacy opencode-omniroute-auth entry (#3711) and preserves unrelated plugins", async () => {
|
||||
|
||||
@@ -1,233 +0,0 @@
|
||||
/**
|
||||
* Unit tests for the "Crush" CLI tool dashboard entry (ported from upstream
|
||||
* decolua/9router#1233). Mirrors the assertions used for the "pi" tool in
|
||||
* tests/unit/cli-tools-schema.test.ts, plus a GET/POST/DELETE round-trip for
|
||||
* /api/cli-tools/crush-settings modeled on tests/integration/cli-settings-pi.test.ts.
|
||||
*/
|
||||
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";
|
||||
|
||||
// ── Catalog shape ────────────────────────────────────────────────────────────
|
||||
|
||||
test("CLI_TOOLS contains a 'crush' entry modeled on 'pi'", async () => {
|
||||
const { CLI_TOOLS } = await import("../../src/shared/constants/cliTools.ts");
|
||||
const crush = CLI_TOOLS["crush"];
|
||||
assert.ok(crush, "crush entry must exist in CLI_TOOLS");
|
||||
assert.equal(crush.id, "crush");
|
||||
assert.equal(crush.name, "Crush");
|
||||
assert.equal(crush.configType, "custom");
|
||||
assert.equal(crush.category, "code");
|
||||
assert.equal(crush.acpSpawnable, false);
|
||||
assert.equal(crush.baseUrlSupport, "full");
|
||||
assert.equal(crush.defaultCommand, "crush");
|
||||
assert.equal(typeof crush.description, "string");
|
||||
assert.ok(crush.description.length > 0);
|
||||
assert.equal(typeof crush.docsUrl, "string");
|
||||
assert.ok(crush.docsUrl.length > 0);
|
||||
});
|
||||
|
||||
test("getCliTool('crush') resolves the catalog entry", async () => {
|
||||
const { getCliTool } = await import("../../src/shared/constants/cliTools.ts");
|
||||
const crush = getCliTool("crush");
|
||||
assert.ok(crush);
|
||||
assert.equal(crush.id, "crush");
|
||||
});
|
||||
|
||||
// ── Runtime config path reconciliation with setup-crush.mjs ─────────────────
|
||||
|
||||
test("getCliConfigPaths('crush') resolves to ~/.config/crush/crush.json (matches setup-crush.mjs default)", async () => {
|
||||
const { getCliConfigPaths } = await import("../../src/shared/services/cliRuntime.ts");
|
||||
const paths = getCliConfigPaths("crush");
|
||||
assert.ok(paths);
|
||||
assert.ok(
|
||||
paths.config?.endsWith(path.join(".config", "crush", "crush.json")),
|
||||
`Expected config path to end with .config/crush/crush.json, got: ${paths.config}`
|
||||
);
|
||||
});
|
||||
|
||||
// ── /api/cli-tools/crush-settings round-trip ─────────────────────────────────
|
||||
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-crush-settings-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
process.env.API_KEY_SECRET = "test-api-key-secret-crush";
|
||||
process.env.JWT_SECRET = "test-jwt-secret-crush";
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const localDb = await import("../../src/lib/localDb.ts");
|
||||
|
||||
const { GET, POST, DELETE } = await import("../../src/app/api/cli-tools/crush-settings/route.ts");
|
||||
|
||||
async function resetStorage() {
|
||||
delete process.env.INITIAL_PASSWORD;
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
async function enableAuth() {
|
||||
process.env.INITIAL_PASSWORD = "test-bootstrap";
|
||||
await localDb.updateSettings({ requireLogin: true, password: "" });
|
||||
}
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await resetStorage();
|
||||
});
|
||||
|
||||
test("crush-settings GET: returns 401 when auth required and no token", async () => {
|
||||
await enableAuth();
|
||||
const res = await GET(new Request("http://localhost/api/cli-tools/crush-settings"));
|
||||
assert.equal(res.status, 401, `Expected 401, got ${res.status}`);
|
||||
});
|
||||
|
||||
test("crush-settings GET: returns 200 when auth not required", async () => {
|
||||
const res = await GET(new Request("http://localhost/api/cli-tools/crush-settings"));
|
||||
assert.equal(res.status, 200, `Expected 200, got ${res.status}`);
|
||||
const body = await res.json();
|
||||
assert.ok(
|
||||
"installed" in body || "config" in body,
|
||||
"Response should contain installed or config field"
|
||||
);
|
||||
});
|
||||
|
||||
test("crush-settings POST: 400 when baseUrl is missing", async () => {
|
||||
const res = await POST(
|
||||
new Request("http://localhost/api/cli-tools/crush-settings", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ apiKey: "sk-test", model: "openai/gpt-5" }),
|
||||
})
|
||||
);
|
||||
assert.equal(res.status, 400, `Expected 400, got ${res.status}`);
|
||||
const body = await res.json();
|
||||
assert.ok(body.error !== undefined);
|
||||
});
|
||||
|
||||
test("crush-settings POST: 400 when model is missing", async () => {
|
||||
const res = await POST(
|
||||
new Request("http://localhost/api/cli-tools/crush-settings", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ baseUrl: "http://localhost:20128", apiKey: "sk-test" }),
|
||||
})
|
||||
);
|
||||
assert.equal(res.status, 400, `Expected 400, got ${res.status}`);
|
||||
});
|
||||
|
||||
test("crush-settings POST: writes crush.json with an openai-compat providers.omniroute block", async () => {
|
||||
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "crush-home-"));
|
||||
const origHome = process.env.HOME;
|
||||
process.env.HOME = tmpHome;
|
||||
|
||||
try {
|
||||
const res = await POST(
|
||||
new Request("http://localhost/api/cli-tools/crush-settings", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
baseUrl: "http://localhost:20128",
|
||||
apiKey: "sk-test-crush-key",
|
||||
model: "openai/gpt-5.4-mini",
|
||||
}),
|
||||
})
|
||||
);
|
||||
assert.ok([200, 403, 500].includes(res.status), `Unexpected status ${res.status}`);
|
||||
if (res.status === 200) {
|
||||
const body = await res.json();
|
||||
assert.equal(body.success, true);
|
||||
const configPath = path.join(tmpHome, ".config", "crush", "crush.json");
|
||||
if (fs.existsSync(configPath)) {
|
||||
const written = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
||||
const provider = written.providers?.omniroute;
|
||||
assert.ok(provider, "providers.omniroute must be written");
|
||||
assert.equal(provider.type, "openai-compat");
|
||||
assert.ok(provider.base_url.includes("localhost:20128"));
|
||||
assert.ok(provider.base_url.endsWith("/v1"));
|
||||
assert.ok(Array.isArray(provider.models) && provider.models.length === 1);
|
||||
assert.equal(provider.models[0].id, "openai/gpt-5.4-mini");
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
process.env.HOME = origHome;
|
||||
fs.rmSync(tmpHome, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("crush-settings DELETE: removes only the omniroute provider entry", async () => {
|
||||
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "crush-home-del-"));
|
||||
const origHome = process.env.HOME;
|
||||
process.env.HOME = tmpHome;
|
||||
|
||||
try {
|
||||
const crushDir = path.join(tmpHome, ".config", "crush");
|
||||
fs.mkdirSync(crushDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(crushDir, "crush.json"),
|
||||
JSON.stringify({
|
||||
providers: {
|
||||
omniroute: {
|
||||
type: "openai-compat",
|
||||
base_url: "http://localhost:20128/v1",
|
||||
api_key: "sk-test",
|
||||
models: [
|
||||
{ id: "openai/gpt-5", name: "OmniRoute: openai/gpt-5", context_window: 128000 },
|
||||
],
|
||||
},
|
||||
other: { type: "openai-compat", base_url: "http://example.com/v1" },
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const res = await DELETE(
|
||||
new Request("http://localhost/api/cli-tools/crush-settings", { method: "DELETE" })
|
||||
);
|
||||
assert.ok([200, 403, 500].includes(res.status), `Expected 200/403/500, got ${res.status}`);
|
||||
if (res.status === 200) {
|
||||
const body = await res.json();
|
||||
assert.equal(body.success, true);
|
||||
const configPath = path.join(crushDir, "crush.json");
|
||||
if (fs.existsSync(configPath)) {
|
||||
const written = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
||||
assert.equal(written.providers?.omniroute, undefined);
|
||||
assert.ok(written.providers?.other, "Unrelated providers must be preserved");
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
process.env.HOME = origHome;
|
||||
fs.rmSync(tmpHome, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("crush-settings: error responses do not leak stack traces", async () => {
|
||||
const badReq = new Request("http://localhost/api/cli-tools/crush-settings", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: "{ bad json }",
|
||||
});
|
||||
const res = await POST(badReq);
|
||||
const bodyStr = JSON.stringify(await res.json());
|
||||
assert.ok(
|
||||
!bodyStr.match(/\s+at\s+\/[^\s]/),
|
||||
"Error response must not contain absolute-path stack traces"
|
||||
);
|
||||
});
|
||||
|
||||
test("crush-settings route.ts: does not call exec() or spawn() directly", () => {
|
||||
const routePath = path.resolve(
|
||||
import.meta.dirname,
|
||||
"../../src/app/api/cli-tools/crush-settings/route.ts"
|
||||
);
|
||||
const content = fs.readFileSync(routePath, "utf-8");
|
||||
assert.ok(!content.match(/\bexec\s*\(/), "Handler must not use exec()");
|
||||
assert.ok(!content.match(/\bspawn\s*\(/), "Handler must not use spawn()");
|
||||
});
|
||||
|
||||
test.after(async () => {
|
||||
await resetStorage();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
delete process.env.DATA_DIR;
|
||||
delete process.env.API_KEY_SECRET;
|
||||
delete process.env.JWT_SECRET;
|
||||
});
|
||||
@@ -1,14 +1,11 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
test("CLI_TOOLS registry contains all expected tools (plan 14 — 30 total + crush + codewhale)", async () => {
|
||||
test("CLI_TOOLS registry contains all expected tools (plan 14 — 28 total)", async () => {
|
||||
const { CLI_TOOLS } = await import("../../src/shared/constants/cliTools.ts");
|
||||
// windsurf and amp removed per plan 14 D17 (MITM backlog plan 11)
|
||||
// New entries added: roo, jcode, deepseek-tui, smelt, pi, aider, forge,
|
||||
// cursor-cli, goose, interpreter, warp, agent-deck (+ hermes-agent already existed)
|
||||
// crush added — ported from upstream decolua/9router#1233
|
||||
// codewhale added 2026-07-02 as a dual entry alongside deepseek-tui
|
||||
// (CodeWhale is the actively-maintained successor to DeepSeek TUI).
|
||||
const expected = [
|
||||
"claude",
|
||||
"codex",
|
||||
@@ -32,14 +29,12 @@ test("CLI_TOOLS registry contains all expected tools (plan 14 — 30 total + cru
|
||||
"roo",
|
||||
"jcode",
|
||||
"deepseek-tui",
|
||||
"codewhale",
|
||||
"smelt",
|
||||
"pi",
|
||||
"goose",
|
||||
"interpreter",
|
||||
"warp",
|
||||
"agent-deck",
|
||||
"crush",
|
||||
];
|
||||
for (const id of expected) {
|
||||
assert.ok(id in CLI_TOOLS, `Missing tool: ${id}`);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { test, before, after } from "node:test";
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
@@ -10,27 +10,6 @@ import {
|
||||
import { buildClaudeEnv, resolveLaunchTarget } from "../../../bin/cli/commands/launch.mjs";
|
||||
import { categoriseModel } from "../../../bin/cli/commands/setup-codex.mjs";
|
||||
|
||||
// #5959 — deflake: `node --test` runs each file in a child process that streams
|
||||
// its report back to the parent as V8-serialized frames on fd 1 (stdout). The CLI
|
||||
// helpers under test (`syncClaudeProfilesFromModels`) print progress via
|
||||
// `console.log`, and that stdout output interleaves with the serialized frames,
|
||||
// corrupting the stream — the parent then throws
|
||||
// "Unable to deserialize cloned data due to invalid or unsupported version" at
|
||||
// file teardown ~50% of runs (all subtests pass; only the file errors). No test
|
||||
// here asserts on stdout, so silence the stdout-writing console methods for the
|
||||
// duration of this file. Restored in `after` for good hygiene.
|
||||
const _console = { log: console.log, info: console.info, warn: console.warn };
|
||||
before(() => {
|
||||
console.log = () => {};
|
||||
console.info = () => {};
|
||||
console.warn = () => {};
|
||||
});
|
||||
after(() => {
|
||||
console.log = _console.log;
|
||||
console.info = _console.info;
|
||||
console.warn = _console.warn;
|
||||
});
|
||||
|
||||
// ── setup-claude profile generation ──────────────────────────────────────────
|
||||
|
||||
test("buildProfileSettings pins the model + base URL + gateway discovery", () => {
|
||||
@@ -89,30 +68,19 @@ test("syncClaudeProfilesFromModels writes directory-per-profile settings + threa
|
||||
}
|
||||
});
|
||||
|
||||
test("syncClaudeProfilesFromModels dry-run writes nothing and reports via the injected log (#5959)", async () => {
|
||||
test("syncClaudeProfilesFromModels dry-run writes nothing", async () => {
|
||||
const claudeHome = await fs.mkdtemp(path.join(os.tmpdir(), "omniroute-claude-dry-"));
|
||||
// The collector keeps the dry-run's multi-byte "──" heading OFF this child
|
||||
// process's stdout: under the node:test runner that write corrupts the V8
|
||||
// serialization stream ~50% of runs (#5959, "Unable to deserialize cloned
|
||||
// data due to invalid or unsupported version").
|
||||
const lines: string[] = [];
|
||||
try {
|
||||
const result = await syncClaudeProfilesFromModels([{ id: "glm/glm-5.2" }], {
|
||||
claudeHome,
|
||||
baseUrl: "http://vps:20128",
|
||||
dryRun: true,
|
||||
log: (line: string) => lines.push(line),
|
||||
});
|
||||
assert.equal(result.written, 1);
|
||||
// Dry-run reports the would-be file + its content through the log sink…
|
||||
assert.equal(lines.length, 2);
|
||||
const settingsPath = path.join(claudeHome, "profiles", "glm52", "settings.json");
|
||||
assert.ok(lines[0].includes(settingsPath));
|
||||
const printed = JSON.parse(lines[1]);
|
||||
assert.equal(printed.model, "glm/glm-5.2");
|
||||
assert.equal(printed.env.ANTHROPIC_BASE_URL, "http://vps:20128");
|
||||
// …and writes nothing to disk.
|
||||
await assert.rejects(fs.stat(settingsPath), /ENOENT/);
|
||||
await assert.rejects(
|
||||
fs.stat(path.join(claudeHome, "profiles", "glm52", "settings.json")),
|
||||
/ENOENT/
|
||||
);
|
||||
} finally {
|
||||
await fs.rm(claudeHome, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
// DefaultExecutor transitively touches the DB layer (provider/key rotation
|
||||
// lookups) at import/call time. Point DATA_DIR at an isolated temp dir
|
||||
// BEFORE importing it so these tests never read/write the operator's real
|
||||
// ~/.omniroute database (see CLAUDE.md "Database Handles in Tests").
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-client-identity-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
|
||||
const {
|
||||
CLIENT_IDENTITY_PROFILES,
|
||||
CLIENT_IDENTITY_PROFILE_OPTIONS,
|
||||
getClientIdentityProfileHeaders,
|
||||
isClientIdentityProfileId,
|
||||
} = await import("../../src/shared/constants/clientIdentityProfiles.ts");
|
||||
const { isForbiddenCustomHeaderName } = await import("../../src/shared/constants/upstreamHeaders.ts");
|
||||
const { DefaultExecutor } = await import("../../open-sse/executors/default.ts");
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
|
||||
test.after(async () => {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("getClientIdentityProfileHeaders: default profile adds no headers", () => {
|
||||
assert.deepEqual(getClientIdentityProfileHeaders("default"), {});
|
||||
assert.deepEqual(getClientIdentityProfileHeaders(undefined), {});
|
||||
assert.deepEqual(getClientIdentityProfileHeaders(null), {});
|
||||
});
|
||||
|
||||
test("getClientIdentityProfileHeaders: unknown profile id falls back to no headers", () => {
|
||||
assert.deepEqual(getClientIdentityProfileHeaders("not-a-real-profile"), {});
|
||||
});
|
||||
|
||||
test("getClientIdentityProfileHeaders: known CLI profiles expose their preset headers", () => {
|
||||
const claudeCli = getClientIdentityProfileHeaders("claude-cli");
|
||||
assert.equal(claudeCli["User-Agent"], "claude-cli/2.1.195 (external, cli)");
|
||||
assert.equal(claudeCli["X-App"], "cli");
|
||||
|
||||
const codexCli = getClientIdentityProfileHeaders("codex-cli");
|
||||
assert.equal(codexCli["User-Agent"], "codex_cli_rs/0.136.0");
|
||||
assert.equal(codexCli.originator, "codex_cli_rs");
|
||||
|
||||
const geminiCli = getClientIdentityProfileHeaders("gemini-cli");
|
||||
assert.equal(geminiCli["User-Agent"], "GeminiCLI/0.1.0 (linux; x64)");
|
||||
});
|
||||
|
||||
test("getClientIdentityProfileHeaders: returns a fresh mutable copy (catalog stays frozen)", () => {
|
||||
const headers = getClientIdentityProfileHeaders("claude-cli");
|
||||
headers["User-Agent"] = "tampered";
|
||||
assert.equal(
|
||||
CLIENT_IDENTITY_PROFILES["claude-cli"].headers["User-Agent"],
|
||||
"claude-cli/2.1.195 (external, cli)"
|
||||
);
|
||||
});
|
||||
|
||||
test("isClientIdentityProfileId / CLIENT_IDENTITY_PROFILE_OPTIONS stay in sync with the catalog", () => {
|
||||
for (const id of Object.keys(CLIENT_IDENTITY_PROFILES)) {
|
||||
assert.equal(isClientIdentityProfileId(id), true);
|
||||
}
|
||||
assert.equal(isClientIdentityProfileId("bogus"), false);
|
||||
|
||||
const optionValues = CLIENT_IDENTITY_PROFILE_OPTIONS.map((o) => o.value);
|
||||
assert.deepEqual(optionValues, Object.keys(CLIENT_IDENTITY_PROFILES));
|
||||
assert.equal(CLIENT_IDENTITY_PROFILE_OPTIONS[0].value, "default");
|
||||
});
|
||||
|
||||
test("a selected profile's headers land in providerSpecificData.customHeaders", () => {
|
||||
// This is exactly what the compatible-provider modal does when an operator
|
||||
// picks a profile from the <Select>: merge the preset onto the existing
|
||||
// customHeaders record before persisting the node/connection.
|
||||
const profileHeaders = getClientIdentityProfileHeaders("codex-cli");
|
||||
const providerSpecificData = {
|
||||
baseUrl: "https://proxy.example.com/v1",
|
||||
customHeaders: { ...profileHeaders, "X-Operator-Set": "keep-me" },
|
||||
};
|
||||
|
||||
assert.equal(providerSpecificData.customHeaders["User-Agent"], "codex_cli_rs/0.136.0");
|
||||
assert.equal(providerSpecificData.customHeaders.originator, "codex_cli_rs");
|
||||
assert.equal(providerSpecificData.customHeaders["X-Operator-Set"], "keep-me");
|
||||
});
|
||||
|
||||
test("profile headers merged into customHeaders survive applyCustomHeaders sanitization via DefaultExecutor", () => {
|
||||
const executor = new DefaultExecutor("openai-compatible-test");
|
||||
const profileHeaders = getClientIdentityProfileHeaders("claude-cli");
|
||||
|
||||
const headers = executor.buildHeaders(
|
||||
{
|
||||
apiKey: "test-key",
|
||||
providerSpecificData: {
|
||||
baseUrl: "https://proxy.example.com/v1",
|
||||
customHeaders: profileHeaders,
|
||||
},
|
||||
},
|
||||
true
|
||||
) as Record<string, string>;
|
||||
|
||||
assert.equal(headers["User-Agent"], "claude-cli/2.1.195 (external, cli)");
|
||||
assert.equal(headers["X-App"], "cli");
|
||||
assert.equal(headers["Authorization"], "Bearer test-key");
|
||||
});
|
||||
|
||||
test("a malicious profile-shaped header set has its auth/cookie entries dropped by applyCustomHeaders", () => {
|
||||
const executor = new DefaultExecutor("openai-compatible-test");
|
||||
|
||||
// Simulate a compromised/hand-crafted profile that tries to smuggle in
|
||||
// credential-owning header names alongside a legitimate identity header.
|
||||
// isForbiddenCustomHeaderName is the single source of truth used by both
|
||||
// the Zod schema and the executor, so assert against it directly too.
|
||||
const maliciousProfileHeaders: Record<string, string> = {
|
||||
"User-Agent": "totally-legit-cli/1.0",
|
||||
Authorization: "Bearer stolen-token",
|
||||
"x-api-key": "stolen-key",
|
||||
cookie: "session=stolen",
|
||||
};
|
||||
assert.equal(isForbiddenCustomHeaderName("Authorization"), true);
|
||||
assert.equal(isForbiddenCustomHeaderName("x-api-key"), true);
|
||||
assert.equal(isForbiddenCustomHeaderName("cookie"), true);
|
||||
|
||||
const headers = executor.buildHeaders(
|
||||
{
|
||||
apiKey: "real-key",
|
||||
providerSpecificData: {
|
||||
baseUrl: "https://proxy.example.com/v1",
|
||||
customHeaders: maliciousProfileHeaders,
|
||||
},
|
||||
},
|
||||
true
|
||||
) as Record<string, string>;
|
||||
|
||||
assert.equal(headers["User-Agent"], "totally-legit-cli/1.0");
|
||||
assert.equal(headers["Authorization"], "Bearer real-key");
|
||||
assert.notEqual(headers["Authorization"], "Bearer stolen-token");
|
||||
assert.equal(headers["x-api-key"], undefined);
|
||||
assert.equal(headers["cookie"], undefined);
|
||||
});
|
||||
|
||||
test("DefaultExecutor.execute sends the selected profile's headers for a compatible-node connection", async () => {
|
||||
const executor = new DefaultExecutor("openai-compatible-test");
|
||||
const originalFetch = globalThis.fetch;
|
||||
let capturedHeaders: Record<string, string> = {};
|
||||
|
||||
globalThis.fetch = async (_url: string | URL | Request, init: RequestInit = {}) => {
|
||||
capturedHeaders = (init.headers as Record<string, string>) || {};
|
||||
return new Response(JSON.stringify({ ok: true }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
await executor.execute({
|
||||
model: "gpt-4.1",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: false,
|
||||
credentials: {
|
||||
apiKey: "real-key",
|
||||
providerSpecificData: {
|
||||
baseUrl: "https://test.proxy.com/v1",
|
||||
customHeaders: getClientIdentityProfileHeaders("gemini-cli"),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(capturedHeaders["User-Agent"], "GeminiCLI/0.1.0 (linux; x64)");
|
||||
assert.equal(capturedHeaders["Authorization"], "Bearer real-key");
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
@@ -1,27 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const { unwrapClineNonStreamingEnvelope } = await import(
|
||||
"../../open-sse/handlers/chatCore/clineResponseEnvelope.ts"
|
||||
);
|
||||
|
||||
test("unwrapClineNonStreamingEnvelope extracts Cline wrapped chat completions", () => {
|
||||
const wrapped = {
|
||||
success: true,
|
||||
data: {
|
||||
id: "chatcmpl_cline",
|
||||
model: "cline/model",
|
||||
choices: [{ index: 0, message: { role: "assistant", content: "ok" } }],
|
||||
usage: { prompt_tokens: 3, completion_tokens: 2, total_tokens: 5 },
|
||||
},
|
||||
};
|
||||
|
||||
assert.deepEqual(unwrapClineNonStreamingEnvelope("cline", wrapped), wrapped.data);
|
||||
});
|
||||
|
||||
test("unwrapClineNonStreamingEnvelope keeps non-Cline and malformed envelopes untouched", () => {
|
||||
const wrapped = { success: true, data: { message: "missing choices" } };
|
||||
|
||||
assert.equal(unwrapClineNonStreamingEnvelope("openai", wrapped), wrapped);
|
||||
assert.equal(unwrapClineNonStreamingEnvelope("cline", wrapped), wrapped);
|
||||
});
|
||||
@@ -1,117 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const { APIKEY_PROVIDERS } = await import("../../src/shared/constants/providers.ts");
|
||||
const { REGISTRY: providerRegistry } = await import("../../open-sse/config/providerRegistry.ts");
|
||||
const { unwrapClinepassEnvelope } = await import("../../open-sse/utils/clinepassEnvelope.ts");
|
||||
const { filterClinepassModels } = await import("../../open-sse/services/clinepassModels.ts");
|
||||
const { parseUpstreamError, buildErrorBody } = await import("../../open-sse/utils/error.ts");
|
||||
|
||||
// ── Provider metadata (Zod-validated APIKEY catalog) ─────────────────────────
|
||||
test("ClinePass is registered as an API-key provider with the canonical identity", () => {
|
||||
const cp = APIKEY_PROVIDERS.clinepass;
|
||||
assert.ok(cp, "APIKEY_PROVIDERS.clinepass must be defined");
|
||||
assert.equal(cp.id, "clinepass");
|
||||
assert.equal(cp.alias, "clinepass");
|
||||
assert.equal(cp.name, "ClinePass");
|
||||
assert.equal(cp.website, "https://cline.bot");
|
||||
assert.equal(
|
||||
(cp as { notice?: { apiKeyUrl?: string } }).notice?.apiKeyUrl,
|
||||
"https://app.cline.bot/settings/api-keys"
|
||||
);
|
||||
});
|
||||
|
||||
test("ClinePass registry entry uses OpenAI format with bearer apikey auth + Cline headers", () => {
|
||||
const entry = providerRegistry.clinepass;
|
||||
assert.ok(entry, "providerRegistry.clinepass must be defined");
|
||||
assert.equal(entry.id, "clinepass");
|
||||
assert.equal(entry.format, "openai");
|
||||
assert.equal(entry.executor, "default");
|
||||
assert.equal(entry.authType, "apikey");
|
||||
assert.equal(entry.authHeader, "bearer");
|
||||
assert.equal(entry.baseUrl, "https://api.cline.bot/api/v1/chat/completions");
|
||||
assert.equal(entry.extraHeaders?.["HTTP-Referer"], "https://cline.bot");
|
||||
assert.equal(entry.extraHeaders?.["X-Title"], "Cline");
|
||||
});
|
||||
|
||||
test("ClinePass models are cline-pass/* and deepseek entries flag reasoning", () => {
|
||||
const models = providerRegistry.clinepass.models;
|
||||
const ids = models.map((m: { id: string }) => m.id);
|
||||
assert.ok(ids.length >= 8, "expect a non-trivial seed list");
|
||||
assert.equal(new Set(ids).size, ids.length, "model ids must be unique");
|
||||
for (const id of ids) {
|
||||
assert.ok(id.startsWith("cline-pass/"), `${id} must be in the cline-pass/ namespace`);
|
||||
}
|
||||
const deepseek = models.filter((m: { id: string }) => m.id.includes("deepseek"));
|
||||
assert.ok(deepseek.length >= 2, "expect the two DeepSeek V4 entries");
|
||||
for (const m of deepseek) {
|
||||
assert.equal((m as { supportsReasoning?: boolean }).supportsReasoning, true);
|
||||
}
|
||||
});
|
||||
|
||||
// ── Envelope unwrap ──────────────────────────────────────────────────────────
|
||||
test("unwrapClinepassEnvelope: success unwraps to data", () => {
|
||||
const inner = { id: "chatcmpl-1", choices: [] };
|
||||
const { body, error } = unwrapClinepassEnvelope({ success: true, data: inner }, "clinepass");
|
||||
assert.equal(error, null);
|
||||
assert.deepEqual(body, inner);
|
||||
});
|
||||
|
||||
test("unwrapClinepassEnvelope: {success:false} yields an error", () => {
|
||||
const { body, error } = unwrapClinepassEnvelope(
|
||||
{ success: false, error: "empty response content", statusCode: 502 },
|
||||
"clinepass"
|
||||
);
|
||||
assert.equal(body, null);
|
||||
assert.ok(error);
|
||||
assert.equal(error?.message, "empty response content");
|
||||
assert.equal(error?.status, 502);
|
||||
});
|
||||
|
||||
test("unwrapClinepassEnvelope: nested error.message extracted", () => {
|
||||
const { error } = unwrapClinepassEnvelope(
|
||||
{ success: false, error: { message: "quota exceeded" } },
|
||||
"clinepass"
|
||||
);
|
||||
assert.equal(error?.message, "quota exceeded");
|
||||
});
|
||||
|
||||
test("unwrapClinepassEnvelope: non-clinepass provider passes through untouched", () => {
|
||||
const payload = { success: false, error: "boom" };
|
||||
const { body, error } = unwrapClinepassEnvelope(payload, "openai");
|
||||
assert.equal(error, null);
|
||||
assert.deepEqual(body, payload);
|
||||
});
|
||||
|
||||
test("unwrapClinepassEnvelope: non-object / array / no-success passthrough", () => {
|
||||
assert.deepEqual(unwrapClinepassEnvelope("plain", "clinepass"), { body: "plain", error: null });
|
||||
assert.deepEqual(unwrapClinepassEnvelope([1, 2], "clinepass"), { body: [1, 2], error: null });
|
||||
const bare = { id: "x" };
|
||||
assert.deepEqual(unwrapClinepassEnvelope(bare, "clinepass"), { body: bare, error: null });
|
||||
});
|
||||
|
||||
// ── Model filter ─────────────────────────────────────────────────────────────
|
||||
test("filterClinepassModels keeps only cline-pass/* ids", () => {
|
||||
const out = filterClinepassModels([
|
||||
{ id: "cline-pass/glm-5.2", name: "GLM" },
|
||||
{ id: "openai/gpt-5.5" },
|
||||
{ id: "cline-pass/deepseek-v4-pro" },
|
||||
{ notId: true },
|
||||
]);
|
||||
assert.deepEqual(out, [
|
||||
{ id: "cline-pass/glm-5.2", name: "GLM" },
|
||||
{ id: "cline-pass/deepseek-v4-pro", name: "cline-pass/deepseek-v4-pro" },
|
||||
]);
|
||||
assert.deepEqual(filterClinepassModels("not-array"), []);
|
||||
});
|
||||
|
||||
// ── Error sanitization (Rule #12 — no stack leak) ────────────────────────────
|
||||
test("parseUpstreamError unwraps clinepass envelope error without leaking a stack", async () => {
|
||||
const upstream = new Response(
|
||||
JSON.stringify({ success: false, error: "upstream at /srv/x.js:1:1 failed" }),
|
||||
{ status: 502, headers: { "content-type": "application/json" } }
|
||||
);
|
||||
const parsed = await parseUpstreamError(upstream, "clinepass");
|
||||
const body = buildErrorBody(502, parsed.message) as { error: { message: string } };
|
||||
assert.ok(!body.error.message.includes("at /"), "sanitized error must not include a stack frame");
|
||||
});
|
||||
@@ -1,77 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { DefaultExecutor } from "../../open-sse/executors/default.ts";
|
||||
|
||||
// DefaultExecutor.ensureThinkingBudget — clinepass-gated max_tokens floor for
|
||||
// reasoning models (prevents empty content when the budget is undersized).
|
||||
|
||||
test("bumps undersized max_tokens to 4096 for a clinepass reasoning model", () => {
|
||||
const executor = new DefaultExecutor("clinepass");
|
||||
const body = {
|
||||
model: "cline-pass/deepseek-v4-pro",
|
||||
reasoning_effort: "high",
|
||||
max_tokens: 512,
|
||||
} as Record<string, unknown>;
|
||||
|
||||
executor.ensureThinkingBudget(body, "cline-pass/deepseek-v4-pro");
|
||||
assert.equal(body.max_tokens, 4096);
|
||||
});
|
||||
|
||||
test("sets max_tokens floor when absent for a reasoning model", () => {
|
||||
const executor = new DefaultExecutor("clinepass");
|
||||
const body = {
|
||||
model: "cline-pass/deepseek-v4-flash",
|
||||
reasoning_effort: "medium",
|
||||
} as Record<string, unknown>;
|
||||
|
||||
executor.ensureThinkingBudget(body, "cline-pass/deepseek-v4-flash");
|
||||
assert.equal(body.max_tokens, 4096);
|
||||
});
|
||||
|
||||
test("leaves an already-sufficient budget untouched", () => {
|
||||
const executor = new DefaultExecutor("clinepass");
|
||||
const body = {
|
||||
model: "cline-pass/deepseek-v4-pro",
|
||||
reasoning_effort: "high",
|
||||
max_tokens: 8000,
|
||||
} as Record<string, unknown>;
|
||||
|
||||
executor.ensureThinkingBudget(body, "cline-pass/deepseek-v4-pro");
|
||||
assert.equal(body.max_tokens, 8000);
|
||||
});
|
||||
|
||||
test("no-op when reasoning is disabled", () => {
|
||||
const executor = new DefaultExecutor("clinepass");
|
||||
const body = {
|
||||
model: "cline-pass/deepseek-v4-pro",
|
||||
max_tokens: 100,
|
||||
} as Record<string, unknown>;
|
||||
|
||||
executor.ensureThinkingBudget(body, "cline-pass/deepseek-v4-pro");
|
||||
assert.equal(body.max_tokens, 100);
|
||||
});
|
||||
|
||||
test("no-op for a non-reasoning clinepass model", () => {
|
||||
const executor = new DefaultExecutor("clinepass");
|
||||
const body = {
|
||||
model: "cline-pass/glm-5.2",
|
||||
reasoning_effort: "high",
|
||||
max_tokens: 100,
|
||||
} as Record<string, unknown>;
|
||||
|
||||
executor.ensureThinkingBudget(body, "cline-pass/glm-5.2");
|
||||
assert.equal(body.max_tokens, 100);
|
||||
});
|
||||
|
||||
test("no-op for a non-clinepass provider (gate)", () => {
|
||||
const executor = new DefaultExecutor("openrouter");
|
||||
const body = {
|
||||
model: "cline-pass/deepseek-v4-pro",
|
||||
reasoning_effort: "high",
|
||||
max_tokens: 100,
|
||||
} as Record<string, unknown>;
|
||||
|
||||
executor.ensureThinkingBudget(body, "cline-pass/deepseek-v4-pro");
|
||||
assert.equal(body.max_tokens, 100);
|
||||
});
|
||||
@@ -1,200 +0,0 @@
|
||||
/**
|
||||
* Regression test for Codex "banked reset credits" (issue #5199).
|
||||
*
|
||||
* DISPLAY ONLY: OmniRoute already calls the ChatGPT backend
|
||||
* `/backend-api/wham/usage` endpoint for quota tracking. Some eligibility-gated
|
||||
* accounts additionally expose `rate_limit_reset_credits.available_count` (a
|
||||
* count of extra rate-limit resets banked on the account) and, optionally,
|
||||
* `rate_limit_reached_type` (which window is currently blocking). This test
|
||||
* verifies:
|
||||
* 1. The field is parsed and surfaced additively when present, across both
|
||||
* independent parsers that read this payload (codexUsageQuotas.ts used by
|
||||
* the dashboard usage fetcher, and codexQuotaFetcher.ts used by the
|
||||
* preflight/monitor fetcher).
|
||||
* 2. Existing quota parsing is completely unaffected when the field is
|
||||
* absent (fail-open — no throw, no regression to session/weekly/etc).
|
||||
*
|
||||
* Redemption of banked reset credits is an unofficial, mutating upstream
|
||||
* endpoint and is explicitly OUT OF SCOPE — this only reads and surfaces data
|
||||
* already present in the existing usage-fetch response.
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { buildCodexUsageQuotas } from "../../open-sse/services/codexUsageQuotas.ts";
|
||||
import { getCodexUsage } from "../../open-sse/services/usage/codex.ts";
|
||||
import {
|
||||
fetchCodexQuota,
|
||||
invalidateCodexQuotaCache,
|
||||
registerCodexConnection,
|
||||
unregisterCodexConnection,
|
||||
} from "../../open-sse/services/codexQuotaFetcher.ts";
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
test.afterEach(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
});
|
||||
|
||||
// ─── codexUsageQuotas.ts (dashboard usage-fetch path) ──────────────────────
|
||||
|
||||
test("buildCodexUsageQuotas surfaces bankedResetCredits when present", () => {
|
||||
const { quotas, bankedResetCredits, rateLimitReachedType } = buildCodexUsageQuotas({
|
||||
rate_limit: {
|
||||
primary_window: { used_percent: 10 },
|
||||
secondary_window: { used_percent: 20 },
|
||||
},
|
||||
rate_limit_reset_credits: { available_count: 3 },
|
||||
rate_limit_reached_type: { type: "secondary_window" },
|
||||
});
|
||||
|
||||
assert.equal(bankedResetCredits, 3);
|
||||
assert.equal(rateLimitReachedType, "secondary_window");
|
||||
// Existing quotas remain intact.
|
||||
assert.equal(quotas.session?.used, 10);
|
||||
assert.equal(quotas.weekly?.used, 20);
|
||||
});
|
||||
|
||||
test("buildCodexUsageQuotas tolerates camelCase field shape", () => {
|
||||
const { bankedResetCredits, rateLimitReachedType } = buildCodexUsageQuotas({
|
||||
rateLimit: { primaryWindow: { usedPercent: 5 } },
|
||||
rateLimitResetCredits: { availableCount: 7 },
|
||||
rateLimitReachedType: "primary_window",
|
||||
});
|
||||
|
||||
assert.equal(bankedResetCredits, 7);
|
||||
assert.equal(rateLimitReachedType, "primary_window");
|
||||
});
|
||||
|
||||
test("buildCodexUsageQuotas leaves bankedResetCredits undefined when absent (fail-open)", () => {
|
||||
const result = buildCodexUsageQuotas({
|
||||
rate_limit: {
|
||||
primary_window: { used_percent: 10 },
|
||||
secondary_window: { used_percent: 20 },
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result.bankedResetCredits, undefined);
|
||||
assert.equal(result.rateLimitReachedType, undefined);
|
||||
// Existing quota parsing is unaffected — no throw, no missing windows.
|
||||
assert.equal(result.quotas.session?.used, 10);
|
||||
assert.equal(result.quotas.weekly?.used, 20);
|
||||
});
|
||||
|
||||
test("buildCodexUsageQuotas never throws on a garbage rate_limit_reset_credits shape", () => {
|
||||
assert.doesNotThrow(() => {
|
||||
const result = buildCodexUsageQuotas({
|
||||
rate_limit: { primary_window: { used_percent: 1 } },
|
||||
rate_limit_reset_credits: "not-an-object",
|
||||
rate_limit_reached_type: 12345,
|
||||
});
|
||||
assert.equal(result.bankedResetCredits, undefined);
|
||||
assert.equal(result.rateLimitReachedType, undefined);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── usage/codex.ts (getCodexUsage — full dashboard fetch) ─────────────────
|
||||
|
||||
test("getCodexUsage threads bankedResetCredits through additively", async () => {
|
||||
globalThis.fetch = async () =>
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
plan_type: "plus",
|
||||
rate_limit: {
|
||||
primary_window: { used_percent: 15 },
|
||||
secondary_window: { used_percent: 25 },
|
||||
},
|
||||
rate_limit_reset_credits: { available_count: 2 },
|
||||
}),
|
||||
{ status: 200, headers: { "content-type": "application/json" } }
|
||||
);
|
||||
|
||||
const usage = await getCodexUsage("token", { workspaceId: "ws-1" });
|
||||
|
||||
assert.equal((usage as any).plan, "plus");
|
||||
assert.equal((usage as any).bankedResetCredits, 2);
|
||||
assert.equal((usage as any).quotas.session.used, 15);
|
||||
assert.equal((usage as any).quotas.weekly.used, 25);
|
||||
});
|
||||
|
||||
test("getCodexUsage omits bankedResetCredits and stays intact when absent", async () => {
|
||||
globalThis.fetch = async () =>
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
plan_type: "plus",
|
||||
rate_limit: {
|
||||
primary_window: { used_percent: 15 },
|
||||
secondary_window: { used_percent: 25 },
|
||||
},
|
||||
}),
|
||||
{ status: 200, headers: { "content-type": "application/json" } }
|
||||
);
|
||||
|
||||
const usage = await getCodexUsage("token", { workspaceId: "ws-1" });
|
||||
|
||||
assert.equal("bankedResetCredits" in (usage as any), false);
|
||||
assert.equal((usage as any).quotas.session.used, 15);
|
||||
assert.equal((usage as any).quotas.weekly.used, 25);
|
||||
});
|
||||
|
||||
// ─── codexQuotaFetcher.ts (preflight/monitor path) ─────────────────────────
|
||||
|
||||
test("fetchCodexQuota surfaces bankedResetCredits from the dual-window parser", async () => {
|
||||
const connectionId = `codex-banked-${Date.now()}`;
|
||||
|
||||
globalThis.fetch = async () =>
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
rate_limit: {
|
||||
primary_window: { used_percent: 70, reset_after_seconds: 45 },
|
||||
secondary_window: { used_percent: 20, reset_after_seconds: 300 },
|
||||
},
|
||||
rate_limit_reset_credits: { available_count: 4 },
|
||||
rate_limit_reached_type: { type: "primary_window" },
|
||||
}),
|
||||
{ status: 200, headers: { "content-type": "application/json" } }
|
||||
);
|
||||
|
||||
const quota = await fetchCodexQuota(connectionId, {
|
||||
accessToken: "token",
|
||||
providerSpecificData: { workspaceId: "ws" },
|
||||
});
|
||||
|
||||
assert.ok(quota);
|
||||
assert.equal(quota?.bankedResetCredits, 4);
|
||||
assert.equal(quota?.rateLimitReachedType, "primary_window");
|
||||
// Existing dual-window parsing stays intact.
|
||||
assert.equal(quota?.window5h.percentUsed, 0.7);
|
||||
assert.equal(quota?.window7d.percentUsed, 0.2);
|
||||
|
||||
invalidateCodexQuotaCache(connectionId);
|
||||
unregisterCodexConnection(connectionId);
|
||||
});
|
||||
|
||||
test("fetchCodexQuota omits bankedResetCredits when the payload does not have it (fail-open)", async () => {
|
||||
const connectionId = `codex-nobanked-${Date.now()}`;
|
||||
|
||||
registerCodexConnection(connectionId, { accessToken: "token" });
|
||||
|
||||
globalThis.fetch = async () =>
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
rate_limit: {
|
||||
primary_window: { used_percent: 30 },
|
||||
secondary_window: { used_percent: 10 },
|
||||
},
|
||||
}),
|
||||
{ status: 200, headers: { "content-type": "application/json" } }
|
||||
);
|
||||
|
||||
const quota = await fetchCodexQuota(connectionId);
|
||||
|
||||
assert.ok(quota);
|
||||
assert.equal(quota?.bankedResetCredits, undefined);
|
||||
assert.equal(quota?.rateLimitReachedType, undefined);
|
||||
assert.equal(quota?.window5h.percentUsed, 0.3);
|
||||
assert.equal(quota?.window7d.percentUsed, 0.1);
|
||||
|
||||
invalidateCodexQuotaCache(connectionId);
|
||||
unregisterCodexConnection(connectionId);
|
||||
});
|
||||
@@ -1,36 +0,0 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
// Split-guard for the codex executor quota extraction.
|
||||
// The pure quota-snapshot parsing + reset/cooldown scheduling lives in codex/quota.ts.
|
||||
// Host re-exports the 4 public symbols (chatCore/codexQuota.ts + tests import them).
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const EXE = join(HERE, "../../open-sse/executors");
|
||||
const HOST = join(EXE, "codex.ts");
|
||||
const LEAF = join(EXE, "codex/quota.ts");
|
||||
|
||||
test("leaf hosts the quota helpers and does not import the host", () => {
|
||||
const src = readFileSync(LEAF, "utf8");
|
||||
for (const sym of [
|
||||
"parseCodexQuotaHeaders",
|
||||
"getCodexResetTime",
|
||||
"getCodexDualWindowCooldownMs",
|
||||
"CodexQuotaSnapshot",
|
||||
]) {
|
||||
assert.match(src, new RegExp(`export (function|interface) ${sym}\\b`));
|
||||
}
|
||||
assert.doesNotMatch(src, /from "\.\.\/codex\.ts"/);
|
||||
});
|
||||
|
||||
test("host re-exports the quota symbols for external importers", () => {
|
||||
const host = readFileSync(HOST, "utf8");
|
||||
assert.match(host, /from "\.\/codex\/quota\.ts"/);
|
||||
});
|
||||
|
||||
test("parseCodexQuotaHeaders returns null without quota headers", async () => {
|
||||
const { parseCodexQuotaHeaders } = await import("../../open-sse/executors/codex/quota.ts");
|
||||
assert.equal(parseCodexQuotaHeaders({}), null);
|
||||
});
|
||||
@@ -1,143 +0,0 @@
|
||||
// Route-wiring tests for /api/oauth/codex/import-token (#1290).
|
||||
//
|
||||
// Imports a Codex connection from a bare ChatGPT access token — no refresh
|
||||
// token required. Auth is disabled via settings (requireLogin:false) so we
|
||||
// reach the schema/decode logic rather than a 401. DB handles are released
|
||||
// in test.after (CLAUDE.md learning: unreleased SQLite handles hang node:test).
|
||||
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-codex-import-token-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const settingsDb = await import("../../src/lib/db/settings.ts");
|
||||
const providersDb = await import("../../src/lib/db/providers.ts");
|
||||
const route = await import("../../src/app/api/oauth/codex/import-token/route.ts");
|
||||
|
||||
function b64url(obj: unknown): string {
|
||||
return Buffer.from(JSON.stringify(obj))
|
||||
.toString("base64")
|
||||
.replace(/=+$/, "")
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_");
|
||||
}
|
||||
|
||||
function makeJwt(payload: Record<string, unknown>): string {
|
||||
const header = b64url({ alg: "RS256", typ: "JWT" });
|
||||
const body = b64url(payload);
|
||||
return `${header}.${body}.signature`;
|
||||
}
|
||||
|
||||
test.before(async () => {
|
||||
await settingsDb.updateSettings({ requireLogin: false });
|
||||
});
|
||||
|
||||
test.after(async () => {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
async function postImportToken(body: unknown) {
|
||||
const request = new Request("http://localhost:20128/api/oauth/codex/import-token", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const response = await route.POST(request);
|
||||
return { status: response.status, body: await response.json() };
|
||||
}
|
||||
|
||||
test("import-token: decodes email + workspace claims from the access token and creates a connection", async () => {
|
||||
const accessToken = makeJwt({
|
||||
email: "bare-token@example.com",
|
||||
"https://api.openai.com/auth": {
|
||||
chatgpt_account_id: "acct-bare",
|
||||
chatgpt_plan_type: "plus",
|
||||
},
|
||||
});
|
||||
|
||||
const { status, body } = await postImportToken({ accessToken });
|
||||
assert.equal(status, 200);
|
||||
assert.equal(body.success, true);
|
||||
assert.equal(body.connection.provider, "codex");
|
||||
assert.equal(body.connection.email, "bare-token@example.com");
|
||||
|
||||
const rows = await providersDb.getProviderConnections({ provider: "codex" });
|
||||
const created = rows.find((r) => r.id === body.connection.id);
|
||||
assert.equal(created?.authType, "access_token");
|
||||
assert.equal(created?.accessToken, accessToken);
|
||||
assert.ok(!created?.refreshToken, "no refresh token should be persisted");
|
||||
assert.deepEqual(created?.providerSpecificData, {
|
||||
chatgptAccountId: "acct-bare",
|
||||
chatgptPlanType: "plus",
|
||||
});
|
||||
});
|
||||
|
||||
test("import-token: falls back to the explicit `name` when the JWT carries no email", async () => {
|
||||
const accessToken = makeJwt({
|
||||
"https://api.openai.com/auth": { chatgpt_account_id: "acct-noemail" },
|
||||
});
|
||||
|
||||
const { status, body } = await postImportToken({ accessToken, name: "My Bare Token" });
|
||||
assert.equal(status, 200);
|
||||
assert.equal(body.connection.name, "My Bare Token");
|
||||
});
|
||||
|
||||
test("import-token: missing accessToken fails schema validation with 400", async () => {
|
||||
const { status, body } = await postImportToken({});
|
||||
assert.equal(status, 400);
|
||||
assert.ok(typeof body.error.message === "string" && body.error.message.length > 0);
|
||||
});
|
||||
|
||||
test("import-token: empty-string accessToken fails schema validation with 400", async () => {
|
||||
const { status } = await postImportToken({ accessToken: " " });
|
||||
assert.equal(status, 400);
|
||||
});
|
||||
|
||||
test("import-token: undecodable token with no name and no claims is rejected with 400", async () => {
|
||||
const { status, body } = await postImportToken({ accessToken: "not-a-jwt" });
|
||||
assert.equal(status, 400);
|
||||
assert.match(body.error.message, /decode|account info/i);
|
||||
});
|
||||
|
||||
test("import-token: undecodable token IS accepted when an explicit name is supplied", async () => {
|
||||
const { status, body } = await postImportToken({
|
||||
accessToken: "not-a-jwt-but-thats-ok",
|
||||
name: "Manually Labeled",
|
||||
});
|
||||
assert.equal(status, 200);
|
||||
assert.equal(body.connection.name, "Manually Labeled");
|
||||
});
|
||||
|
||||
test("import-token: malformed JSON body is rejected with 400", async () => {
|
||||
const request = new Request("http://localhost:20128/api/oauth/codex/import-token", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: "{not json",
|
||||
});
|
||||
const response = await route.POST(request);
|
||||
assert.equal(response.status, 400);
|
||||
});
|
||||
|
||||
test("import-token: repeated imports for the same email never dedup (each is a new connection)", async () => {
|
||||
const tokenA = makeJwt({ email: "repeat@example.com" });
|
||||
const tokenB = makeJwt({ email: "repeat@example.com" });
|
||||
|
||||
const first = await postImportToken({ accessToken: tokenA });
|
||||
const second = await postImportToken({ accessToken: tokenB });
|
||||
|
||||
assert.equal(first.status, 200);
|
||||
assert.equal(second.status, 200);
|
||||
assert.notEqual(first.body.connection.id, second.body.connection.id);
|
||||
});
|
||||
|
||||
test("import-token: error responses never leak a stack trace", async () => {
|
||||
const { body } = await postImportToken({});
|
||||
assert.ok(!JSON.stringify(body).includes("at /"), "must not leak a stack trace");
|
||||
assert.ok(!JSON.stringify(body).includes(".ts:"), "must not leak a source location");
|
||||
});
|
||||
@@ -1,181 +0,0 @@
|
||||
// #5903: Codex session affinity must win over a per-request reset-aware
|
||||
// re-scoring. The reset-aware combo strategy (open-sse/services/combo/quotaStrategies.ts)
|
||||
// recomputes its "winner" connection on every request and hands it to
|
||||
// getProviderCredentials as forcedConnectionId (src/sse/handlers/chat.ts).
|
||||
// Before the fix, forcedConnectionId narrowed the connection pool BEFORE
|
||||
// session affinity was consulted, so a fresh quota-scoring winner silently
|
||||
// evicted the existing pin (deleteSessionAccountAffinity) on every request —
|
||||
// breaking "same session -> reuse pinned account".
|
||||
//
|
||||
// This test drives auth.getProviderCredentials directly (the same call shape
|
||||
// chat.ts uses: sessionKey + forcedConnectionId together) to reproduce the
|
||||
// bug without needing the full combo/quota-scoring machinery.
|
||||
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-codex-affinity-5903-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "codex-affinity-5903-test-secret";
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const providersDb = await import("../../src/lib/db/providers.ts");
|
||||
const settingsDb = await import("../../src/lib/db/settings.ts");
|
||||
const apiKeysDb = await import("../../src/lib/db/apiKeys.ts");
|
||||
const affinityDb = await import("../../src/lib/db/sessionAccountAffinity.ts");
|
||||
const auth = await import("../../src/sse/services/auth.ts");
|
||||
|
||||
async function resetStorage() {
|
||||
core.resetDbInstance();
|
||||
apiKeysDb.resetApiKeyState();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
async function seedConnection(provider: string, overrides: any = {}) {
|
||||
return providersDb.createProviderConnection({
|
||||
provider,
|
||||
authType: overrides.authType || "oauth",
|
||||
name: overrides.name || `${provider}-${Math.random().toString(16).slice(2, 8)}`,
|
||||
accessToken: overrides.accessToken || `at-${Math.random().toString(16).slice(2, 10)}`,
|
||||
refreshToken: overrides.refreshToken,
|
||||
isActive: overrides.isActive ?? true,
|
||||
testStatus: overrides.testStatus || "active",
|
||||
priority: overrides.priority,
|
||||
providerSpecificData: overrides.providerSpecificData || {},
|
||||
});
|
||||
}
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await resetStorage();
|
||||
});
|
||||
|
||||
test.after(async () => {
|
||||
core.resetDbInstance();
|
||||
apiKeysDb.resetApiKeyState();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("codex session affinity wins over a per-request reset-aware forcedConnectionId (#5903)", async () => {
|
||||
await settingsDb.updateSettings({
|
||||
fallbackStrategy: "reset-aware",
|
||||
codexSessionAffinityTtlMs: 60_000,
|
||||
});
|
||||
|
||||
const connectionA = await seedConnection("codex", { name: "codex-reset-aware-a" });
|
||||
const connectionB = await seedConnection("codex", { name: "codex-reset-aware-b" });
|
||||
|
||||
// Request 1: reset-aware quota scoring picks A as the winner for session S.
|
||||
const request1 = await auth.getProviderCredentials("codex", null, null, "gpt-5.5", {
|
||||
sessionKey: "session-S",
|
||||
forcedConnectionId: connectionA.id,
|
||||
});
|
||||
assert.equal(request1?.connectionId, connectionA.id, "request 1 should pin to the scored winner A");
|
||||
assert.equal(
|
||||
affinityDb.getSessionAccountAffinity("session-S", "codex", 60_000)?.connectionId,
|
||||
connectionA.id,
|
||||
"affinity row must be created for session-S pointing at A"
|
||||
);
|
||||
|
||||
// Request 2: quota state shifted and reset-aware now scores B higher for
|
||||
// the SAME session. Without the fix, forcedConnectionId=B narrows the pool
|
||||
// to just B before affinity is checked, evicting the A pin and re-pinning
|
||||
// to B. With the fix, the existing active pin (A) must win.
|
||||
const request2 = await auth.getProviderCredentials("codex", null, null, "gpt-5.5", {
|
||||
sessionKey: "session-S",
|
||||
forcedConnectionId: connectionB.id,
|
||||
});
|
||||
assert.equal(
|
||||
request2?.connectionId,
|
||||
connectionA.id,
|
||||
"request 2 must still use the pinned connection A, not the freshly re-scored B"
|
||||
);
|
||||
assert.equal(
|
||||
affinityDb.getSessionAccountAffinity("session-S", "codex", 60_000)?.connectionId,
|
||||
connectionA.id,
|
||||
"affinity row for session-S must remain pinned to A after re-scoring"
|
||||
);
|
||||
|
||||
// A brand-new session (S2) has no existing pin, so the freshly re-scored
|
||||
// winner (B) must be honored and a NEW pin created for S2.
|
||||
const request3 = await auth.getProviderCredentials("codex", null, null, "gpt-5.5", {
|
||||
sessionKey: "session-S2",
|
||||
forcedConnectionId: connectionB.id,
|
||||
});
|
||||
assert.equal(request3?.connectionId, connectionB.id, "a new session must honor the fresh re-scored pick");
|
||||
assert.equal(
|
||||
affinityDb.getSessionAccountAffinity("session-S2", "codex", 60_000)?.connectionId,
|
||||
connectionB.id,
|
||||
"a new affinity row for session-S2 must be created pointing at B"
|
||||
);
|
||||
|
||||
// Session S must remain unaffected by S2's independent pin.
|
||||
assert.equal(
|
||||
affinityDb.getSessionAccountAffinity("session-S", "codex", 60_000)?.connectionId,
|
||||
connectionA.id,
|
||||
"session-S pin must stay isolated from session-S2"
|
||||
);
|
||||
});
|
||||
|
||||
test("reset-aware forcedConnectionId is honored when the pinned connection becomes ineligible (#5903)", async () => {
|
||||
await settingsDb.updateSettings({
|
||||
fallbackStrategy: "reset-aware",
|
||||
codexSessionAffinityTtlMs: 60_000,
|
||||
});
|
||||
|
||||
const connectionA = await seedConnection("codex", { name: "codex-reset-aware-ineligible-a" });
|
||||
const connectionB = await seedConnection("codex", { name: "codex-reset-aware-ineligible-b" });
|
||||
|
||||
const request1 = await auth.getProviderCredentials("codex", null, null, "gpt-5.5", {
|
||||
sessionKey: "session-failover",
|
||||
forcedConnectionId: connectionA.id,
|
||||
});
|
||||
assert.equal(request1?.connectionId, connectionA.id);
|
||||
|
||||
// A becomes rate-limited (e.g. 429 handled by markAccountUnavailable in
|
||||
// production). Reset-aware re-scores and now forces B. The pin (A) is no
|
||||
// longer eligible, so the freshly forced B must be used instead of
|
||||
// failing the whole request.
|
||||
await providersDb.updateProviderConnection(connectionA.id, {
|
||||
rateLimitedUntil: new Date(Date.now() + 60_000).toISOString(),
|
||||
});
|
||||
|
||||
const request2 = await auth.getProviderCredentials("codex", null, null, "gpt-5.5", {
|
||||
sessionKey: "session-failover",
|
||||
forcedConnectionId: connectionB.id,
|
||||
});
|
||||
assert.equal(
|
||||
request2?.connectionId,
|
||||
connectionB.id,
|
||||
"an ineligible pin must fall through to the freshly forced connection"
|
||||
);
|
||||
});
|
||||
|
||||
test("no session affinity configured: reset-aware forcedConnectionId applies exactly as before (#5903)", async () => {
|
||||
await settingsDb.updateSettings({
|
||||
fallbackStrategy: "reset-aware",
|
||||
codexSessionAffinityTtlMs: 0,
|
||||
});
|
||||
|
||||
const connectionA = await seedConnection("codex", { name: "codex-no-affinity-a" });
|
||||
const connectionB = await seedConnection("codex", { name: "codex-no-affinity-b" });
|
||||
|
||||
const request1 = await auth.getProviderCredentials("codex", null, null, "gpt-5.5", {
|
||||
sessionKey: "session-no-ttl",
|
||||
forcedConnectionId: connectionA.id,
|
||||
});
|
||||
assert.equal(request1?.connectionId, connectionA.id);
|
||||
|
||||
const request2 = await auth.getProviderCredentials("codex", null, null, "gpt-5.5", {
|
||||
sessionKey: "session-no-ttl",
|
||||
forcedConnectionId: connectionB.id,
|
||||
});
|
||||
assert.equal(
|
||||
request2?.connectionId,
|
||||
connectionB.id,
|
||||
"with affinity disabled (ttl=0) each request must honor the fresh forcedConnectionId"
|
||||
);
|
||||
});
|
||||
@@ -1,29 +0,0 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
// Split-guard for the codex executor tool-normalization extraction.
|
||||
// The hosted-tool passthrough + free-plan gating live in codex/tools.ts (self-contained,
|
||||
// console.debug only). codex.ts re-exports them so external importers keep the path.
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const EXE = join(HERE, "../../open-sse/executors");
|
||||
|
||||
test("leaf hosts the tool normalizers and does not import the host", () => {
|
||||
const src = readFileSync(join(EXE, "codex/tools.ts"), "utf8");
|
||||
assert.match(src, /export function normalizeCodexTools\b/);
|
||||
assert.match(src, /export function isCodexFreePlan\b/);
|
||||
assert.doesNotMatch(src, /from "\.\.\/codex\.ts"/);
|
||||
});
|
||||
|
||||
test("codex.ts re-exports them for external importers", () => {
|
||||
const host = readFileSync(join(EXE, "codex.ts"), "utf8");
|
||||
assert.match(host, /export \{[^}]*normalizeCodexTools[^}]*\} from "\.\/codex\/tools\.ts"/s);
|
||||
});
|
||||
|
||||
test("both import paths resolve to the same function", async () => {
|
||||
const viaHost = (await import("../../open-sse/executors/codex.ts")).normalizeCodexTools;
|
||||
const viaLeaf = (await import("../../open-sse/executors/codex/tools.ts")).normalizeCodexTools;
|
||||
assert.equal(viaHost, viaLeaf);
|
||||
});
|
||||
@@ -37,19 +37,10 @@ test("invalid verbosity is dropped and text removed", () => {
|
||||
assert.equal(body.verbosity, undefined);
|
||||
});
|
||||
|
||||
test("no verbosity + Responses text.format is preserved", () => {
|
||||
const format = { type: "json_schema", name: "schema", schema: { type: "object" } };
|
||||
const body: Record<string, unknown> = { text: { format }, input: [] };
|
||||
test("no verbosity + stray non-verbosity text → text removed (status quo)", () => {
|
||||
const body: Record<string, unknown> = { text: { format: { type: "json" } }, input: [] };
|
||||
normalizeCodexVerbosity(body);
|
||||
assert.deepEqual(body.text, { format });
|
||||
});
|
||||
|
||||
test("verbosity is merged with existing Responses text.format", () => {
|
||||
const format = { type: "json_schema", name: "schema", schema: { type: "object" } };
|
||||
const body: Record<string, unknown> = { verbosity: "low", text: { format }, input: [] };
|
||||
normalizeCodexVerbosity(body);
|
||||
assert.deepEqual(body.text, { format, verbosity: "low" });
|
||||
assert.equal(body.verbosity, undefined);
|
||||
assert.equal(body.text, undefined);
|
||||
});
|
||||
|
||||
test("verbosity is normalized case-insensitively", () => {
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
import { test, after } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { applyStrategyOrdering } from "@omniroute/open-sse/services/combo/applyStrategyOrdering.ts";
|
||||
import { resetDbInstance } from "@/lib/db/core.ts";
|
||||
|
||||
// Split guard for Block J Task 3: the non-`auto` strategy-ordering chain
|
||||
// (lkgp / strict-random / random / fill-first / p2c / ... / quota-share) was
|
||||
// extracted verbatim into applyStrategyOrdering. These tests pin the exits that
|
||||
// need no DB/deck state (random / fill-first / unknown); the DB-backed branches
|
||||
// (lkgp, reset-*, quota-share) are covered end-to-end by the 47 consumer tests
|
||||
// (router-strategies / combo-strategy-fallbacks / rr-session-stickiness).
|
||||
|
||||
after(() => {
|
||||
// some branches (lkgp/quota-share) may touch the DB singleton; release handles.
|
||||
resetDbInstance();
|
||||
});
|
||||
|
||||
const noopLog = { info() {}, warn() {}, error() {}, debug() {} } as never;
|
||||
|
||||
const target = (provider: string, modelStr: string): never =>
|
||||
({
|
||||
kind: "model",
|
||||
stepId: "s1",
|
||||
executionKey: `${provider}>${modelStr}`,
|
||||
modelStr,
|
||||
provider,
|
||||
providerId: null,
|
||||
connectionId: null,
|
||||
weight: 1,
|
||||
label: null,
|
||||
}) as never;
|
||||
|
||||
const deps = () =>
|
||||
({
|
||||
combo: { id: "c1", name: "c1", config: {} },
|
||||
config: {},
|
||||
body: { messages: [] },
|
||||
log: noopLog,
|
||||
apiKeyAllowedConnections: null,
|
||||
}) as never;
|
||||
|
||||
const keys = (arr: Array<{ executionKey: string }>) => arr.map((t) => t.executionKey).sort();
|
||||
|
||||
test("exports applyStrategyOrdering", () => {
|
||||
assert.equal(typeof applyStrategyOrdering, "function");
|
||||
});
|
||||
|
||||
test("unknown strategy -> input order unchanged (same reference contents)", async () => {
|
||||
const input = [target("openai", "gpt-4o"), target("anthropic", "claude-3")];
|
||||
const out = await applyStrategyOrdering("no-such-strategy", input, deps());
|
||||
assert.deepEqual(
|
||||
out.map((t: { executionKey: string }) => t.executionKey),
|
||||
["openai>gpt-4o", "anthropic>claude-3"]
|
||||
);
|
||||
});
|
||||
|
||||
test("fill-first -> preserves priority order", async () => {
|
||||
const input = [target("a", "m1"), target("b", "m2"), target("c", "m3")];
|
||||
const out = await applyStrategyOrdering("fill-first", input, deps());
|
||||
assert.deepEqual(
|
||||
out.map((t: { executionKey: string }) => t.executionKey),
|
||||
["a>m1", "b>m2", "c>m3"]
|
||||
);
|
||||
});
|
||||
|
||||
test("random -> same multiset of targets (a permutation)", async () => {
|
||||
const input = [target("a", "m1"), target("b", "m2"), target("c", "m3")];
|
||||
const out = await applyStrategyOrdering("random", input, deps());
|
||||
assert.equal(out.length, 3);
|
||||
assert.deepEqual(keys(out), keys(input));
|
||||
});
|
||||
@@ -1,79 +0,0 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { parseAutoConfig } from "@omniroute/open-sse/services/combo/autoConfig.ts";
|
||||
import { DEFAULT_WEIGHTS } from "@omniroute/open-sse/services/autoCombo/scoring.ts";
|
||||
|
||||
// Split guard for Block J Task 2: parseAutoConfig was extracted verbatim from
|
||||
// handleComboChat's inline auto-strategy config block. These assertions pin the
|
||||
// pure derivation so the extraction stays behavior-identical.
|
||||
|
||||
const target = (provider: string, modelStr: string) =>
|
||||
({ provider, modelStr, executionKey: `${provider}>${modelStr}` }) as never;
|
||||
|
||||
test("defaults: rules strategy, provider-derived pool, default weights", () => {
|
||||
const cfg = parseAutoConfig({ name: "c", config: {} } as never, [
|
||||
target("openai", "gpt-4o"),
|
||||
target("anthropic", "claude-3"),
|
||||
target("openai", "gpt-4o-mini"),
|
||||
]);
|
||||
assert.equal(cfg.routingStrategy, "rules");
|
||||
assert.deepEqual(cfg.candidatePool, ["openai", "anthropic"]);
|
||||
assert.equal(cfg.weights, DEFAULT_WEIGHTS);
|
||||
assert.equal(cfg.explorationRate, 0.05);
|
||||
assert.equal(cfg.budgetCap, undefined);
|
||||
assert.equal(cfg.modePack, undefined);
|
||||
});
|
||||
|
||||
test("routerStrategy takes precedence over routingStrategy/strategyName", () => {
|
||||
const cfg = parseAutoConfig(
|
||||
{
|
||||
name: "c",
|
||||
autoConfig: {
|
||||
routerStrategy: "lkgp",
|
||||
routingStrategy: "cost",
|
||||
strategyName: "p2c",
|
||||
},
|
||||
} as never,
|
||||
[]
|
||||
);
|
||||
assert.equal(cfg.routingStrategy, "lkgp");
|
||||
});
|
||||
|
||||
test("explicit candidatePool, weights, exploration and budget are honored", () => {
|
||||
const customWeights = { latency: 1 } as never;
|
||||
const cfg = parseAutoConfig(
|
||||
{
|
||||
name: "c",
|
||||
autoConfig: {
|
||||
candidatePool: ["glm", "openai"],
|
||||
weights: customWeights,
|
||||
explorationRate: 0.3,
|
||||
budgetCap: 5,
|
||||
modePack: "coding",
|
||||
},
|
||||
} as never,
|
||||
[target("ignored", "x")]
|
||||
);
|
||||
assert.deepEqual(cfg.candidatePool, ["glm", "openai"]);
|
||||
assert.equal(cfg.weights, customWeights);
|
||||
assert.equal(cfg.explorationRate, 0.3);
|
||||
assert.equal(cfg.budgetCap, 5);
|
||||
assert.equal(cfg.modePack, "coding");
|
||||
});
|
||||
|
||||
test("config.auto is preferred over top-level config", () => {
|
||||
const cfg = parseAutoConfig(
|
||||
{ name: "c", config: { auto: { routerStrategy: "cost" }, routerStrategy: "rules" } } as never,
|
||||
[]
|
||||
);
|
||||
assert.equal(cfg.routingStrategy, "cost");
|
||||
});
|
||||
|
||||
test("non-finite explorationRate falls back to 0.05", () => {
|
||||
const cfg = parseAutoConfig(
|
||||
{ name: "c", autoConfig: { explorationRate: "not-a-number" } } as never,
|
||||
[]
|
||||
);
|
||||
assert.equal(cfg.explorationRate, 0.05);
|
||||
});
|
||||
@@ -1,183 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
/**
|
||||
* #5923 (Finding #4) — the quota-exhaustion preflight cutoff only ran for
|
||||
* strategy === "auto" (buildAutoCandidates / routableCandidates in combo.ts).
|
||||
* Priority/weighted/etc. strategies funneled through the shared executeTarget
|
||||
* per-target loop, which only checked the provider circuit breaker + model
|
||||
* lockout — never a per-(provider, connection) quota-exhaustion cutoff. A 0%-
|
||||
* remaining connection stayed eligible as the lead leg until it reactively
|
||||
* 429'd.
|
||||
*
|
||||
* Regression guard: with the quota-exhaustion opt-in enabled
|
||||
* (`resilienceSettings.quotaPreflight.enabled = true`), a "priority" combo
|
||||
* whose first-listed connection is at 0% remaining must skip straight to the
|
||||
* sibling connection of the SAME provider — never dispatching to the
|
||||
* exhausted connection. This must stay strictly per-connection: it must NOT
|
||||
* touch the provider circuit breaker (both connections belong to the same
|
||||
* provider, and the healthy one must remain fully eligible).
|
||||
*/
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), "omniroute-quota-cutoff-priority-5923-")
|
||||
);
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
|
||||
const dbCore = await import("../../src/lib/db/core.ts");
|
||||
const { handleComboChat } = await import("../../open-sse/services/combo.ts");
|
||||
const { registerQuotaFetcher } = await import("../../open-sse/services/quotaPreflight.ts");
|
||||
const { getCircuitBreaker } = await import("../../src/shared/utils/circuitBreaker.ts");
|
||||
|
||||
test.after(() => {
|
||||
dbCore.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function makeLog() {
|
||||
return {
|
||||
info() {},
|
||||
warn() {},
|
||||
debug() {},
|
||||
error() {},
|
||||
};
|
||||
}
|
||||
|
||||
function okResponse(model: string) {
|
||||
return Response.json({ choices: [{ message: { role: "assistant", content: model } }] });
|
||||
}
|
||||
|
||||
const PROVIDER = "openai";
|
||||
const EXHAUSTED_CONNECTION_ID = "conn-exhausted-5923";
|
||||
const HEALTHY_CONNECTION_ID = "conn-healthy-5923";
|
||||
|
||||
test("#5923 priority combo skips a 0%-remaining lead connection but keeps the sibling connection eligible", async () => {
|
||||
registerQuotaFetcher(PROVIDER, async (connectionId: string) => {
|
||||
if (connectionId === EXHAUSTED_CONNECTION_ID) {
|
||||
return { used: 100, total: 100, percentUsed: 1 };
|
||||
}
|
||||
return { used: 5, total: 100, percentUsed: 0.05 };
|
||||
});
|
||||
|
||||
const combo = {
|
||||
name: `priority-quota-cutoff-5923-${Date.now()}`,
|
||||
strategy: "priority",
|
||||
models: [
|
||||
{
|
||||
kind: "model",
|
||||
provider: PROVIDER,
|
||||
providerId: PROVIDER,
|
||||
model: "gpt-4o-mini",
|
||||
connectionId: EXHAUSTED_CONNECTION_ID,
|
||||
id: "step-a",
|
||||
},
|
||||
{
|
||||
kind: "model",
|
||||
provider: PROVIDER,
|
||||
providerId: PROVIDER,
|
||||
model: "gpt-4o-mini",
|
||||
connectionId: HEALTHY_CONNECTION_ID,
|
||||
id: "step-b",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const calls: Array<string | null> = [];
|
||||
const response = await handleComboChat({
|
||||
body: { model: combo.name, messages: [{ role: "user", content: "hi" }] },
|
||||
combo,
|
||||
allCombos: [combo],
|
||||
isModelAvailable: undefined,
|
||||
relayOptions: undefined,
|
||||
signal: undefined,
|
||||
settings: {
|
||||
resilienceSettings: {
|
||||
quotaPreflight: {
|
||||
enabled: true,
|
||||
defaultThresholdPercent: 2,
|
||||
warnThresholdPercent: 20,
|
||||
},
|
||||
},
|
||||
},
|
||||
log: makeLog(),
|
||||
handleSingleModel: async (
|
||||
_body: unknown,
|
||||
modelStr: string,
|
||||
target?: { connectionId?: string | null }
|
||||
) => {
|
||||
calls.push(target?.connectionId ?? null);
|
||||
return okResponse(modelStr);
|
||||
},
|
||||
} as Parameters<typeof handleComboChat>[0]);
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.ok(calls.length > 0, "expected at least one dispatched target");
|
||||
assert.equal(
|
||||
calls[0],
|
||||
HEALTHY_CONNECTION_ID,
|
||||
"the 0%-remaining lead connection must be skipped; the sibling connection must be dispatched instead"
|
||||
);
|
||||
assert.ok(
|
||||
!calls.includes(EXHAUSTED_CONNECTION_ID),
|
||||
"the exhausted connection must never be dispatched to"
|
||||
);
|
||||
|
||||
// Strictly per-connection — the provider circuit breaker must stay CLOSED.
|
||||
// Only one connection was skipped; the provider itself never failed.
|
||||
assert.equal(
|
||||
getCircuitBreaker(PROVIDER).getStatus().state,
|
||||
"CLOSED",
|
||||
"quota-exhaustion cutoff must never trip the whole-provider circuit breaker"
|
||||
);
|
||||
});
|
||||
|
||||
test("#5923 priority combo does NOT skip a 0%-remaining connection when the cutoff setting is disabled (default)", async () => {
|
||||
const provider = "openai";
|
||||
const exhaustedConnectionId = "conn-exhausted-disabled-5923";
|
||||
registerQuotaFetcher(provider, async () => ({ used: 100, total: 100, percentUsed: 1 }));
|
||||
|
||||
const combo = {
|
||||
name: `priority-quota-cutoff-disabled-5923-${Date.now()}`,
|
||||
strategy: "priority",
|
||||
models: [
|
||||
{
|
||||
kind: "model",
|
||||
provider,
|
||||
providerId: provider,
|
||||
model: "gpt-4o-mini",
|
||||
connectionId: exhaustedConnectionId,
|
||||
id: "step-a",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const calls: Array<string | null> = [];
|
||||
const response = await handleComboChat({
|
||||
body: { model: combo.name, messages: [{ role: "user", content: "hi" }] },
|
||||
combo,
|
||||
allCombos: [combo],
|
||||
isModelAvailable: undefined,
|
||||
relayOptions: undefined,
|
||||
signal: undefined,
|
||||
// No resilienceSettings override → quotaPreflight.enabled defaults to false (opt-in).
|
||||
settings: {},
|
||||
log: makeLog(),
|
||||
handleSingleModel: async (
|
||||
_body: unknown,
|
||||
modelStr: string,
|
||||
target?: { connectionId?: string | null }
|
||||
) => {
|
||||
calls.push(target?.connectionId ?? null);
|
||||
return okResponse(modelStr);
|
||||
},
|
||||
} as Parameters<typeof handleComboChat>[0]);
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.deepEqual(
|
||||
calls,
|
||||
[exhaustedConnectionId],
|
||||
"with the cutoff setting OFF (default), the exhausted connection must still be dispatched to (unchanged auto-off behavior)"
|
||||
);
|
||||
});
|
||||
@@ -1,88 +0,0 @@
|
||||
import { test, after } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { resolveAutoStrategyOrder } from "@omniroute/open-sse/services/combo/resolveAutoStrategy.ts";
|
||||
import { resetDbInstance } from "@/lib/db/core.ts";
|
||||
|
||||
// resolveAutoStrategyOrder loads the LKGP via the DB singleton (dynamic import);
|
||||
// release the handle so the node:test runner does not hang on teardown (learning #3).
|
||||
after(() => {
|
||||
resetDbInstance();
|
||||
});
|
||||
|
||||
// Split guard for Block J Task 2 (coupled slice): the `if (strategy === "auto")`
|
||||
// branch of handleComboChat was extracted verbatim into resolveAutoStrategyOrder,
|
||||
// with `buildAutoCandidates` injected (it lives in combo.ts, so a direct import
|
||||
// would cycle). These tests pin the DI contract and the two control-flow exits
|
||||
// that the host now forwards: an early 429 Response, and the default-ordering
|
||||
// pass-through. The routable-selection path is covered end-to-end by the 60
|
||||
// consumer tests (router-strategies / auto-combo-engine / combo-strategy-fallbacks).
|
||||
|
||||
const noopLog = {
|
||||
info() {},
|
||||
warn() {},
|
||||
error() {},
|
||||
debug() {},
|
||||
} as never;
|
||||
|
||||
const target = (provider: string, modelStr: string): never =>
|
||||
({
|
||||
kind: "model",
|
||||
stepId: "s1",
|
||||
executionKey: `${provider}>${modelStr}`,
|
||||
modelStr,
|
||||
provider,
|
||||
providerId: null,
|
||||
connectionId: null,
|
||||
weight: 1,
|
||||
label: null,
|
||||
}) as never;
|
||||
|
||||
const baseDeps = (buildAutoCandidates: never) =>
|
||||
({
|
||||
orderedTargets: [target("openai", "gpt-4o"), target("anthropic", "claude-3")],
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
combo: { id: "c1", name: "autoc", config: {} },
|
||||
settings: null,
|
||||
config: {},
|
||||
relayOptions: null,
|
||||
resilienceSettings: { quotaPreflight: { enabled: false } },
|
||||
log: noopLog,
|
||||
buildAutoCandidates,
|
||||
}) as never;
|
||||
|
||||
test("exports resolveAutoStrategyOrder", () => {
|
||||
assert.equal(typeof resolveAutoStrategyOrder, "function");
|
||||
});
|
||||
|
||||
test("no candidates -> keeps default ordering, no explicit router", async () => {
|
||||
const build = (async () => []) as never;
|
||||
const result = await resolveAutoStrategyOrder(baseDeps(build));
|
||||
assert.ok(!("earlyResponse" in result));
|
||||
if ("orderedTargets" in result) {
|
||||
assert.equal(result.autoUsedExplicitRouter, false);
|
||||
// default ordering preserved (both original targets survive)
|
||||
assert.equal(result.orderedTargets.length, 2);
|
||||
assert.equal(result.orderedTargets[0].provider, "openai");
|
||||
}
|
||||
});
|
||||
|
||||
test("all candidates quota-cutoff-blocked -> early 429 Response", async () => {
|
||||
const build = (async () => [
|
||||
{
|
||||
kind: "model",
|
||||
stepId: "s1",
|
||||
executionKey: "openai>gpt-4o",
|
||||
modelStr: "gpt-4o",
|
||||
provider: "openai",
|
||||
model: "gpt-4o",
|
||||
quotaCutoffBlocked: true,
|
||||
},
|
||||
]) as never;
|
||||
const result = await resolveAutoStrategyOrder(baseDeps(build));
|
||||
assert.ok("earlyResponse" in result);
|
||||
if ("earlyResponse" in result) {
|
||||
assert.ok(result.earlyResponse instanceof Response);
|
||||
assert.equal(result.earlyResponse.status, 429);
|
||||
}
|
||||
});
|
||||
@@ -14,22 +14,16 @@ import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
// The LKGP fallback (and every non-auto strategy ordering) was extracted verbatim
|
||||
// from combo.ts into the applyStrategyOrdering leaf (Block J Task 3); the guard
|
||||
// scans follow the code to the leaf that now owns the `target.modelStr` usages.
|
||||
const STRATEGY_SRC = path.resolve(
|
||||
__dirname,
|
||||
"../../open-sse/services/combo/applyStrategyOrdering.ts"
|
||||
);
|
||||
const COMBO_SRC = path.resolve(__dirname, "../../open-sse/services/combo.ts");
|
||||
const TEST_ROUTE_SRC = path.resolve(__dirname, "../../src/app/api/combos/test/route.ts");
|
||||
|
||||
test("#2359 LKGP findIndex guards modelStr against non-string", () => {
|
||||
const src = fs.readFileSync(STRATEGY_SRC, "utf8");
|
||||
test("#2359 combo.ts LKGP findIndex guards modelStr against non-string", () => {
|
||||
const src = fs.readFileSync(COMBO_SRC, "utf8");
|
||||
// The findIndex on orderedTargets must check `typeof target.modelStr === "string"`
|
||||
// before calling .startsWith. Anchor on the LKGP fallback branch.
|
||||
assert.ok(
|
||||
/typeof target\.modelStr === "string"[\s\S]{0,80}target\.modelStr\.startsWith/.test(src),
|
||||
"LKGP fallback must type-check target.modelStr before calling .startsWith"
|
||||
"LKGP fallback in combo.ts must type-check target.modelStr before calling .startsWith"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -47,8 +41,8 @@ test("#2359 combo test route falls back instead of throwing on missing modelStr"
|
||||
);
|
||||
});
|
||||
|
||||
test("#2359 strategy ordering has no remaining unguarded target.modelStr.<method> usages", () => {
|
||||
const src = fs.readFileSync(STRATEGY_SRC, "utf8");
|
||||
test("#2359 combo.ts has no remaining unguarded target.modelStr.<method> usages", () => {
|
||||
const src = fs.readFileSync(COMBO_SRC, "utf8");
|
||||
// Strip the line that contains the guard so the regex below only catches
|
||||
// direct, unguarded method calls.
|
||||
const stripped = src.replace(/typeof target\.modelStr === "string"[^\n]*\n[^\n]*/g, "");
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { buildTargetTimeoutRunner } from "../../open-sse/services/combo/targetTimeoutRunner.ts";
|
||||
|
||||
const noopLog = { warn() {}, info() {}, error() {}, debug() {} } as any;
|
||||
|
||||
test("timeout<=0: passthrough direto (sem timer)", async () => {
|
||||
let called = false;
|
||||
const runner = buildTargetTimeoutRunner({
|
||||
handleSingleModel: async () => {
|
||||
called = true;
|
||||
return new Response("ok");
|
||||
},
|
||||
comboTargetTimeoutMs: 0,
|
||||
log: noopLog,
|
||||
});
|
||||
const res = await runner({}, "m");
|
||||
assert.equal(called, true);
|
||||
assert.equal(await res.text(), "ok");
|
||||
});
|
||||
|
||||
test("timeout<=0: erro do upstream vira errorResponse 502", async () => {
|
||||
const runner = buildTargetTimeoutRunner({
|
||||
handleSingleModel: async () => {
|
||||
throw new Error("boom");
|
||||
},
|
||||
comboTargetTimeoutMs: 0,
|
||||
log: noopLog,
|
||||
});
|
||||
const res = await runner({}, "m");
|
||||
assert.equal(res.status, 502);
|
||||
});
|
||||
|
||||
test("excede o limite: aborta e retorna 524 timed out", async () => {
|
||||
const runner = buildTargetTimeoutRunner({
|
||||
handleSingleModel: (_b, _m, target) =>
|
||||
new Promise<Response>((resolve) => {
|
||||
// resolve só se abortado (simula um upstream que respeita o signal)
|
||||
const sig = (target as any)?.modelAbortSignal as AbortSignal | undefined;
|
||||
sig?.addEventListener("abort", () => resolve(new Response(null, { status: 599 })));
|
||||
}),
|
||||
comboTargetTimeoutMs: 20,
|
||||
log: noopLog,
|
||||
});
|
||||
const res = await runner({}, "slow-model");
|
||||
assert.equal(res.status, 524);
|
||||
const body = await res.json();
|
||||
assert.match(JSON.stringify(body), /timed out/i);
|
||||
});
|
||||
|
||||
test("sucesso rápido vence a corrida do timeout", async () => {
|
||||
const runner = buildTargetTimeoutRunner({
|
||||
handleSingleModel: async () => new Response("fast", { status: 200 }),
|
||||
comboTargetTimeoutMs: 1000,
|
||||
log: noopLog,
|
||||
});
|
||||
const res = await runner({}, "m");
|
||||
assert.equal(res.status, 200);
|
||||
assert.equal(await res.text(), "fast");
|
||||
});
|
||||
|
||||
test("hedge do parent já abortado propaga o abort ao filho", async () => {
|
||||
const parent = new AbortController();
|
||||
parent.abort(new Error("hedge-cancelled"));
|
||||
let sawAbort = false;
|
||||
const runner = buildTargetTimeoutRunner({
|
||||
handleSingleModel: (_b, _m, target) =>
|
||||
new Promise<Response>((resolve) => {
|
||||
const sig = (target as any)?.modelAbortSignal as AbortSignal | undefined;
|
||||
if (sig?.aborted) sawAbort = true;
|
||||
resolve(new Response("ok"));
|
||||
}),
|
||||
comboTargetTimeoutMs: 1000,
|
||||
log: noopLog,
|
||||
});
|
||||
await runner({}, "m", { modelAbortSignal: parent.signal } as any);
|
||||
assert.equal(sawAbort, true);
|
||||
});
|
||||
@@ -166,200 +166,3 @@ test("a 200/benign status with no exhaustion mutates nothing and returns false",
|
||||
0
|
||||
);
|
||||
});
|
||||
|
||||
test("does NOT mark provider exhausted for per-model-quota providers (different model)", () => {
|
||||
const s = sets();
|
||||
const exhausted = applyComboTargetExhaustion(target({ provider: "gemini" }), {
|
||||
...baseOpts,
|
||||
result: { status: 429 },
|
||||
fallbackResult: { reason: "quota_exhausted" },
|
||||
errorText: "quota exceeded for model gpt-4",
|
||||
sets: s,
|
||||
});
|
||||
assert.equal(exhausted, false);
|
||||
assert.equal(s.exhaustedProviders.has("gemini"), false);
|
||||
assert.ok(s.transientRateLimitedProviders.has("gemini"));
|
||||
});
|
||||
|
||||
test("does NOT mark provider exhausted for empty provider strings", () => {
|
||||
const s = sets();
|
||||
const exhausted = applyComboTargetExhaustion(target({ provider: "" }), {
|
||||
...baseOpts,
|
||||
result: { status: 503 },
|
||||
fallbackResult: { error: { code: "quota_exhausted" } },
|
||||
errorText: "quota exhausted",
|
||||
allAccountsRateLimited: true,
|
||||
sets: s,
|
||||
});
|
||||
assert.equal(exhausted, false);
|
||||
});
|
||||
|
||||
test("does NOT mark transientRateLimited on 429 when isTokenLimitBreach is true", () => {
|
||||
const s = sets();
|
||||
const exhausted = applyComboTargetExhaustion(target(), {
|
||||
...baseOpts,
|
||||
result: { status: 429 },
|
||||
fallbackResult: {},
|
||||
errorText: "Token limit exceeded",
|
||||
isTokenLimitBreach: true,
|
||||
sets: s,
|
||||
});
|
||||
assert.equal(exhausted, false);
|
||||
assert.equal(s.transientRateLimitedProviders.has("test-dedup-provider"), false);
|
||||
assert.equal(s.exhaustedProviders.has("test-dedup-provider"), false);
|
||||
});
|
||||
|
||||
test("does NOT mark anything for circuit-open (X-OmniRoute-Provider-Breaker header)", () => {
|
||||
const s = sets();
|
||||
const exhausted = applyComboTargetExhaustion(target(), {
|
||||
...baseOpts,
|
||||
result: { status: 503, headers: new Map([["x-omniroute-provider-breaker", "open"]]) },
|
||||
fallbackResult: {},
|
||||
errorText: "",
|
||||
sets: s,
|
||||
});
|
||||
assert.equal(exhausted, false);
|
||||
assert.equal(s.exhaustedProviders.has("test-dedup-provider"), false);
|
||||
assert.equal(s.exhaustedConnections.has("test-dedup-provider:conn-1"), false);
|
||||
assert.equal(s.transientRateLimitedProviders.has("test-dedup-provider"), false);
|
||||
});
|
||||
|
||||
test("does NOT mark exhaustion for non-connection-level status codes (400)", () => {
|
||||
const s = sets();
|
||||
const exhausted = applyComboTargetExhaustion(target(), {
|
||||
...baseOpts,
|
||||
result: { status: 400 },
|
||||
fallbackResult: {},
|
||||
errorText: "Bad Request",
|
||||
sets: s,
|
||||
});
|
||||
assert.equal(exhausted, false);
|
||||
assert.equal(s.exhaustedConnections.size, 0);
|
||||
assert.equal(s.exhaustedProviders.size, 0);
|
||||
assert.equal(s.transientRateLimitedProviders.size, 0);
|
||||
});
|
||||
|
||||
test("does NOT mark connection exhausted for per-model-quota provider on 500 (gemini model-level error)", () => {
|
||||
const s = sets();
|
||||
const exhausted = applyComboTargetExhaustion(
|
||||
target({ provider: "gemini", connectionId: "gemini-conn-1" }),
|
||||
{
|
||||
...baseOpts,
|
||||
result: { status: 500 },
|
||||
fallbackResult: {},
|
||||
errorText: "Internal error encountered.",
|
||||
rawModel: "gemma-4-31b-it",
|
||||
sets: s,
|
||||
}
|
||||
);
|
||||
assert.equal(exhausted, false);
|
||||
assert.equal(s.exhaustedProviders.has("gemini"), false);
|
||||
assert.equal(s.exhaustedConnections.has("gemini:gemini-conn-1"), false);
|
||||
assert.equal(s.transientRateLimitedProviders.has("gemini"), false);
|
||||
});
|
||||
|
||||
// Sanitized Gemini 500 response — model-level "Internal error encountered" should NOT exhaust
|
||||
// the connection, allowing sibling models on the same provider to be tried.
|
||||
test("gemini 500 INTERNAL (sanitized real response) does NOT exhaust connection — sibling retry", () => {
|
||||
const s = sets();
|
||||
const exhausted = applyComboTargetExhaustion(
|
||||
target({ provider: "gemini", connectionId: "gemini-key-abc" }),
|
||||
{
|
||||
...baseOpts,
|
||||
result: { status: 500 },
|
||||
fallbackResult: {},
|
||||
errorText: "Internal error encountered.",
|
||||
rawModel: "gemma-4-31b-it",
|
||||
structuredError: { code: 500, status: "INTERNAL", message: "Internal error encountered." },
|
||||
sets: s,
|
||||
}
|
||||
);
|
||||
assert.equal(exhausted, false, "providerExhausted must be false");
|
||||
assert.equal(s.exhaustedProviders.has("gemini"), false, "must not exhaust provider");
|
||||
assert.equal(
|
||||
s.exhaustedConnections.has("gemini:gemini-key-abc"),
|
||||
false,
|
||||
"must not exhaust connection — sibling model may succeed"
|
||||
);
|
||||
assert.equal(s.transientRateLimitedProviders.has("gemini"), false);
|
||||
});
|
||||
|
||||
// Non-500 connection-level errors MUST exhaust the connection even for per-model-quota providers.
|
||||
// A 503 (Service Unavailable) means the upstream is down — retrying sibling models wastes calls.
|
||||
test("gemini 503 DOES exhaust connection (upstream down, not model-level)", () => {
|
||||
const s = sets();
|
||||
const exhausted = applyComboTargetExhaustion(
|
||||
target({ provider: "gemini", connectionId: "gemini-key-abc" }),
|
||||
{
|
||||
...baseOpts,
|
||||
result: { status: 503 },
|
||||
fallbackResult: {},
|
||||
errorText: "The service is currently unavailable.",
|
||||
rawModel: "gemma-4-31b-it",
|
||||
sets: s,
|
||||
}
|
||||
);
|
||||
assert.equal(exhausted, false, "providerExhausted is false (not quota)");
|
||||
assert.equal(
|
||||
s.exhaustedConnections.has("gemini:gemini-key-abc"),
|
||||
true,
|
||||
"503 must exhaust connection — upstream is down"
|
||||
);
|
||||
assert.equal(s.exhaustedProviders.size, 0);
|
||||
});
|
||||
|
||||
test("gemini 502 DOES exhaust connection (bad gateway)", () => {
|
||||
const s = sets();
|
||||
const exhausted = applyComboTargetExhaustion(
|
||||
target({ provider: "gemini", connectionId: "gemini-key-abc" }),
|
||||
{
|
||||
...baseOpts,
|
||||
result: { status: 502 },
|
||||
fallbackResult: {},
|
||||
errorText: "Bad Gateway",
|
||||
rawModel: "gemma-4-31b-it",
|
||||
sets: s,
|
||||
}
|
||||
);
|
||||
assert.equal(exhausted, false);
|
||||
assert.equal(s.exhaustedConnections.has("gemini:gemini-key-abc"), true);
|
||||
});
|
||||
|
||||
test("gemini 504 DOES exhaust connection (gateway timeout)", () => {
|
||||
const s = sets();
|
||||
applyComboTargetExhaustion(target({ provider: "gemini", connectionId: "gemini-key-abc" }), {
|
||||
...baseOpts,
|
||||
result: { status: 504 },
|
||||
fallbackResult: {},
|
||||
errorText: "Gateway Timeout",
|
||||
rawModel: "gemini-2.0-flash",
|
||||
sets: s,
|
||||
});
|
||||
assert.equal(s.exhaustedConnections.has("gemini:gemini-key-abc"), true);
|
||||
});
|
||||
|
||||
test("gemini 408 DOES exhaust connection (request timeout)", () => {
|
||||
const s = sets();
|
||||
applyComboTargetExhaustion(target({ provider: "gemini", connectionId: "gemini-key-abc" }), {
|
||||
...baseOpts,
|
||||
result: { status: 408 },
|
||||
fallbackResult: {},
|
||||
errorText: "Request Timeout",
|
||||
rawModel: "gemini-2.0-flash",
|
||||
sets: s,
|
||||
});
|
||||
assert.equal(s.exhaustedConnections.has("gemini:gemini-key-abc"), true);
|
||||
});
|
||||
|
||||
test("gemini 524 DOES exhaust connection (cloudflare timeout)", () => {
|
||||
const s = sets();
|
||||
applyComboTargetExhaustion(target({ provider: "gemini", connectionId: "gemini-key-abc" }), {
|
||||
...baseOpts,
|
||||
result: { status: 524 },
|
||||
fallbackResult: {},
|
||||
errorText: "A Timeout Occurred",
|
||||
rawModel: "gemini-2.0-flash",
|
||||
sets: s,
|
||||
});
|
||||
assert.equal(s.exhaustedConnections.has("gemini:gemini-key-abc"), true);
|
||||
});
|
||||
|
||||
@@ -3,7 +3,6 @@ import assert from "node:assert/strict";
|
||||
import { NextResponse } from "next/server";
|
||||
import {
|
||||
applyCorsHeaders,
|
||||
getCorsStatus,
|
||||
resolveAllowedOrigin,
|
||||
setRuntimeAllowedOrigins,
|
||||
STATIC_CORS_HEADERS,
|
||||
@@ -189,48 +188,6 @@ describe("cors/origins.applyCorsHeaders", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("cors/origins.getCorsStatus", () => {
|
||||
let envSnap: Record<string, string | undefined>;
|
||||
|
||||
beforeEach(() => {
|
||||
envSnap = snapshotEnv();
|
||||
for (const key of ENV_KEYS) delete process.env[key];
|
||||
setRuntimeAllowedOrigins("");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
restoreEnv(envSnap);
|
||||
setRuntimeAllowedOrigins("");
|
||||
});
|
||||
|
||||
it("default (no env, no runtime) → allowAll false, empty origins", () => {
|
||||
assert.deepEqual(getCorsStatus(), { allowAll: false, allowedOrigins: [] });
|
||||
});
|
||||
|
||||
it("CORS_ALLOW_ALL=true → allowAll true", () => {
|
||||
process.env.CORS_ALLOW_ALL = "true";
|
||||
assert.equal(getCorsStatus().allowAll, true);
|
||||
});
|
||||
|
||||
it("legacy CORS_ORIGIN=* → allowAll true", () => {
|
||||
process.env.CORS_ORIGIN = "*";
|
||||
assert.equal(getCorsStatus().allowAll, true);
|
||||
});
|
||||
|
||||
it("merges env + runtime allowlists, normalized, sorted, deduped", () => {
|
||||
process.env.CORS_ALLOWED_ORIGINS = "https://Env.Example.com/, https://shared.example.com";
|
||||
setRuntimeAllowedOrigins("https://runtime.example.com, https://shared.example.com/");
|
||||
assert.deepEqual(getCorsStatus(), {
|
||||
allowAll: false,
|
||||
allowedOrigins: [
|
||||
"https://env.example.com",
|
||||
"https://runtime.example.com",
|
||||
"https://shared.example.com",
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("cors/origins.STATIC_CORS_HEADERS", () => {
|
||||
it("never contains Access-Control-Allow-Origin", () => {
|
||||
assert.equal(
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
// Split-guard for the cursor executor pure-helper extraction.
|
||||
// Two pure leaves: cursor/prompt.ts (isRecordLike + toolChoiceDirectiveLine +
|
||||
// buildCursorOutputConstraints) and cursor/composer.ts (composer thinking decoding).
|
||||
// The host re-exports the 3 composer helpers for external importers (tests).
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const EXE = join(HERE, "../../open-sse/executors");
|
||||
const HOST = join(EXE, "cursor.ts");
|
||||
const PROMPT = join(EXE, "cursor/prompt.ts");
|
||||
const COMPOSER = join(EXE, "cursor/composer.ts");
|
||||
|
||||
test("leaves are pure and do not import the host", () => {
|
||||
const prompt = readFileSync(PROMPT, "utf8");
|
||||
const composer = readFileSync(COMPOSER, "utf8");
|
||||
assert.match(prompt, /export function toolChoiceDirectiveLine\b/);
|
||||
assert.match(prompt, /export function buildCursorOutputConstraints\b/);
|
||||
assert.match(composer, /export function isComposerModel\b/);
|
||||
assert.doesNotMatch(prompt, /from "\.\.\/cursor\.ts"/);
|
||||
assert.doesNotMatch(composer, /from "\.\.\/cursor\.ts"/);
|
||||
});
|
||||
|
||||
test("host re-exports the composer helpers", () => {
|
||||
const host = readFileSync(HOST, "utf8");
|
||||
assert.match(host, /from "\.\/cursor\/composer\.ts"/);
|
||||
assert.match(host, /from "\.\/cursor\/prompt\.ts"/);
|
||||
});
|
||||
|
||||
test("composer thinking decoding behaves via the leaf", async () => {
|
||||
const { visibleComposerContentFromThinking, composerReasoningRemainder, isComposerModel } =
|
||||
await import("../../open-sse/executors/cursor/composer.ts");
|
||||
assert.equal(isComposerModel("composer-1"), true);
|
||||
assert.equal(isComposerModel("gpt-4"), false);
|
||||
assert.equal(visibleComposerContentFromThinking("hidden</think>visible"), "visible");
|
||||
assert.equal(composerReasoningRemainder("hidden</think>visible"), "hidden");
|
||||
});
|
||||
|
||||
test("prompt constraint builder behaves via the leaf", async () => {
|
||||
const { toolChoiceDirectiveLine, buildCursorOutputConstraints } =
|
||||
await import("../../open-sse/executors/cursor/prompt.ts");
|
||||
assert.match(toolChoiceDirectiveLine("required"), /MUST call at least one/);
|
||||
assert.equal(toolChoiceDirectiveLine("auto"), "");
|
||||
assert.match(buildCursorOutputConstraints({ max_tokens: 100 }), /100 output tokens/);
|
||||
assert.equal(buildCursorOutputConstraints({}), "");
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
/**
|
||||
* MuxServiceTab unit test — verifies module shape only (no DOM renderer wired
|
||||
* into the node:test runner for this suite; mirrors CliproxyServiceTab.tsx's
|
||||
* module-shape test).
|
||||
*/
|
||||
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
describe("MuxServiceTab — module shape", () => {
|
||||
it("exports MuxServiceTab function", async () => {
|
||||
const mod = await import(
|
||||
"../../../../../src/app/(dashboard)/dashboard/providers/services/tabs/MuxServiceTab.tsx"
|
||||
);
|
||||
assert.equal(typeof mod.MuxServiceTab, "function");
|
||||
});
|
||||
});
|
||||
@@ -1,119 +0,0 @@
|
||||
// #1290 — bare-access-token Codex import. createProviderConnection must
|
||||
// never dedup authType "access_token" rows (each import is intentionally a
|
||||
// new connection — a raw access token has no stable long-lived identity to
|
||||
// safely match against), and must derive a connection name from email when
|
||||
// no explicit name is supplied, mirroring the existing "oauth" behavior.
|
||||
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-access-token-1290-"));
|
||||
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");
|
||||
|
||||
async function resetStorage() {
|
||||
core.resetDbInstance();
|
||||
for (let attempt = 0; attempt < 10; attempt++) {
|
||||
try {
|
||||
if (fs.existsSync(TEST_DATA_DIR)) {
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
}
|
||||
break;
|
||||
} catch (error: unknown) {
|
||||
const code = (error as { code?: string } | null)?.code;
|
||||
if ((code === "EBUSY" || code === "EPERM") && attempt < 9) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 50 * (attempt + 1)));
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await resetStorage();
|
||||
});
|
||||
|
||||
test.after(async () => {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("createProviderConnection: authType access_token never dedups — same email creates a new row each time", async () => {
|
||||
const first = await providersDb.createProviderConnection({
|
||||
provider: "codex",
|
||||
authType: "access_token",
|
||||
accessToken: "eyJfirst.token.sig",
|
||||
email: "user@example.com",
|
||||
testStatus: "active",
|
||||
});
|
||||
const second = await providersDb.createProviderConnection({
|
||||
provider: "codex",
|
||||
authType: "access_token",
|
||||
accessToken: "eyJsecond.token.sig",
|
||||
email: "user@example.com",
|
||||
testStatus: "active",
|
||||
});
|
||||
|
||||
assert.notEqual(first.id, second.id);
|
||||
|
||||
const rows = await providersDb.getProviderConnections({ provider: "codex" });
|
||||
assert.equal(rows.length, 2);
|
||||
assert.deepEqual(
|
||||
rows.map((r) => r.accessToken).sort(),
|
||||
["eyJfirst.token.sig", "eyJsecond.token.sig"].sort()
|
||||
);
|
||||
});
|
||||
|
||||
test("createProviderConnection: authType access_token falls back to email for the connection name", async () => {
|
||||
const conn = await providersDb.createProviderConnection({
|
||||
provider: "codex",
|
||||
authType: "access_token",
|
||||
accessToken: "eyJtoken.sig",
|
||||
email: "labeled@example.com",
|
||||
});
|
||||
|
||||
assert.equal(conn.name, "labeled@example.com");
|
||||
});
|
||||
|
||||
test("createProviderConnection: authType access_token prefers an explicit name over email", async () => {
|
||||
const conn = await providersDb.createProviderConnection({
|
||||
provider: "codex",
|
||||
authType: "access_token",
|
||||
accessToken: "eyJtoken.sig",
|
||||
email: "labeled@example.com",
|
||||
name: "My Bare Token",
|
||||
});
|
||||
|
||||
assert.equal(conn.name, "My Bare Token");
|
||||
});
|
||||
|
||||
test("createProviderConnection: authType access_token does not collide with an existing oauth row for the same email", async () => {
|
||||
const oauthConn = await providersDb.createProviderConnection({
|
||||
provider: "codex",
|
||||
authType: "oauth",
|
||||
accessToken: "oauth-access",
|
||||
refreshToken: "oauth-refresh",
|
||||
email: "shared@example.com",
|
||||
});
|
||||
const tokenConn = await providersDb.createProviderConnection({
|
||||
provider: "codex",
|
||||
authType: "access_token",
|
||||
accessToken: "eyJbare.token.sig",
|
||||
email: "shared@example.com",
|
||||
});
|
||||
|
||||
assert.notEqual(oauthConn.id, tokenConn.id);
|
||||
|
||||
// The oauth row must be untouched (not silently overwritten by the
|
||||
// access_token import).
|
||||
const refreshed = await providersDb.getProviderConnections({ provider: "codex" });
|
||||
const oauthRow = refreshed.find((r) => r.id === oauthConn.id);
|
||||
assert.equal(oauthRow?.authType, "oauth");
|
||||
assert.equal(oauthRow?.refreshToken, "oauth-refresh");
|
||||
});
|
||||
@@ -1,143 +0,0 @@
|
||||
import { describe, test, before, after } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
// Isolate the DB into a throwaway DATA_DIR so migrations run against a fresh file.
|
||||
let tmpDataDir: string;
|
||||
let mod: typeof import("@/lib/db/discoveryResults");
|
||||
let core: typeof import("@/lib/db/core");
|
||||
|
||||
before(async () => {
|
||||
tmpDataDir = mkdtempSync(join(tmpdir(), "omniroute-discovery-"));
|
||||
process.env.DATA_DIR = tmpDataDir;
|
||||
core = await import("@/lib/db/core");
|
||||
core.resetDbInstance();
|
||||
// Touch the instance so migrations (incl. 074_discovery_results) apply.
|
||||
core.getDbInstance();
|
||||
mod = await import("@/lib/db/discoveryResults");
|
||||
});
|
||||
|
||||
after(() => {
|
||||
core.resetDbInstance();
|
||||
if (tmpDataDir) rmSync(tmpDataDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe("discoveryResults DB module", () => {
|
||||
test("upsert inserts a new row and returns it with an id", () => {
|
||||
const row = mod.upsertDiscoveryResult({
|
||||
providerId: "acme",
|
||||
method: "free_tier",
|
||||
authType: "none",
|
||||
feasibility: 4,
|
||||
riskLevel: "low",
|
||||
status: "pending",
|
||||
models: ["acme-large", "acme-small"],
|
||||
endpoint: "https://acme.example/api",
|
||||
});
|
||||
assert.ok(typeof row.id === "number" && row.id > 0);
|
||||
assert.equal(row.providerId, "acme");
|
||||
assert.deepEqual(row.models, ["acme-large", "acme-small"]);
|
||||
assert.equal(row.status, "pending");
|
||||
});
|
||||
|
||||
test("upsert on the same (provider, method, endpoint) updates instead of duplicating", () => {
|
||||
const first = mod.upsertDiscoveryResult({
|
||||
providerId: "beta",
|
||||
method: "web_cookie",
|
||||
authType: "cookie",
|
||||
feasibility: 3,
|
||||
riskLevel: "medium",
|
||||
status: "pending",
|
||||
endpoint: "https://beta.example/chat",
|
||||
});
|
||||
const second = mod.upsertDiscoveryResult({
|
||||
providerId: "beta",
|
||||
method: "web_cookie",
|
||||
authType: "cookie",
|
||||
feasibility: 5,
|
||||
riskLevel: "medium",
|
||||
status: "testing",
|
||||
endpoint: "https://beta.example/chat",
|
||||
});
|
||||
assert.equal(second.id, first.id);
|
||||
assert.equal(second.feasibility, 5);
|
||||
assert.equal(second.status, "testing");
|
||||
const all = mod.getDiscoveryResults("beta");
|
||||
assert.equal(all.length, 1);
|
||||
});
|
||||
|
||||
test("getDiscoveryResults filters by providerId and returns all when omitted", () => {
|
||||
const beta = mod.getDiscoveryResults("beta");
|
||||
assert.ok(beta.every((r) => r.providerId === "beta"));
|
||||
const all = mod.getDiscoveryResults();
|
||||
assert.ok(all.length >= 2);
|
||||
});
|
||||
|
||||
test("getDiscoveryResultById returns the row or null", () => {
|
||||
const created = mod.upsertDiscoveryResult({
|
||||
providerId: "gamma",
|
||||
method: "trial",
|
||||
authType: "api_key",
|
||||
feasibility: 2,
|
||||
riskLevel: "low",
|
||||
status: "pending",
|
||||
});
|
||||
const found = mod.getDiscoveryResultById(created.id!);
|
||||
assert.equal(found?.providerId, "gamma");
|
||||
assert.equal(mod.getDiscoveryResultById(999999), null);
|
||||
});
|
||||
|
||||
test("markVerified sets status=verified and stamps verified_at", () => {
|
||||
const created = mod.upsertDiscoveryResult({
|
||||
providerId: "delta",
|
||||
method: "public_api",
|
||||
authType: "api_key",
|
||||
feasibility: 5,
|
||||
riskLevel: "none",
|
||||
status: "pending",
|
||||
});
|
||||
const updated = mod.markVerified(created.id!);
|
||||
assert.equal(updated?.status, "verified");
|
||||
assert.ok(updated?.verifiedAt);
|
||||
});
|
||||
|
||||
test("markVerified on a missing id returns null", () => {
|
||||
assert.equal(mod.markVerified(999999), null);
|
||||
});
|
||||
|
||||
test("deleteDiscoveryResult removes the row and returns true, false if absent", () => {
|
||||
const created = mod.upsertDiscoveryResult({
|
||||
providerId: "epsilon",
|
||||
method: "free_tier",
|
||||
authType: "none",
|
||||
feasibility: 1,
|
||||
riskLevel: "none",
|
||||
status: "pending",
|
||||
});
|
||||
assert.equal(mod.deleteDiscoveryResult(created.id!), true);
|
||||
assert.equal(mod.getDiscoveryResultById(created.id!), null);
|
||||
assert.equal(mod.deleteDiscoveryResult(created.id!), false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("discovery service reporter delegation", () => {
|
||||
test("persistDiscoveryResult writes through and getDiscoveryResults reads it back", async () => {
|
||||
const svc = await import("@/lib/discovery/index");
|
||||
const saved = svc.persistDiscoveryResult({
|
||||
providerId: "zeta",
|
||||
method: "public_api",
|
||||
authType: "api_key",
|
||||
feasibility: 5,
|
||||
riskLevel: "none",
|
||||
status: "verified",
|
||||
models: ["zeta-1"],
|
||||
});
|
||||
assert.ok(saved.id! > 0);
|
||||
const read = svc.getDiscoveryResults("zeta");
|
||||
assert.equal(read.length, 1);
|
||||
assert.equal(read[0].providerId, "zeta");
|
||||
assert.deepEqual(read[0].models, ["zeta-1"]);
|
||||
});
|
||||
});
|
||||
@@ -1,135 +0,0 @@
|
||||
import { describe, it, afterEach } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
DeepSeekWebWithAutoRefreshExecutor,
|
||||
} from "../../open-sse/executors/deepseek-web-with-auto-refresh.ts";
|
||||
import { DeepSeekWebExecutor } from "../../open-sse/executors/deepseek-web.ts";
|
||||
|
||||
// Regression: the base DeepSeekWebExecutor.execute() never throws — it converts
|
||||
// upstream auth failures (401/403) into a returned error Response. The auto-refresh
|
||||
// subclass used to trigger refresh+retry only from its catch block (thrown errors),
|
||||
// so the retry path was dead code: a stale access token surfaced a 401 to the client
|
||||
// on every refresh boundary instead of self-healing. See executor-deepseek-web-auto-refresh.
|
||||
|
||||
const baseProto = DeepSeekWebExecutor.prototype;
|
||||
const originalExecute = baseProto.execute;
|
||||
|
||||
function makeInput(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
model: "deepseek-web",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: false,
|
||||
credentials: { apiKey: "user-token-abc" },
|
||||
signal: null,
|
||||
log: null,
|
||||
...overrides,
|
||||
} as any;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
baseProto.execute = originalExecute;
|
||||
});
|
||||
|
||||
describe("DeepSeekWebWithAutoRefreshExecutor — 401 Response retry (regression)", () => {
|
||||
it("refreshes and retries when the base returns a 401 error Response", async () => {
|
||||
const executor = new DeepSeekWebWithAutoRefreshExecutor({ autoRefresh: false });
|
||||
|
||||
let baseCalls = 0;
|
||||
baseProto.execute = async function () {
|
||||
baseCalls++;
|
||||
if (baseCalls === 1) {
|
||||
return {
|
||||
response: new Response(
|
||||
JSON.stringify({ error: { message: "DeepSeek token expired" } }),
|
||||
{ status: 401, headers: { "Content-Type": "application/json" } }
|
||||
),
|
||||
url: "https://chat.deepseek.com/api/v0/chat/completion",
|
||||
headers: {},
|
||||
transformedBody: {},
|
||||
};
|
||||
}
|
||||
return {
|
||||
response: new Response(
|
||||
JSON.stringify({ choices: [{ message: { role: "assistant", content: "ok" } }] }),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } }
|
||||
),
|
||||
url: "https://chat.deepseek.com/api/v0/chat/completion",
|
||||
headers: {},
|
||||
transformedBody: {},
|
||||
};
|
||||
};
|
||||
|
||||
let refreshCalls = 0;
|
||||
(executor as any).doRefreshSession = async () => {
|
||||
refreshCalls++;
|
||||
};
|
||||
|
||||
const result = await executor.execute(makeInput());
|
||||
|
||||
assert.equal(refreshCalls, 1, "auto-refresh should fire exactly once on a 401 Response");
|
||||
assert.equal(baseCalls, 2, "base execute should run twice (initial 401 → refreshed retry)");
|
||||
assert.equal(result.response.status, 200, "the retried request's success should reach the client");
|
||||
});
|
||||
|
||||
it("does not refresh/retry on a successful 200 Response", async () => {
|
||||
const executor = new DeepSeekWebWithAutoRefreshExecutor({ autoRefresh: false });
|
||||
|
||||
let baseCalls = 0;
|
||||
baseProto.execute = async function () {
|
||||
baseCalls++;
|
||||
return {
|
||||
response: new Response(JSON.stringify({ choices: [] }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
url: "https://chat.deepseek.com/api/v0/chat/completion",
|
||||
headers: {},
|
||||
transformedBody: {},
|
||||
};
|
||||
};
|
||||
|
||||
let refreshCalls = 0;
|
||||
(executor as any).doRefreshSession = async () => {
|
||||
refreshCalls++;
|
||||
};
|
||||
|
||||
const result = await executor.execute(makeInput());
|
||||
|
||||
assert.equal(baseCalls, 1, "base execute should run once on success");
|
||||
assert.equal(refreshCalls, 0, "no refresh on a 200 Response");
|
||||
assert.equal(result.response.status, 200);
|
||||
});
|
||||
|
||||
it("stops retrying (no loop) when refresh cannot recover a dead userToken", async () => {
|
||||
const executor = new DeepSeekWebWithAutoRefreshExecutor({ autoRefresh: false });
|
||||
|
||||
let baseCalls = 0;
|
||||
baseProto.execute = async function () {
|
||||
baseCalls++;
|
||||
return {
|
||||
response: new Response(JSON.stringify({ error: { message: "expired" } }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
url: "https://chat.deepseek.com/api/v0/chat/completion",
|
||||
headers: {},
|
||||
transformedBody: {},
|
||||
};
|
||||
};
|
||||
|
||||
let refreshCalls = 0;
|
||||
(executor as any).doRefreshSession = async () => {
|
||||
refreshCalls++;
|
||||
throw new Error("Token expired — get a new userToken from DeepSeek localStorage");
|
||||
};
|
||||
|
||||
const result = await executor.execute(makeInput());
|
||||
|
||||
// Refresh is attempted once; when it throws (dead userToken), we surface the
|
||||
// original 401 instead of looping forever.
|
||||
assert.equal(refreshCalls, 1, "refresh attempted once");
|
||||
assert.equal(baseCalls, 1, "no retry when refresh itself fails");
|
||||
assert.equal(result.response.status, 401, "original auth failure surfaces to the client");
|
||||
});
|
||||
});
|
||||
@@ -1,34 +0,0 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
// Split-guard for the deepseek-web executor stream-format extraction.
|
||||
// The pure content/citation formatters live in deepseek-web/stream-format.ts
|
||||
// (module-private; host imports them into transformSSE/collectSSEContent).
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const EXE = join(HERE, "../../open-sse/executors");
|
||||
const HOST = join(EXE, "deepseek-web.ts");
|
||||
const LEAF = join(EXE, "deepseek-web/stream-format.ts");
|
||||
|
||||
test("leaf hosts the formatters and does not import the host", () => {
|
||||
const src = readFileSync(LEAF, "utf8");
|
||||
for (const sym of ["formatStreamContent", "appendSearchCitations", "isThinkingModel"]) {
|
||||
assert.match(src, new RegExp(`export function ${sym}\\b`));
|
||||
}
|
||||
assert.doesNotMatch(src, /from "\.\.\/deepseek-web\.ts"/);
|
||||
});
|
||||
|
||||
test("host imports the formatters back from the leaf", () => {
|
||||
const host = readFileSync(HOST, "utf8");
|
||||
assert.match(host, /from "\.\/deepseek-web\/stream-format\.ts"/);
|
||||
});
|
||||
|
||||
test("formatStreamContent + model classifiers behave", async () => {
|
||||
const { isThinkingModel, isSearchModel, formatStreamContent } =
|
||||
await import("../../open-sse/executors/deepseek-web/stream-format.ts");
|
||||
assert.equal(typeof isThinkingModel("deepseek-reasoner"), "boolean");
|
||||
assert.equal(typeof isSearchModel("deepseek-search"), "boolean");
|
||||
assert.equal(typeof formatStreamContent("hi", "deepseek-chat"), "string");
|
||||
});
|
||||
@@ -311,19 +311,8 @@ test("execute: sends PoW response header", async () => {
|
||||
"Has Bearer token"
|
||||
);
|
||||
assert.ok(compCall.headers["X-Ds-Pow-Response"], "Has PoW header");
|
||||
// Header set matches the current chat.deepseek.com web client (v2.0.0):
|
||||
// legacy X-App-Version dropped, X-Client-Bundle-Id added, version bumped.
|
||||
assert.ok(
|
||||
!("X-App-Version" in compCall.headers),
|
||||
"Legacy X-App-Version must not be sent (removed in web client 2.0.0)"
|
||||
);
|
||||
assert.equal(
|
||||
compCall.headers["X-Client-Bundle-Id"],
|
||||
"com.deepseek.chat",
|
||||
"Sends X-Client-Bundle-Id"
|
||||
);
|
||||
assert.equal(compCall.headers["X-Client-Version"], "2.0.0", "Sends current X-Client-Version");
|
||||
assert.equal(compCall.headers["X-Client-Platform"], "web", "Has X-Client-Platform");
|
||||
assert.ok(compCall.headers["X-App-Version"], "Has X-App-Version");
|
||||
assert.ok(compCall.headers["X-Client-Platform"] === "web", "Has X-Client-Platform");
|
||||
} finally {
|
||||
mock.restore();
|
||||
}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
// Split-guard for the default executor URL-normalizer extraction.
|
||||
// The pure per-provider chat-URL normalizers live in default/urlNormalizers.ts
|
||||
// (string transforms only). Host imports them back into buildUrl/transformRequest.
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const EXE = join(HERE, "../../open-sse/executors");
|
||||
const HOST = join(EXE, "default.ts");
|
||||
const LEAF = join(EXE, "default/urlNormalizers.ts");
|
||||
|
||||
test("leaf hosts the normalizers and does not import the host", () => {
|
||||
const src = readFileSync(LEAF, "utf8");
|
||||
for (const sym of [
|
||||
"normalizeOpenAIChatUrl",
|
||||
"normalizeSapChatUrl",
|
||||
"getOpenRouterConnectionPreset",
|
||||
]) {
|
||||
assert.match(src, new RegExp(`export function ${sym}\\b`));
|
||||
}
|
||||
assert.doesNotMatch(src, /from "\.\.\/default\.ts"/);
|
||||
});
|
||||
|
||||
test("host imports the normalizers back from the leaf", () => {
|
||||
const host = readFileSync(HOST, "utf8");
|
||||
assert.match(host, /from "\.\/default\/urlNormalizers\.ts"/);
|
||||
});
|
||||
|
||||
test("normalizeOpenAIChatUrl appends chat/completions for a bare base URL", async () => {
|
||||
const { normalizeOpenAIChatUrl } =
|
||||
await import("../../open-sse/executors/default/urlNormalizers.ts");
|
||||
assert.match(normalizeOpenAIChatUrl("https://api.example.com"), /\/v1\/chat\/completions$/);
|
||||
});
|
||||
@@ -1,195 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const {
|
||||
trackDevice,
|
||||
getDeviceCount,
|
||||
getDeviceDetails,
|
||||
getAllDeviceCounts,
|
||||
expireDevices,
|
||||
extractIpFromHeaders,
|
||||
maskIp,
|
||||
clearDeviceTracker,
|
||||
} = await import("../../open-sse/services/deviceTracker.ts");
|
||||
|
||||
test.beforeEach(() => {
|
||||
delete process.env.DEVICE_TRACKER_TTL_MS;
|
||||
delete process.env.DEVICE_TRACKER_MAX_DEVICES_PER_KEY;
|
||||
delete process.env.DEVICE_TRACKER_MAX_TOTAL_DEVICES;
|
||||
clearDeviceTracker();
|
||||
});
|
||||
|
||||
// ─── Fingerprint dedup ──────────────────────────────────────────────────────
|
||||
|
||||
test("trackDevice: same IP + UA for a key counts as one device", async () => {
|
||||
await trackDevice("key-1", "203.0.113.5", "Mozilla/5.0 test-agent");
|
||||
await trackDevice("key-1", "203.0.113.5", "Mozilla/5.0 test-agent");
|
||||
await trackDevice("key-1", "203.0.113.5", "Mozilla/5.0 test-agent");
|
||||
|
||||
assert.equal(getDeviceCount("key-1"), 1);
|
||||
});
|
||||
|
||||
test("trackDevice: different User-Agent for same key/IP counts as a new device", async () => {
|
||||
await trackDevice("key-1", "203.0.113.5", "curl/8.0");
|
||||
await trackDevice("key-1", "203.0.113.5", "python-requests/2.31");
|
||||
|
||||
assert.equal(getDeviceCount("key-1"), 2);
|
||||
});
|
||||
|
||||
test("trackDevice: different IP for same key/UA counts as a new device", async () => {
|
||||
await trackDevice("key-1", "203.0.113.5", "curl/8.0");
|
||||
await trackDevice("key-1", "198.51.100.9", "curl/8.0");
|
||||
|
||||
assert.equal(getDeviceCount("key-1"), 2);
|
||||
});
|
||||
|
||||
test("trackDevice: repeated tracking refreshes lastSeen instead of duplicating", async () => {
|
||||
await trackDevice("key-1", "203.0.113.5", "curl/8.0");
|
||||
const [before] = getDeviceDetails("key-1");
|
||||
await new Promise((resolve) => setTimeout(resolve, 5));
|
||||
await trackDevice("key-1", "203.0.113.5", "curl/8.0");
|
||||
const [after] = getDeviceDetails("key-1");
|
||||
|
||||
assert.equal(getDeviceCount("key-1"), 1);
|
||||
assert.ok(after.lastSeen >= before.lastSeen);
|
||||
});
|
||||
|
||||
test("trackDevice: no-ops and returns null when apiKeyId is missing", async () => {
|
||||
const fingerprint = await trackDevice(null, "203.0.113.5", "curl/8.0");
|
||||
assert.equal(fingerprint, null);
|
||||
assert.equal(getDeviceCount(null), 0);
|
||||
});
|
||||
|
||||
// ─── Per-key isolation ──────────────────────────────────────────────────────
|
||||
|
||||
test("trackDevice: devices are scoped per API key, not global", async () => {
|
||||
await trackDevice("key-1", "203.0.113.5", "curl/8.0");
|
||||
await trackDevice("key-2", "203.0.113.5", "curl/8.0"); // same fingerprint, different key
|
||||
|
||||
assert.equal(getDeviceCount("key-1"), 1);
|
||||
assert.equal(getDeviceCount("key-2"), 1);
|
||||
|
||||
const allCounts = getAllDeviceCounts();
|
||||
assert.equal(allCounts["key-1"], 1);
|
||||
assert.equal(allCounts["key-2"], 1);
|
||||
});
|
||||
|
||||
// ─── TTL expiry ─────────────────────────────────────────────────────────────
|
||||
|
||||
test("expireDevices: evicts devices whose lastSeen exceeds the TTL window", async () => {
|
||||
process.env.DEVICE_TRACKER_TTL_MS = "1000";
|
||||
clearDeviceTracker();
|
||||
|
||||
await trackDevice("key-1", "203.0.113.5", "curl/8.0");
|
||||
assert.equal(getDeviceCount("key-1"), 1);
|
||||
|
||||
const farFuture = Date.now() + 5000;
|
||||
const expiredCount = expireDevices(farFuture);
|
||||
|
||||
assert.equal(expiredCount, 1);
|
||||
assert.equal(getDeviceCount("key-1"), 0);
|
||||
});
|
||||
|
||||
test("expireDevices: keeps devices seen within the TTL window", async () => {
|
||||
process.env.DEVICE_TRACKER_TTL_MS = "60000";
|
||||
clearDeviceTracker();
|
||||
|
||||
await trackDevice("key-1", "203.0.113.5", "curl/8.0");
|
||||
const soon = Date.now() + 1000;
|
||||
const expiredCount = expireDevices(soon);
|
||||
|
||||
assert.equal(expiredCount, 0);
|
||||
assert.equal(getDeviceCount("key-1"), 1);
|
||||
});
|
||||
|
||||
// ─── Eviction under caps ────────────────────────────────────────────────────
|
||||
|
||||
test("trackDevice: enforces maxDevicesPerApiKey by evicting the oldest device", async () => {
|
||||
process.env.DEVICE_TRACKER_MAX_DEVICES_PER_KEY = "2";
|
||||
clearDeviceTracker();
|
||||
|
||||
await trackDevice("key-1", "203.0.113.1", "ua-1");
|
||||
await new Promise((resolve) => setTimeout(resolve, 2));
|
||||
await trackDevice("key-1", "203.0.113.2", "ua-2");
|
||||
await new Promise((resolve) => setTimeout(resolve, 2));
|
||||
// Third distinct device should evict the oldest (203.0.113.1 / ua-1).
|
||||
await trackDevice("key-1", "203.0.113.3", "ua-3");
|
||||
|
||||
assert.equal(getDeviceCount("key-1"), 2);
|
||||
const uaSet = new Set(getDeviceDetails("key-1").map((d) => d.userAgent));
|
||||
assert.ok(!uaSet.has("ua-1"), "oldest device should have been evicted");
|
||||
assert.ok(uaSet.has("ua-2"));
|
||||
assert.ok(uaSet.has("ua-3"));
|
||||
});
|
||||
|
||||
test("trackDevice: enforces maxTotalDevices globally across all keys", async () => {
|
||||
process.env.DEVICE_TRACKER_MAX_TOTAL_DEVICES = "2";
|
||||
clearDeviceTracker();
|
||||
|
||||
await trackDevice("key-1", "203.0.113.1", "ua-1");
|
||||
await new Promise((resolve) => setTimeout(resolve, 2));
|
||||
await trackDevice("key-2", "203.0.113.2", "ua-2");
|
||||
await new Promise((resolve) => setTimeout(resolve, 2));
|
||||
await trackDevice("key-3", "203.0.113.3", "ua-3");
|
||||
|
||||
const total = Object.values(getAllDeviceCounts()).reduce((a, b) => a + b, 0);
|
||||
assert.equal(total, 2);
|
||||
// key-1's device was the oldest globally and should be gone.
|
||||
assert.equal(getDeviceCount("key-1"), 0);
|
||||
});
|
||||
|
||||
// ─── IP masking ─────────────────────────────────────────────────────────────
|
||||
|
||||
test("maskIp: masks the last two octets of an IPv4 address", () => {
|
||||
assert.equal(maskIp("203.0.113.42"), "203.0.x.x");
|
||||
});
|
||||
|
||||
test("maskIp: masks an IPv6 address down to its first three groups", () => {
|
||||
assert.equal(maskIp("2001:db8:85a3:0:0:8a2e:370:7334"), "2001:db8:85a3:...");
|
||||
});
|
||||
|
||||
test("maskIp: returns 'unknown' for missing/unknown input", () => {
|
||||
assert.equal(maskIp(null), "unknown");
|
||||
assert.equal(maskIp(undefined), "unknown");
|
||||
assert.equal(maskIp("unknown"), "unknown");
|
||||
});
|
||||
|
||||
test("trackDevice: never stores the raw IP — only the masked form", async () => {
|
||||
await trackDevice("key-1", "203.0.113.42", "curl/8.0");
|
||||
const [detail] = getDeviceDetails("key-1");
|
||||
|
||||
assert.equal(detail.ip, "203.0.x.x");
|
||||
assert.notEqual(detail.ip, "203.0.113.42");
|
||||
});
|
||||
|
||||
test("getDeviceDetails: truncates the fingerprint instead of exposing the full SHA-256 hash", async () => {
|
||||
await trackDevice("key-1", "203.0.113.42", "curl/8.0");
|
||||
const [detail] = getDeviceDetails("key-1");
|
||||
|
||||
assert.equal(detail.fingerprint.length, 12);
|
||||
});
|
||||
|
||||
// ─── IP extraction from headers ─────────────────────────────────────────────
|
||||
|
||||
test("extractIpFromHeaders: prefers cf-connecting-ip over x-forwarded-for", () => {
|
||||
const ip = extractIpFromHeaders({
|
||||
"cf-connecting-ip": "203.0.113.5",
|
||||
"x-forwarded-for": "198.51.100.9, 10.0.0.1",
|
||||
});
|
||||
assert.equal(ip, "203.0.113.5");
|
||||
});
|
||||
|
||||
test("extractIpFromHeaders: falls back to the first hop of x-forwarded-for", () => {
|
||||
const ip = extractIpFromHeaders({ "x-forwarded-for": "198.51.100.9, 10.0.0.1" });
|
||||
assert.equal(ip, "198.51.100.9");
|
||||
});
|
||||
|
||||
test("extractIpFromHeaders: works with a real Headers instance", () => {
|
||||
const headers = new Headers({ "x-real-ip": "192.0.2.7" });
|
||||
assert.equal(extractIpFromHeaders(headers), "192.0.2.7");
|
||||
});
|
||||
|
||||
test("extractIpFromHeaders: returns 'unknown' when no IP header is present", () => {
|
||||
assert.equal(extractIpFromHeaders({}), "unknown");
|
||||
assert.equal(extractIpFromHeaders(null), "unknown");
|
||||
});
|
||||
@@ -1,35 +0,0 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
// Split-guard for the duckduckgo-web challenge-solver extraction.
|
||||
// The anti-abuse challenge solver + FE signals live in duckduckgo-web/challenge.ts
|
||||
// (pure of module state; the vm sandbox + 5s timeout are preserved). Host imports back.
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const EXE = join(HERE, "../../open-sse/executors");
|
||||
const HOST = join(EXE, "duckduckgo-web.ts");
|
||||
const LEAF = join(EXE, "duckduckgo-web/challenge.ts");
|
||||
|
||||
test("leaf hosts the solver and does not import the host", () => {
|
||||
const src = readFileSync(LEAF, "utf8");
|
||||
assert.match(src, /export async function solveDuckDuckGoChallenge\b/);
|
||||
assert.match(src, /export function makeDuckDuckGoFeSignals\b/);
|
||||
assert.doesNotMatch(src, /from "\.\.\/duckduckgo-web\.ts"/);
|
||||
// The vm sandbox timeout must survive the move (security invariant).
|
||||
assert.match(src, /timeout/);
|
||||
});
|
||||
|
||||
test("host imports the solver back from the leaf", () => {
|
||||
const host = readFileSync(HOST, "utf8");
|
||||
assert.match(host, /from "\.\/duckduckgo-web\/challenge\.ts"/);
|
||||
});
|
||||
|
||||
test("makeDuckDuckGoFeSignals returns a base64 string", async () => {
|
||||
const { makeDuckDuckGoFeSignals } =
|
||||
await import("../../open-sse/executors/duckduckgo-web/challenge.ts");
|
||||
const out = makeDuckDuckGoFeSignals();
|
||||
assert.equal(typeof out, "string");
|
||||
assert.ok(out.length > 0);
|
||||
});
|
||||
@@ -1,110 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import http from "node:http";
|
||||
|
||||
// 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-proxy-"));
|
||||
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 settingsDb = await import("../../src/lib/db/settings.ts");
|
||||
const { createEmbeddingResponse } = await import("../../src/lib/embeddings/service.ts");
|
||||
const { resolveProxyForRequest } = await import("../../open-sse/utils/proxyFetch.ts");
|
||||
|
||||
test.after(() => {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
async function withHttpServer(handler: http.RequestListener, fn: (baseUrl: string) => Promise<void>) {
|
||||
const server = http.createServer(handler);
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.once("error", reject);
|
||||
server.listen(0, "127.0.0.1", () => resolve());
|
||||
});
|
||||
const address = server.address();
|
||||
assert.ok(address && typeof address === "object");
|
||||
try {
|
||||
await fn(`http://127.0.0.1:${(address as { port: number }).port}`);
|
||||
} finally {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.close((err) => (err ? reject(err) : resolve()));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
test("embeddings forward the connection-level (key) pinned proxy to the upstream fetch", async () => {
|
||||
// T14 Proxy Fast-Fail performs a real TCP reachability check before running
|
||||
// the request inside the proxy context, so the "proxy" must be a real,
|
||||
// reachable local listener (its actual response body is irrelevant here —
|
||||
// the assertion is about which proxy context the upstream fetch observes).
|
||||
await withHttpServer(
|
||||
(_req, res) => {
|
||||
res.writeHead(200);
|
||||
res.end("ok");
|
||||
},
|
||||
async (proxyBaseUrl) => {
|
||||
const proxyUrl = new URL(proxyBaseUrl);
|
||||
|
||||
const connection = await providersDb.createProviderConnection({
|
||||
provider: "mistral",
|
||||
authType: "apikey",
|
||||
name: "Test Mistral Proxy",
|
||||
apiKey: "mistral-test-key",
|
||||
});
|
||||
|
||||
// Pin a proxy at the connection ("key") level — the most specific level,
|
||||
// exactly like a user would configure per-connection in the dashboard.
|
||||
await settingsDb.setProxyForLevel("key", (connection as any).id, {
|
||||
type: "http",
|
||||
host: proxyUrl.hostname,
|
||||
port: Number(proxyUrl.port),
|
||||
});
|
||||
|
||||
let capturedProxySource: string | null = null;
|
||||
let capturedProxyUrl: string | null = null;
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = async (input: RequestInfo | URL) => {
|
||||
const targetUrl = typeof input === "string" ? input : (input as URL).toString();
|
||||
const resolved = resolveProxyForRequest(targetUrl);
|
||||
capturedProxySource = resolved.source;
|
||||
capturedProxyUrl = resolved.proxyUrl;
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
data: [{ object: "embedding", embedding: [0.1, 0.2], index: 0 }],
|
||||
usage: { prompt_tokens: 3, total_tokens: 3 },
|
||||
}),
|
||||
{ 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 request should succeed");
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
|
||||
// The upstream fetch must have run inside the AsyncLocalStorage proxy
|
||||
// context carrying the connection's pinned proxy — not "direct" (no
|
||||
// context) and not the env-var proxy fallback.
|
||||
assert.equal(
|
||||
capturedProxySource,
|
||||
"context",
|
||||
`expected the embeddings upstream fetch to run inside a proxy context, got source="${capturedProxySource}"`
|
||||
);
|
||||
assert.ok(
|
||||
capturedProxyUrl && capturedProxyUrl.includes(`${proxyUrl.hostname}:${proxyUrl.port}`),
|
||||
`expected the connection's pinned proxy to be forwarded, got "${capturedProxyUrl}"`
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -80,10 +80,6 @@ test("resolveEndpointCategory: maps /v1/moderations to 'moderations'", () => {
|
||||
assert.equal(resolveEndpointCategory("/v1/moderations"), "moderations");
|
||||
});
|
||||
|
||||
test("resolveEndpointCategory: maps /v1/ocr to 'ocr'", () => {
|
||||
assert.equal(resolveEndpointCategory("/v1/ocr"), "ocr");
|
||||
});
|
||||
|
||||
test("resolveEndpointCategory: maps /v1/batches to 'batches'", () => {
|
||||
assert.equal(resolveEndpointCategory("/v1/batches"), "batches");
|
||||
});
|
||||
|
||||
@@ -587,7 +587,7 @@ test("DefaultExecutor.buildHeaders rotates extra API keys and builds Claude Code
|
||||
assert.equal(ccHeaders["x-api-key"], undefined);
|
||||
assert.equal(ccHeaders["anthropic-version"], CLAUDE_CODE_COMPATIBLE_ANTHROPIC_VERSION);
|
||||
assert.equal(ccHeaders["X-Claude-Code-Session-Id"], "session-1");
|
||||
assert.equal(ccHeaders.Accept, "text/event-stream");
|
||||
assert.equal(ccHeaders.Accept, "application/json");
|
||||
assert.equal(ccJsonHeaders.Accept, "application/json");
|
||||
});
|
||||
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const { firecrawlFetch } = await import("../../open-sse/executors/firecrawl-fetch.ts");
|
||||
|
||||
// ── #2253 (decolua/9router): self-hosted Firecrawl support ────────────────────
|
||||
// FIRECRAWL_BASE_URL lets operators point the executor at a self-hosted instance.
|
||||
// The API key stays required for the default cloud endpoint, but becomes optional
|
||||
// once a custom base URL is configured (self-hosted instances usually run with no
|
||||
// auth in front of them).
|
||||
|
||||
function withEnv(vars: Record<string, string | undefined>, fn: () => Promise<void>) {
|
||||
const original: Record<string, string | undefined> = {};
|
||||
for (const key of Object.keys(vars)) {
|
||||
original[key] = process.env[key];
|
||||
}
|
||||
for (const [key, value] of Object.entries(vars)) {
|
||||
if (value === undefined) delete process.env[key];
|
||||
else process.env[key] = value;
|
||||
}
|
||||
return fn().finally(() => {
|
||||
for (const [key, value] of Object.entries(original)) {
|
||||
if (value === undefined) delete process.env[key];
|
||||
else process.env[key] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
test("firecrawlFetch routes to FIRECRAWL_BASE_URL when set", async () => {
|
||||
await withEnv({ FIRECRAWL_BASE_URL: "http://127.0.0.1:3002" }, async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
let capturedUrl = "";
|
||||
|
||||
globalThis.fetch = async (url) => {
|
||||
capturedUrl = String(url);
|
||||
return new Response(JSON.stringify({ data: { markdown: "# Self-hosted" } }), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await firecrawlFetch({
|
||||
url: "https://example.com",
|
||||
format: "markdown",
|
||||
depth: 0,
|
||||
includeMetadata: false,
|
||||
credentials: { apiKey: "any-key" },
|
||||
});
|
||||
|
||||
assert.equal(result.success, true);
|
||||
assert.equal(capturedUrl, "http://127.0.0.1:3002/v1/scrape");
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("firecrawlFetch allows missing apiKey when FIRECRAWL_BASE_URL is custom", async () => {
|
||||
await withEnv({ FIRECRAWL_BASE_URL: "http://127.0.0.1:3002" }, async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
let capturedHeaders: Record<string, string> = {};
|
||||
|
||||
globalThis.fetch = async (_url, init = {}) => {
|
||||
capturedHeaders = (init as RequestInit).headers as Record<string, string>;
|
||||
return new Response(JSON.stringify({ data: { markdown: "# Self-hosted" } }), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await firecrawlFetch({
|
||||
url: "https://example.com",
|
||||
format: "markdown",
|
||||
depth: 0,
|
||||
includeMetadata: false,
|
||||
credentials: {},
|
||||
});
|
||||
|
||||
assert.equal(result.success, true, "custom base URL must not require an API key");
|
||||
assert.equal(
|
||||
capturedHeaders["Authorization"],
|
||||
undefined,
|
||||
"no Authorization header should be sent without an apiKey"
|
||||
);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("firecrawlFetch still requires apiKey against the default cloud base URL", async () => {
|
||||
await withEnv({ FIRECRAWL_BASE_URL: undefined }, async () => {
|
||||
const result = await firecrawlFetch({
|
||||
url: "https://example.com",
|
||||
format: "markdown",
|
||||
depth: 0,
|
||||
includeMetadata: false,
|
||||
credentials: {},
|
||||
});
|
||||
|
||||
assert.equal(result.success, false);
|
||||
assert.equal(result.status, 401);
|
||||
assert.ok(!result.error?.includes("at /"), "error must not contain stack trace");
|
||||
});
|
||||
});
|
||||
|
||||
test("firecrawlFetch honors FIRECRAWL_TIMEOUT_MS override", async () => {
|
||||
await withEnv(
|
||||
{ FIRECRAWL_BASE_URL: "http://127.0.0.1:3002", FIRECRAWL_TIMEOUT_MS: "5" },
|
||||
async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
globalThis.fetch = async (_url, init = {}) => {
|
||||
const signal = (init as RequestInit).signal;
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(
|
||||
() =>
|
||||
resolve(
|
||||
new Response(JSON.stringify({ data: { markdown: "late" } }), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
})
|
||||
),
|
||||
50
|
||||
);
|
||||
signal?.addEventListener("abort", () => {
|
||||
clearTimeout(timer);
|
||||
reject(new DOMException("Aborted", "AbortError"));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await firecrawlFetch({
|
||||
url: "https://example.com",
|
||||
format: "markdown",
|
||||
depth: 0,
|
||||
includeMetadata: false,
|
||||
credentials: {},
|
||||
});
|
||||
|
||||
assert.equal(result.success, false);
|
||||
assert.equal(result.status, 504);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -19,7 +19,7 @@ describe("KimiWebExecutor", () => {
|
||||
it("execute returns a 400 error when no JWT is provided", async () => {
|
||||
const executor = new mod.KimiWebExecutor();
|
||||
const result = await executor.execute({
|
||||
model: "k2d6",
|
||||
model: "kimi-default",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: false,
|
||||
credentials: { apiKey: "" },
|
||||
@@ -43,7 +43,7 @@ describe("KimiWebExecutor", () => {
|
||||
});
|
||||
}) as typeof fetch;
|
||||
await executor.execute({
|
||||
model: "k2d6",
|
||||
model: "kimi-default",
|
||||
body: { messages: [{ role: "user", content: "hi" }] },
|
||||
stream: false,
|
||||
credentials: { apiKey: "kimi-auth=fake.jwt.token" },
|
||||
@@ -57,28 +57,6 @@ describe("KimiWebExecutor", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveModelConfig", () => {
|
||||
const { resolveModelConfig } = mod;
|
||||
|
||||
it("maps k2d6-thinking to the K2D5 scenario with thinking enabled", () => {
|
||||
const cfg = resolveModelConfig("k2d6-thinking");
|
||||
assert.equal(cfg.scenario, "SCENARIO_K2D5");
|
||||
assert.equal(cfg.thinking, true);
|
||||
});
|
||||
|
||||
it("maps k2d6 (Instant) to the K2D5 scenario without thinking", () => {
|
||||
const cfg = resolveModelConfig("k2d6");
|
||||
assert.equal(cfg.scenario, "SCENARIO_K2D5");
|
||||
assert.equal(cfg.thinking, false);
|
||||
});
|
||||
|
||||
it("falls back to K2D5 + no thinking for an unknown model id", () => {
|
||||
const cfg = resolveModelConfig("k2d6-agent");
|
||||
assert.equal(cfg.scenario, "SCENARIO_K2D5");
|
||||
assert.equal(cfg.thinking, false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("extractKimiJwt", () => {
|
||||
const { extractKimiJwt } = mod;
|
||||
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { XaiExecutor } from "../../open-sse/executors/xai.ts";
|
||||
import { getExecutor, hasSpecializedExecutor } from "../../open-sse/executors/index.ts";
|
||||
import { xaiProvider } from "../../open-sse/config/providers/registry/xai/index.ts";
|
||||
|
||||
// Real xai catalog ids (open-sse/config/providers/registry/xai/index.ts):
|
||||
// grok-4.3 — plain, reasoning-capable
|
||||
// grok-build-0.1 — build/tool model, no reasoning mode
|
||||
// grok-4.20-multi-agent-0309 — neutral (not in either allow/deny list)
|
||||
// grok-4.20-0309-reasoning — already encodes reasoning in the id
|
||||
// grok-4.20-0309-non-reasoning — already encodes non-reasoning in the id
|
||||
|
||||
const credentials = { apiKey: "test-key" };
|
||||
|
||||
test("XaiExecutor is registered under the 'xai' key and set as the registry executor", () => {
|
||||
assert.equal(hasSpecializedExecutor("xai"), true);
|
||||
assert.ok(getExecutor("xai") instanceof XaiExecutor);
|
||||
assert.equal(xaiProvider.executor, "xai");
|
||||
});
|
||||
|
||||
test("strips a -{level} suffix from an allow-listed model and sets reasoning_effort", () => {
|
||||
const executor = new XaiExecutor();
|
||||
|
||||
for (const level of ["low", "medium", "high", "xhigh"] as const) {
|
||||
const body = { model: `grok-4.3-${level}`, messages: [] };
|
||||
const out = executor.transformRequest(`grok-4.3-${level}`, body, false, credentials) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
assert.equal(out.model, "grok-4.3", `level=${level} should strip suffix from model id`);
|
||||
assert.equal(out.reasoning_effort, level, `level=${level} should set reasoning_effort`);
|
||||
}
|
||||
});
|
||||
|
||||
test("suffix parsing also applies to the explicit -reasoning variant without double-mutating it", () => {
|
||||
const executor = new XaiExecutor();
|
||||
const body = { model: "grok-4.20-0309-reasoning-high", messages: [] };
|
||||
const out = executor.transformRequest(
|
||||
"grok-4.20-0309-reasoning-high",
|
||||
body,
|
||||
false,
|
||||
credentials
|
||||
) as Record<string, unknown>;
|
||||
|
||||
assert.equal(out.model, "grok-4.20-0309-reasoning");
|
||||
assert.equal(out.reasoning_effort, "high");
|
||||
});
|
||||
|
||||
test("strips reasoning_effort for a deny-listed model (grok-build-0.1)", () => {
|
||||
const executor = new XaiExecutor();
|
||||
const body = { model: "grok-build-0.1", reasoning_effort: "high", messages: [] };
|
||||
const out = executor.transformRequest("grok-build-0.1", body, false, credentials) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
|
||||
assert.equal(out.model, "grok-build-0.1");
|
||||
assert.equal(out.reasoning_effort, undefined);
|
||||
});
|
||||
|
||||
test("strips reasoning_effort for the explicit -non-reasoning variant (already encodes reasoning state)", () => {
|
||||
const executor = new XaiExecutor();
|
||||
const body = {
|
||||
model: "grok-4.20-0309-non-reasoning",
|
||||
reasoning_effort: "high",
|
||||
messages: [],
|
||||
};
|
||||
const out = executor.transformRequest(
|
||||
"grok-4.20-0309-non-reasoning",
|
||||
body,
|
||||
false,
|
||||
credentials
|
||||
) as Record<string, unknown>;
|
||||
|
||||
assert.equal(out.model, "grok-4.20-0309-non-reasoning");
|
||||
assert.equal(out.reasoning_effort, undefined);
|
||||
});
|
||||
|
||||
test("leaves a plain, unlisted model id and body unchanged (no suffix, not allow/deny listed)", () => {
|
||||
const executor = new XaiExecutor();
|
||||
const body = { model: "grok-4.20-multi-agent-0309", messages: [{ role: "user", content: "hi" }] };
|
||||
const out = executor.transformRequest(
|
||||
"grok-4.20-multi-agent-0309",
|
||||
body,
|
||||
false,
|
||||
credentials
|
||||
) as Record<string, unknown>;
|
||||
|
||||
assert.equal(out.model, "grok-4.20-multi-agent-0309");
|
||||
assert.equal(out.reasoning_effort, undefined);
|
||||
assert.deepEqual(out.messages, body.messages);
|
||||
});
|
||||
@@ -29,14 +29,13 @@ test.after(() => {
|
||||
|
||||
// ── Registry ─────────────────────────────────────────────────────────────────
|
||||
|
||||
test("getAllProviders returns exactly 4 providers", () => {
|
||||
test("getAllProviders returns exactly 3 providers", () => {
|
||||
const providers = getAllProviders();
|
||||
assert.equal(providers.length, 4);
|
||||
assert.equal(providers.length, 3);
|
||||
const ids = providers.map((p) => p.id);
|
||||
assert.ok(ids.includes("1proxy"));
|
||||
assert.ok(ids.includes("proxifly"));
|
||||
assert.ok(ids.includes("iplocate"));
|
||||
assert.ok(ids.includes("webshare"));
|
||||
});
|
||||
|
||||
test("getProvider returns the correct provider by id", () => {
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { parseSSELine, stripAnsiCodes } from "../../open-sse/utils/streamHelpers.ts";
|
||||
|
||||
test("parseSSELine resolves an ANSI/VT100-prefixed data: frame (gemini-cli redraw)", () => {
|
||||
// gemini-cli prefixes SSE frames with cursor-redraw escapes (\x1b[2K clears the
|
||||
// line, \x1b[1A moves the cursor up). 0x1b is not whitespace, so before the fix
|
||||
// trimStart().startsWith("data:") failed and the frame was silently dropped (#2273).
|
||||
const line = `\x1b[2K\x1b[1Adata: ${JSON.stringify({
|
||||
choices: [{ delta: { content: "hi" } }],
|
||||
})}`;
|
||||
const r = parseSSELine(line);
|
||||
assert.ok(r, "expected a parsed payload, got null (frame was dropped)");
|
||||
assert.equal(r?.choices?.[0]?.delta?.content, "hi");
|
||||
});
|
||||
|
||||
test("parseSSELine returns null for a pure-ANSI line (nothing after stripping)", () => {
|
||||
assert.equal(parseSSELine("\x1b[2K\x1b[1A"), null);
|
||||
});
|
||||
|
||||
test("stripAnsiCodes strips CSI/SGR/OSC/C0 but preserves \\t \\n \\r", () => {
|
||||
// CSI cursor moves + SGR color codes
|
||||
assert.equal(stripAnsiCodes("\x1b[2K\x1b[1Ahello"), "hello");
|
||||
assert.equal(stripAnsiCodes("\x1b[31mred\x1b[0m"), "red");
|
||||
// OSC sequence terminated by BEL (\x07)
|
||||
assert.equal(stripAnsiCodes("\x1b]0;title\x07text"), "text");
|
||||
// OSC sequence terminated by ST (\x1b\\)
|
||||
assert.equal(stripAnsiCodes("\x1b]8;;https://x\x1b\\link"), "link");
|
||||
// stray C0 control byte
|
||||
assert.equal(stripAnsiCodes("a\x00b"), "ab");
|
||||
// whitespace preserved
|
||||
assert.equal(stripAnsiCodes("a\tb\nc\rd"), "a\tb\nc\rd");
|
||||
});
|
||||
|
||||
test("stripAnsiCodes passes null/undefined through unchanged", () => {
|
||||
assert.equal(stripAnsiCodes(null), null);
|
||||
assert.equal(stripAnsiCodes(undefined), undefined);
|
||||
});
|
||||
|
||||
test("stripAnsiCodes runs in linear time on adversarial input (ReDoS guard)", () => {
|
||||
const hostile = "\x1b[" + "0;".repeat(50000) + "m";
|
||||
const start = Date.now();
|
||||
stripAnsiCodes(hostile);
|
||||
assert.ok(Date.now() - start < 1000, "stripAnsiCodes should not backtrack catastrophically");
|
||||
});
|
||||
@@ -1,41 +0,0 @@
|
||||
/**
|
||||
* #2309 — antigravity/gemini returned [400] "Invalid JSON payload received.
|
||||
* Unknown name \"multipleOf\" at 'request.tools[0].function_declarations[...]"
|
||||
*
|
||||
* Root cause: `multipleOf` (a JSON Schema numeric constraint) was NOT listed in
|
||||
* `GEMINI_UNSUPPORTED_SCHEMA_KEYS`, so `cleanJSONSchemaForAntigravity` left it in
|
||||
* the function-declaration parameters. The Gemini/antigravity upstream (OpenAPI
|
||||
* 3.0 schema subset) rejects `multipleOf` with a hard 400.
|
||||
*
|
||||
* Fix: add `multipleOf` to the unsupported-keys set so it is stripped at every
|
||||
* level (top-level property, nested object, and inside array `items`). Sibling
|
||||
* numeric constraints `minimum`/`maximum` ARE accepted by Gemini and must stay.
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
cleanJSONSchemaForAntigravity,
|
||||
GEMINI_UNSUPPORTED_SCHEMA_KEYS,
|
||||
} from "../../open-sse/translator/helpers/geminiHelper.ts";
|
||||
|
||||
test("#2309 multipleOf is stripped at all levels for antigravity/gemini schemas", () => {
|
||||
const schema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
count: { type: "integer", multipleOf: 2, minimum: 0 },
|
||||
ratio: { type: "number", multipleOf: 0.5 },
|
||||
tags: { type: "array", items: { type: "number", multipleOf: 10 } },
|
||||
},
|
||||
};
|
||||
|
||||
const cleaned = JSON.stringify(cleanJSONSchemaForAntigravity(schema));
|
||||
|
||||
assert.ok(!cleaned.includes("multipleOf"), "multipleOf must be removed");
|
||||
// Gemini DOES support minimum/maximum — those must survive.
|
||||
assert.ok(cleaned.includes("minimum"), "minimum must be preserved");
|
||||
});
|
||||
|
||||
test("#2309 multipleOf is in GEMINI_UNSUPPORTED_SCHEMA_KEYS", () => {
|
||||
assert.ok(GEMINI_UNSUPPORTED_SCHEMA_KEYS.has("multipleOf"));
|
||||
});
|
||||
@@ -1,110 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const mod = await import("../../scripts/release/gen-contributors.mjs");
|
||||
const {
|
||||
extractVersionSection,
|
||||
parseContributors,
|
||||
renderContributors,
|
||||
injectContributors,
|
||||
NOISE_HANDLES,
|
||||
} = mod;
|
||||
|
||||
const FIXTURE = `# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
---
|
||||
|
||||
## [3.9.0] — 2026-08-01
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(a):** thing one. ([#100](https://github.com/x/y/pull/100) — thanks @alice)
|
||||
- **feat(b):** uses \`@toon-format/toon\` and \`@dnd-kit\`. ([#101](https://github.com/x/y/pull/101) — thanks @bob)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(c):** direct commit fix. (thanks @carol)
|
||||
- **fix(d):** extracted. Extracted from [#102](https://github.com/x/y/pull/102) by [@dave](https://github.com/dave).
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **refactor(rollup):** god-file split ([#200](https://github.com/x/y/pull/200), [#201](https://github.com/x/y/pull/201) — thanks @erin); editorconfig ([#202](https://github.com/x/y/pull/202) — thanks @frank). — thanks @diegosouzapw
|
||||
|
||||
---
|
||||
|
||||
## [3.8.99] — 2026-07-31
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(z):** other version, must not leak. ([#999](https://github.com/x/y/pull/999) — thanks @zoe)
|
||||
|
||||
---
|
||||
`;
|
||||
|
||||
test("extractVersionSection returns only the target version body (not the next section)", () => {
|
||||
const sec = extractVersionSection(FIXTURE, "3.9.0");
|
||||
assert.ok(sec.includes("thing one"), "includes 3.9.0 content");
|
||||
assert.ok(!sec.includes("must not leak"), "excludes 3.8.99 content");
|
||||
assert.ok(!sec.includes("#999"), "does not bleed into next version");
|
||||
});
|
||||
|
||||
test("parseContributors credits per parenthetical group, not a flat scan", () => {
|
||||
const agg = parseContributors(extractVersionSection(FIXTURE, "3.9.0"));
|
||||
// rollup: erin gets 200+201, frank gets 202 — NOT both getting all three
|
||||
assert.deepEqual(
|
||||
[...agg.get("erin")].sort((a, b) => a - b),
|
||||
[200, 201]
|
||||
);
|
||||
assert.deepEqual([...agg.get("frank")], [202]);
|
||||
// simple bullets
|
||||
assert.deepEqual([...agg.get("alice")], [100]);
|
||||
// direct-commit credit with no PR ref
|
||||
assert.ok(agg.has("carol") && agg.get("carol").size === 0);
|
||||
// "Extracted from #N by @X"
|
||||
assert.deepEqual([...agg.get("dave")], [102]);
|
||||
});
|
||||
|
||||
test("noise handles and the maintainer are excluded from the contributor map", () => {
|
||||
const agg = parseContributors(extractVersionSection(FIXTURE, "3.9.0"));
|
||||
assert.ok(!agg.has("toon-format"), "package scope is not a contributor");
|
||||
assert.ok(!agg.has("dnd-kit"), "package scope is not a contributor");
|
||||
assert.ok(!agg.has("diegosouzapw"), "maintainer is rendered separately, not in the map");
|
||||
assert.ok(NOISE_HANDLES.has("toon-format"));
|
||||
});
|
||||
|
||||
test("renderContributors emits an alphabetical table with maintainer last", () => {
|
||||
const agg = parseContributors(extractVersionSection(FIXTURE, "3.9.0"));
|
||||
const table = renderContributors("3.9.0", agg);
|
||||
assert.ok(table.startsWith("### 🙌 Contributors"));
|
||||
const rows = table.split("\n").filter((l) => l.startsWith("| [@"));
|
||||
const handles = rows.map((r) => r.match(/@([A-Za-z0-9_-]+)/)[1]);
|
||||
assert.equal(handles[handles.length - 1], "diegosouzapw", "maintainer is last");
|
||||
const external = handles.slice(0, -1);
|
||||
assert.deepEqual(
|
||||
external,
|
||||
[...external].sort((a, b) => a.localeCompare(b)),
|
||||
"external sorted"
|
||||
);
|
||||
assert.ok(table.includes("| [@carol](https://github.com/carol) | direct commit / report |"));
|
||||
});
|
||||
|
||||
test("injectContributors inserts before the closing --- and is idempotent", () => {
|
||||
const once = injectContributors(
|
||||
FIXTURE,
|
||||
"3.9.0",
|
||||
renderContributors("3.9.0", parseContributors(extractVersionSection(FIXTURE, "3.9.0")))
|
||||
);
|
||||
assert.ok(once.includes("### 🙌 Contributors"), "section injected");
|
||||
// 3.8.99 untouched
|
||||
assert.ok(once.includes("must not leak"));
|
||||
// idempotent: injecting again does not duplicate
|
||||
const twice = injectContributors(
|
||||
once,
|
||||
"3.9.0",
|
||||
renderContributors("3.9.0", parseContributors(extractVersionSection(once, "3.9.0")))
|
||||
);
|
||||
const count = (twice.match(/### 🙌 Contributors/g) || []).length;
|
||||
assert.equal(count, 1, "no duplicate Contributors section on re-run");
|
||||
});
|
||||
@@ -1,145 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { GitlabExecutor } from "../../open-sse/executors/gitlab.ts";
|
||||
|
||||
function jsonResponse(body: unknown, status = 200) {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
const WEATHER_TOOL = {
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_weather",
|
||||
description: "Get the current weather for a city",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: { city: { type: "string" } },
|
||||
required: ["city"],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
test("GitlabExecutor emulates OpenAI tool_calls when body.tools is present (#6051)", async () => {
|
||||
const executor = new GitlabExecutor();
|
||||
const originalFetch = globalThis.fetch;
|
||||
const calls: Array<{ url: string; body: Record<string, unknown> }> = [];
|
||||
|
||||
// Upstream (GitLab code_suggestions) replies with the tool invocation as raw
|
||||
// text, exactly how a web/completion model would when handed the tool contract.
|
||||
globalThis.fetch = async (url, init = {}) => {
|
||||
calls.push({ url: String(url), body: JSON.parse(String(init.body || "{}")) });
|
||||
return jsonResponse({
|
||||
model: { name: "code-gecko" },
|
||||
choices: [
|
||||
{
|
||||
text: 'Sure, let me check.\n<tool>{"name": "get_weather", "arguments": {"city": "Paris"}}</tool>',
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await executor.execute({
|
||||
model: "gitlab-duo-code-suggestions",
|
||||
body: {
|
||||
messages: [{ role: "user", content: "What's the weather in Paris?" }],
|
||||
tools: [WEATHER_TOOL],
|
||||
tool_choice: "auto",
|
||||
},
|
||||
stream: false,
|
||||
credentials: { apiKey: "glpat-test" },
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
log: null,
|
||||
} as any);
|
||||
|
||||
// The serialized tool contract must reach the GitLab prompt.
|
||||
assert.equal(calls.length, 1);
|
||||
assert.match(
|
||||
String((calls[0].body.current_file as any)?.content_above_cursor ?? ""),
|
||||
/get_weather/,
|
||||
"tool contract must be serialized into the GitLab prompt"
|
||||
);
|
||||
|
||||
const body = (await result.response.json()) as any;
|
||||
const choice = body.choices[0];
|
||||
assert.equal(choice.finish_reason, "tool_calls");
|
||||
assert.ok(Array.isArray(choice.message.tool_calls), "tool_calls array must be present");
|
||||
assert.equal(choice.message.tool_calls.length, 1);
|
||||
assert.equal(choice.message.tool_calls[0].type, "function");
|
||||
assert.equal(choice.message.tool_calls[0].function.name, "get_weather");
|
||||
assert.deepEqual(JSON.parse(choice.message.tool_calls[0].function.arguments), {
|
||||
city: "Paris",
|
||||
});
|
||||
assert.equal(choice.message.content, null);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("GitlabExecutor streams a tool_calls chunk when tools are present (#6051)", async () => {
|
||||
const executor = new GitlabExecutor();
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
globalThis.fetch = async () =>
|
||||
jsonResponse({
|
||||
model: { name: "code-gecko" },
|
||||
choices: [{ text: '<tool>{"name": "get_weather", "arguments": {"city": "Paris"}}</tool>' }],
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await executor.execute({
|
||||
model: "gitlab-duo-code-suggestions",
|
||||
body: {
|
||||
messages: [{ role: "user", content: "weather in Paris?" }],
|
||||
tools: [WEATHER_TOOL],
|
||||
},
|
||||
stream: true,
|
||||
credentials: { apiKey: "glpat-test" },
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
log: null,
|
||||
} as any);
|
||||
|
||||
assert.equal(result.response.headers.get("Content-Type"), "text/event-stream");
|
||||
const text = await result.response.text();
|
||||
assert.match(text, /"tool_calls"/, "SSE stream must carry a tool_calls delta");
|
||||
assert.match(text, /get_weather/);
|
||||
assert.match(text, /"finish_reason":"tool_calls"/);
|
||||
assert.match(text, /data: \[DONE\]/);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("GitlabExecutor keeps plain-text finish_reason:stop for non-tool requests (#6051)", async () => {
|
||||
const executor = new GitlabExecutor();
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
globalThis.fetch = async () =>
|
||||
jsonResponse({
|
||||
model: { name: "code-gecko" },
|
||||
choices: [{ text: "def hello():\n return 'world'" }],
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await executor.execute({
|
||||
model: "gitlab-duo-code-suggestions",
|
||||
body: { messages: [{ role: "user", content: "Write a hello world function" }] },
|
||||
stream: false,
|
||||
credentials: { apiKey: "glpat-test" },
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
log: null,
|
||||
} as any);
|
||||
|
||||
const body = (await result.response.json()) as any;
|
||||
const choice = body.choices[0];
|
||||
assert.equal(choice.finish_reason, "stop");
|
||||
assert.equal(choice.message.tool_calls, undefined);
|
||||
assert.match(choice.message.content, /hello/);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
@@ -1,52 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { getModelInfoCore } from "../../open-sse/services/model.ts";
|
||||
|
||||
// Bug #5852: resolveModelByProviderInference() in open-sse/services/model.ts had an
|
||||
// unconditional `/^gpt-/i` heuristic that fired for ANY model id starting with
|
||||
// "gpt-", hijacking open-weight models cataloged under other providers (e.g.
|
||||
// "gpt-oss-120b", served by fireworks/cerebras/scaleway/byteplus) into
|
||||
// provider "openai", which does not carry them — producing a 404 with no
|
||||
// fallback for bare (non-combo) requests.
|
||||
//
|
||||
// Fix: the fallback must only apply when there are ZERO known catalog
|
||||
// candidates for the model id (providers.length === 0).
|
||||
|
||||
const KNOWN_GPT_OSS_120B_PROVIDERS = new Set(["fireworks", "cerebras", "scaleway", "byteplus"]);
|
||||
|
||||
test("gpt-oss-120b resolves into its cataloged open-weight providers, not openai (#5852)", async () => {
|
||||
const info = await getModelInfoCore("gpt-oss-120b", null);
|
||||
|
||||
assert.notEqual(
|
||||
info.provider,
|
||||
"openai",
|
||||
"gpt-oss-120b must not be hijacked into the openai-family fallback"
|
||||
);
|
||||
|
||||
// Multiple providers catalog this open-weight model id, so the resolver correctly
|
||||
// reports it as ambiguous (asking the caller to disambiguate with a provider/model
|
||||
// prefix) instead of silently defaulting to openai (which doesn't carry it → 404).
|
||||
assert.equal(info.errorType, "ambiguous_model");
|
||||
assert.ok(Array.isArray(info.candidateProviders) && info.candidateProviders.length > 0);
|
||||
assert.ok(
|
||||
info.candidateProviders.some((p: string) => KNOWN_GPT_OSS_120B_PROVIDERS.has(p)),
|
||||
`expected at least one candidate from ${[...KNOWN_GPT_OSS_120B_PROVIDERS].join(
|
||||
", "
|
||||
)}, got ${JSON.stringify(info.candidateProviders)}`
|
||||
);
|
||||
assert.ok(
|
||||
!info.candidateProviders.includes("openai"),
|
||||
"openai must not be listed as a candidate for gpt-oss-120b"
|
||||
);
|
||||
});
|
||||
|
||||
test("regression guard: a genuinely-uncataloged openai-family id still falls back to openai (#5852)", async () => {
|
||||
const info = await getModelInfoCore("gpt-5.9-imaginary-unreleased", null);
|
||||
|
||||
assert.equal(
|
||||
info.provider,
|
||||
"openai",
|
||||
"uncataloged gpt-* ids with zero known candidates must still fall back to openai"
|
||||
);
|
||||
});
|
||||
@@ -1,36 +0,0 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
// Split-guard for the grok-web executor extraction.
|
||||
// Pure clusters live in 4 leaves: types.ts (stream types), tool-bridge.ts (OpenAI<->Grok
|
||||
// tool translation), native-tools.ts (native-tool selection + mapping), text-cleanup.ts
|
||||
// (markup cleanup). All are module-private (no host re-export). No leaf imports the host.
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const DIR = join(HERE, "../../open-sse/executors/grok-web");
|
||||
const HOST = join(HERE, "../../open-sse/executors/grok-web.ts");
|
||||
|
||||
test("leaves are acyclic: none imports the host, layered types<-tool-bridge<-native-tools", () => {
|
||||
for (const f of ["types", "tool-bridge", "native-tools", "text-cleanup"]) {
|
||||
const src = readFileSync(join(DIR, `${f}.ts`), "utf8");
|
||||
assert.doesNotMatch(src, /from "\.\.\/grok-web\.ts"/, `${f} must not import the host`);
|
||||
}
|
||||
assert.doesNotMatch(readFileSync(join(DIR, "types.ts"), "utf8"), /^import /m);
|
||||
assert.match(readFileSync(join(DIR, "native-tools.ts"), "utf8"), /from "\.\/tool-bridge\.ts"/);
|
||||
});
|
||||
|
||||
test("host imports the tool-bridge + native-tools + text-cleanup helpers", () => {
|
||||
const host = readFileSync(HOST, "utf8");
|
||||
assert.match(host, /from "\.\/grok-web\/tool-bridge\.ts"/);
|
||||
assert.match(host, /from "\.\/grok-web\/native-tools\.ts"/);
|
||||
assert.match(host, /from "\.\/grok-web\/text-cleanup\.ts"/);
|
||||
});
|
||||
|
||||
test("text-cleanup strips Grok markup", async () => {
|
||||
const { cleanGrokContentText } =
|
||||
await import("../../open-sse/executors/grok-web/text-cleanup.ts");
|
||||
assert.equal(typeof cleanGrokContentText, "function");
|
||||
assert.equal(typeof cleanGrokContentText("plain text"), "string");
|
||||
});
|
||||
@@ -1,103 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
resolveHfPipelineTag,
|
||||
sortHfSuggestedModels,
|
||||
type HfModelSummary,
|
||||
} from "../../open-sse/services/hfModelSuggestions.ts";
|
||||
|
||||
test("resolveHfPipelineTag: maps the 'image' kind to HF's text-to-image pipeline_tag", () => {
|
||||
assert.equal(resolveHfPipelineTag("image"), "text-to-image");
|
||||
});
|
||||
|
||||
test("resolveHfPipelineTag: returns null for an unmapped kind", () => {
|
||||
assert.equal(resolveHfPipelineTag("video"), null);
|
||||
assert.equal(resolveHfPipelineTag("does-not-exist"), null);
|
||||
});
|
||||
|
||||
test("sortHfSuggestedModels: sorts descending by downloads (default)", () => {
|
||||
const models: HfModelSummary[] = [
|
||||
{ id: "a/low", downloads: 10, likes: 500 },
|
||||
{ id: "b/high", downloads: 1000, likes: 1 },
|
||||
{ id: "c/mid", downloads: 100, likes: 50 },
|
||||
];
|
||||
|
||||
const result = sortHfSuggestedModels(models);
|
||||
assert.deepEqual(
|
||||
result.map((m) => m.id),
|
||||
["b/high", "c/mid", "a/low"]
|
||||
);
|
||||
});
|
||||
|
||||
test("sortHfSuggestedModels: sorts descending by likes when requested", () => {
|
||||
const models: HfModelSummary[] = [
|
||||
{ id: "a/low", downloads: 10, likes: 500 },
|
||||
{ id: "b/high", downloads: 1000, likes: 1 },
|
||||
{ id: "c/mid", downloads: 100, likes: 50 },
|
||||
];
|
||||
|
||||
const result = sortHfSuggestedModels(models, "likes");
|
||||
assert.deepEqual(
|
||||
result.map((m) => m.id),
|
||||
["a/low", "c/mid", "b/high"]
|
||||
);
|
||||
});
|
||||
|
||||
test("sortHfSuggestedModels: caps results at the requested limit", () => {
|
||||
const models: HfModelSummary[] = Array.from({ length: 30 }, (_, i) => ({
|
||||
id: `model/${i}`,
|
||||
downloads: i,
|
||||
}));
|
||||
|
||||
const result = sortHfSuggestedModels(models, "downloads", 5);
|
||||
assert.equal(result.length, 5);
|
||||
// Highest downloads (29..25) come first
|
||||
assert.deepEqual(
|
||||
result.map((m) => m.id),
|
||||
["model/29", "model/28", "model/27", "model/26", "model/25"]
|
||||
);
|
||||
});
|
||||
|
||||
test("sortHfSuggestedModels: drops entries without a usable string id", () => {
|
||||
const models = [
|
||||
{ id: "", downloads: 999 },
|
||||
{ id: " ", downloads: 998 },
|
||||
{ downloads: 997 },
|
||||
{ id: "valid/model", downloads: 1 },
|
||||
] as HfModelSummary[];
|
||||
|
||||
const result = sortHfSuggestedModels(models);
|
||||
assert.deepEqual(
|
||||
result.map((m) => m.id),
|
||||
["valid/model"]
|
||||
);
|
||||
});
|
||||
|
||||
test("sortHfSuggestedModels: treats missing/non-numeric metric values as 0 (no throw)", () => {
|
||||
const models = [
|
||||
{ id: "a/no-metric" },
|
||||
{ id: "b/has-metric", downloads: 5 },
|
||||
{ id: "c/nan-metric", downloads: Number.NaN },
|
||||
] as HfModelSummary[];
|
||||
|
||||
const result = sortHfSuggestedModels(models, "downloads");
|
||||
assert.deepEqual(
|
||||
result.map((m) => m.id),
|
||||
["b/has-metric", "a/no-metric", "c/nan-metric"]
|
||||
);
|
||||
});
|
||||
|
||||
test("sortHfSuggestedModels: handles an empty input array", () => {
|
||||
assert.deepEqual(sortHfSuggestedModels([]), []);
|
||||
});
|
||||
|
||||
test("sortHfSuggestedModels: falls back to a default limit for an invalid limit value", () => {
|
||||
const models: HfModelSummary[] = Array.from({ length: 25 }, (_, i) => ({
|
||||
id: `model/${i}`,
|
||||
downloads: i,
|
||||
}));
|
||||
|
||||
const result = sortHfSuggestedModels(models, "downloads", 0);
|
||||
assert.equal(result.length, 20);
|
||||
});
|
||||
@@ -1,32 +0,0 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
// Split-guard for the huggingchat executor JSONL-stream extraction.
|
||||
// The pure JSONL->OpenAI-SSE translation lives in huggingchat/jsonlStream.ts
|
||||
// (consumes a passed-in ReadableStream; no host state/fetch/auth). Host imports it back.
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const EXE = join(HERE, "../../open-sse/executors");
|
||||
const HOST = join(EXE, "huggingchat.ts");
|
||||
const LEAF = join(EXE, "huggingchat/jsonlStream.ts");
|
||||
|
||||
test("leaf hosts the jsonl translators and does not import the host", () => {
|
||||
const src = readFileSync(LEAF, "utf8");
|
||||
assert.match(src, /export async function\* streamJsonlToOpenAi\b/);
|
||||
assert.match(src, /export async function readJsonlResponse\b/);
|
||||
assert.match(src, /export function (sseChunk|parseJsonlLine)\b/);
|
||||
assert.doesNotMatch(src, /from "\.\.\/huggingchat\.ts"/);
|
||||
});
|
||||
|
||||
test("host imports the jsonl translators back from the leaf", () => {
|
||||
const host = readFileSync(HOST, "utf8");
|
||||
assert.match(host, /from "\.\/huggingchat\/jsonlStream\.ts"/);
|
||||
});
|
||||
|
||||
test("parseJsonlLine parses a jsonl data line", async () => {
|
||||
const { parseJsonlLine } = await import("../../open-sse/executors/huggingchat/jsonlStream.ts");
|
||||
assert.equal(typeof parseJsonlLine, "function");
|
||||
assert.doesNotThrow(() => parseJsonlLine("not json"));
|
||||
});
|
||||
@@ -1,43 +0,0 @@
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { detectBrowserLocale } from "../../src/i18n/detectBrowserLocale";
|
||||
|
||||
const SUPPORTED_LOCALES = ["en", "pt-BR", "es", "zh-TW", "fr", "de"] as const;
|
||||
|
||||
describe("detectBrowserLocale", () => {
|
||||
it("returns the exact match when a browser language equals a supported locale", () => {
|
||||
assert.equal(detectBrowserLocale(["pt-BR"], SUPPORTED_LOCALES), "pt-BR");
|
||||
});
|
||||
|
||||
it("folds zh-HK to zh-TW when zh-TW is supported", () => {
|
||||
assert.equal(detectBrowserLocale(["zh-HK"], SUPPORTED_LOCALES), "zh-TW");
|
||||
});
|
||||
|
||||
it("folds zh-MO to zh-TW when zh-TW is supported", () => {
|
||||
assert.equal(detectBrowserLocale(["zh-MO"], SUPPORTED_LOCALES), "zh-TW");
|
||||
});
|
||||
|
||||
it("falls back to a language-prefix match when no exact match exists", () => {
|
||||
assert.equal(detectBrowserLocale(["en-US"], SUPPORTED_LOCALES), "en");
|
||||
});
|
||||
|
||||
it("returns null when nothing matches", () => {
|
||||
assert.equal(detectBrowserLocale(["ja-JP"], SUPPORTED_LOCALES), null);
|
||||
});
|
||||
|
||||
it("returns null for an empty languages list", () => {
|
||||
assert.equal(detectBrowserLocale([], SUPPORTED_LOCALES), null);
|
||||
});
|
||||
|
||||
it("returns null for an empty locales list", () => {
|
||||
assert.equal(detectBrowserLocale(["en-US"], []), null);
|
||||
});
|
||||
|
||||
it("tries each browser language in order until one matches", () => {
|
||||
assert.equal(detectBrowserLocale(["ja-JP", "fr-CA"], SUPPORTED_LOCALES), "fr");
|
||||
});
|
||||
|
||||
it("is case-insensitive", () => {
|
||||
assert.equal(detectBrowserLocale(["PT-br"], SUPPORTED_LOCALES), "pt-BR");
|
||||
});
|
||||
});
|
||||
@@ -3,10 +3,7 @@ import assert from "node:assert/strict";
|
||||
|
||||
import { kiroProvider } from "../../open-sse/config/providers/registry/kiro/index.ts";
|
||||
|
||||
const { getNextFamilyFallback } = await import("../../open-sse/services/modelFamilyFallback.ts");
|
||||
|
||||
// Regression for the port of decolua/9router#2267 ("claude-sonnet-5 is not supported"),
|
||||
// upstream PR diegosouzapw/OmniRoute#5796.
|
||||
// Regression for the port of decolua/9router#2267 ("claude-sonnet-5 is not supported").
|
||||
//
|
||||
// The Kiro provider's OAuth model catalog lives in `registry/kiro/index.ts` `models[]`.
|
||||
// That list is both the model selector's source and the fallback for the live
|
||||
@@ -31,12 +28,3 @@ test("kiro claude-sonnet-5 declares the 1M-context / 128K-output capability", ()
|
||||
assert.equal(sonnet5.contextLength, 1000000);
|
||||
assert.equal(sonnet5.maxOutputTokens, 128000);
|
||||
});
|
||||
|
||||
test("claude-sonnet-5 degrades to the Sonnet family, not Opus", () => {
|
||||
// Sonnet 5 is Sonnet-tier: its first fallback must be a cheaper Sonnet, never an Opus.
|
||||
const next = getNextFamilyFallback("kiro/claude-sonnet-5", new Set(["kiro/claude-sonnet-5"]));
|
||||
assert.ok(
|
||||
next && /claude-sonnet-4/.test(next),
|
||||
`expected claude-sonnet-5 to fall back within the Sonnet family, got: ${next}`
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
// Split-guard for the kiro executor EventStream framing extraction.
|
||||
// The pure AWS EventStream binary framing (ByteQueue, CRC32, parseEventFrame) lives in
|
||||
// kiro/eventstream.ts (self-contained, no host imports). Host imports back what it uses.
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const EXE = join(HERE, "../../open-sse/executors");
|
||||
const HOST = join(EXE, "kiro.ts");
|
||||
const LEAF = join(EXE, "kiro/eventstream.ts");
|
||||
|
||||
test("leaf hosts the framing primitives and does not import the host", () => {
|
||||
const src = readFileSync(LEAF, "utf8");
|
||||
assert.match(src, /export class ByteQueue\b/);
|
||||
assert.match(src, /export function crc32\b/);
|
||||
assert.match(src, /export function parseEventFrame\b/);
|
||||
assert.doesNotMatch(src, /from "\.\.\/kiro\.ts"/);
|
||||
});
|
||||
|
||||
test("host imports the framing primitives back from the leaf", () => {
|
||||
const host = readFileSync(HOST, "utf8");
|
||||
assert.match(host, /from "\.\/kiro\/eventstream\.ts"/);
|
||||
});
|
||||
|
||||
test("crc32 is deterministic and ByteQueue buffers bytes", async () => {
|
||||
const { crc32, ByteQueue } = await import("../../open-sse/executors/kiro/eventstream.ts");
|
||||
const a = crc32(new Uint8Array([1, 2, 3]));
|
||||
const b = crc32(new Uint8Array([1, 2, 3]));
|
||||
assert.equal(a, b);
|
||||
const q = new ByteQueue();
|
||||
assert.equal(typeof q, "object");
|
||||
});
|
||||
@@ -1,35 +0,0 @@
|
||||
/**
|
||||
* #2306 — When Claude Code routes through the Kiro/CodeWhisperer backend, the
|
||||
* `system` message was normalized to `role: user` WITHOUT any wrapper, so the
|
||||
* full system prompt (env info, tool defs, memory instructions, etc.) appeared
|
||||
* as raw user text — indistinguishable from real user input, polluting context.
|
||||
*
|
||||
* Fix: wrap system-origin content in `<system-reminder>...</system-reminder>`
|
||||
* before it is merged into the Kiro user message. Real user turns stay raw.
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { buildKiroPayload } from "../../open-sse/translator/request/openai-to-kiro.ts";
|
||||
|
||||
test("#2306 system prompt is wrapped in <system-reminder> for Kiro, not raw user text", () => {
|
||||
const body = {
|
||||
messages: [
|
||||
{ role: "system", content: "You are Claude Code. ENV: cwd=/tmp. Secret: do not reveal." },
|
||||
{ role: "user", content: "hello there" },
|
||||
],
|
||||
};
|
||||
|
||||
const payload = JSON.stringify(buildKiroPayload("claude-sonnet-4-5", body, false, {}));
|
||||
|
||||
assert.ok(payload.includes("<system-reminder>"), "system content must be wrapped");
|
||||
assert.ok(payload.includes("You are Claude Code"), "system text must still be present");
|
||||
// The real user turn must NOT be wrapped.
|
||||
assert.ok(payload.includes("hello there"), "user text preserved");
|
||||
});
|
||||
|
||||
test("#2306 a plain user-only request is never wrapped in <system-reminder>", () => {
|
||||
const body = { messages: [{ role: "user", content: "just a normal question" }] };
|
||||
const payload = JSON.stringify(buildKiroPayload("claude-sonnet-4-5", body, false, {}));
|
||||
assert.ok(!payload.includes("<system-reminder>"), "no system → no wrapper");
|
||||
});
|
||||
@@ -1,63 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const mod = await import("../../scripts/release/list-uncovered-commits.mjs");
|
||||
const { refsOf, typeOf, computeUncovered, changelogRefWindow } = mod;
|
||||
|
||||
test("refsOf extracts every #N from a subject", () => {
|
||||
assert.deepEqual(refsOf("fix(x): thing (#5842) (#5901)"), [5842, 5901]);
|
||||
assert.deepEqual(refsOf("chore: no refs here"), []);
|
||||
});
|
||||
|
||||
test("typeOf reads the conventional-commit type", () => {
|
||||
assert.equal(typeOf("feat(api): x"), "feat");
|
||||
assert.equal(typeOf("fix: y"), "fix");
|
||||
assert.equal(typeOf("refactor(db)!: z"), "refactor");
|
||||
assert.equal(typeOf("Merge branch main"), "other");
|
||||
});
|
||||
|
||||
test("computeUncovered: a commit is covered iff ANY of its refs is in the changelog window", () => {
|
||||
const commits = [
|
||||
{ hash: "a1", subject: "fix(x): covered by issue ref (#100)" }, // issue 100 in changelog
|
||||
{ hash: "b2", subject: "feat(y): uncovered feature (#200)" }, // 200 not in changelog
|
||||
{ hash: "c3", subject: "refactor(z): internal (#300)" }, // rollup type, uncovered
|
||||
{ hash: "d4", subject: "chore: no ref at all" }, // no ref → uncovered, rollup
|
||||
];
|
||||
const refs = new Set([100]); // only #100 is documented
|
||||
const { covered, uncovered } = computeUncovered(commits, refs);
|
||||
assert.equal(covered, 1);
|
||||
assert.equal(uncovered.length, 3);
|
||||
const byHash = Object.fromEntries(uncovered.map((c) => [c.hash, c]));
|
||||
assert.equal(byHash.b2.rollup, false, "feat is user-facing, not a rollup candidate");
|
||||
assert.equal(byHash.c3.rollup, true, "refactor is a rollup candidate");
|
||||
assert.equal(byHash.d4.rollup, true, "chore is a rollup candidate");
|
||||
});
|
||||
|
||||
test("changelogRefWindow scans [Unreleased] + the version section but not older versions", () => {
|
||||
const cl = `# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
- **fix:** something ([#10](u))
|
||||
|
||||
---
|
||||
|
||||
## [3.9.0] — x
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(a):** landed ([#20](u))
|
||||
|
||||
---
|
||||
|
||||
## [3.8.99] — y
|
||||
|
||||
- **fix(old):** must not count ([#999](u))
|
||||
|
||||
---
|
||||
`;
|
||||
const refs = changelogRefWindow(cl, "3.9.0");
|
||||
assert.ok(refs.has(10), "picks up [Unreleased] refs");
|
||||
assert.ok(refs.has(20), "picks up the target version refs");
|
||||
assert.ok(!refs.has(999), "does NOT bleed into the previous version");
|
||||
});
|
||||
@@ -347,76 +347,3 @@ test("handleMcpStreamableHTTP keeps 400 for a missing session id (non-initialize
|
||||
// only the *present-but-unknown* case changed to 404. This must NOT regress.
|
||||
assert.equal(res.status, 400);
|
||||
});
|
||||
|
||||
test("handleMcpStreamableHTTP auto-recovers when stale session id is sent with initialize", async () => {
|
||||
mod.shutdownMcpHttp();
|
||||
|
||||
const initReq = new Request("http://localhost/api/mcp/stream", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json, text/event-stream",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
jsonrpc: "2.0",
|
||||
method: "initialize",
|
||||
id: 1,
|
||||
params: {
|
||||
protocolVersion: "2025-03-26",
|
||||
capabilities: {},
|
||||
clientInfo: { name: "test-client", version: "1.0.0" },
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const firstRes = await mod.handleMcpStreamableHTTP(initReq);
|
||||
const staleSessionId = firstRes.headers.get("mcp-session-id");
|
||||
if (!staleSessionId) {
|
||||
mod.shutdownMcpHttp();
|
||||
return;
|
||||
}
|
||||
|
||||
mod.shutdownMcpHttp();
|
||||
assert.equal(mod.isMcpHttpActive(), false);
|
||||
|
||||
const reinitReq = new Request("http://localhost/api/mcp/stream", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json, text/event-stream",
|
||||
"mcp-session-id": staleSessionId,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
jsonrpc: "2.0",
|
||||
method: "initialize",
|
||||
id: 2,
|
||||
params: {
|
||||
protocolVersion: "2025-03-26",
|
||||
capabilities: {},
|
||||
clientInfo: { name: "test-client", version: "1.0.0" },
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const recoveryRes = await mod.handleMcpStreamableHTTP(reinitReq);
|
||||
|
||||
assert.notEqual(
|
||||
recoveryRes.status,
|
||||
404,
|
||||
"Stale session + initialize must NOT return 404 — server should auto-recover"
|
||||
);
|
||||
assert.ok(
|
||||
recoveryRes.status >= 200 && recoveryRes.status < 300,
|
||||
`Expected 2xx response on auto-recovery, got ${recoveryRes.status}`
|
||||
);
|
||||
|
||||
const newSessionId = recoveryRes.headers.get("mcp-session-id");
|
||||
assert.ok(newSessionId, "Server must issue a new mcp-session-id on auto-recovery");
|
||||
assert.equal(
|
||||
mod.isMcpHttpActive(),
|
||||
true,
|
||||
"Server must have an active session after auto-recovery"
|
||||
);
|
||||
|
||||
mod.shutdownMcpHttp();
|
||||
});
|
||||
|
||||
@@ -81,19 +81,6 @@ test("media listing filter surfaces minimax where the old declared-only filter m
|
||||
assert.ok(oldListFor("tts").length < newListFor("tts").length, "fix surfaces additional tts providers");
|
||||
});
|
||||
|
||||
test("ocr is a registry-backed media kind and mistral derives it", () => {
|
||||
assert.ok(
|
||||
(REGISTRY_MEDIA_KINDS as readonly string[]).includes("ocr"),
|
||||
"REGISTRY_MEDIA_KINDS should include ocr once the OCR registry is wired"
|
||||
);
|
||||
assert.ok(
|
||||
getRegistryMediaKinds("mistral").includes("ocr" as never),
|
||||
"mistral should derive the ocr media kind from OCR_PROVIDERS"
|
||||
);
|
||||
const merged = resolveProviderServiceKinds("mistral", ["llm"]);
|
||||
assert.ok(merged.includes("ocr"), `expected ocr in ${merged.join(",")}`);
|
||||
});
|
||||
|
||||
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)) {
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { modelscopeProvider } from "../../open-sse/config/providers/registry/modelscope/index.ts";
|
||||
import { REGISTRY } from "../../open-sse/config/providerRegistry.ts";
|
||||
import { APIKEY_PROVIDERS } from "../../src/shared/constants/providers/apikey/index.ts";
|
||||
import { AI_PROVIDERS, getProviderById } from "../../src/shared/constants/providers.ts";
|
||||
|
||||
// Upstream 9router PR #1764 (@tn5052) ports ModelScope (Alibaba 魔搭) as an OpenAI-compatible
|
||||
// BYOK free-tier provider. The upstream PR hardcoded `https://api-inference.modelscope.ai/...`
|
||||
// (`.ai` TLD) — ModelScope's own docs confirm the real production domain is
|
||||
// `api-inference.modelscope.cn` (`.cn` TLD). This suite locks in the verified domain and
|
||||
// guards against a regression back to the unverified `.ai` domain.
|
||||
|
||||
test("providers-shape: modelscope is registered in APIKEY_PROVIDERS metadata", () => {
|
||||
assert.ok("modelscope" in APIKEY_PROVIDERS, "modelscope missing from APIKEY_PROVIDERS");
|
||||
const meta = (APIKEY_PROVIDERS as Record<string, Record<string, unknown>>).modelscope;
|
||||
assert.equal(meta.id, "modelscope");
|
||||
assert.equal(meta.alias, "ms");
|
||||
assert.equal(meta.name, "ModelScope");
|
||||
assert.equal(meta.website, "https://modelscope.cn");
|
||||
assert.equal(meta.hasFree, true);
|
||||
});
|
||||
|
||||
test("providers-shape: modelscope is a canonical AI_PROVIDERS entry resolvable by id", () => {
|
||||
assert.ok("modelscope" in AI_PROVIDERS, "modelscope missing from AI_PROVIDERS");
|
||||
const provider = getProviderById("modelscope");
|
||||
assert.ok(provider, "getProviderById('modelscope') returned nothing");
|
||||
assert.equal(provider?.id, "modelscope");
|
||||
});
|
||||
|
||||
test("registry resolution: modelscope resolves in the provider REGISTRY as OpenAI-compatible", () => {
|
||||
assert.ok("modelscope" in REGISTRY, "modelscope missing from REGISTRY");
|
||||
const entry = REGISTRY.modelscope;
|
||||
assert.equal(entry.format, "openai");
|
||||
assert.equal(entry.executor, "default");
|
||||
assert.equal(entry.authType, "apikey");
|
||||
assert.equal(entry.authHeader, "bearer");
|
||||
assert.equal(entry, modelscopeProvider);
|
||||
});
|
||||
|
||||
test("registry resolution: modelscope uses passthrough models with an empty static seed list", () => {
|
||||
assert.equal(modelscopeProvider.passthroughModels, true);
|
||||
assert.deepEqual(modelscopeProvider.models, []);
|
||||
assert.equal(
|
||||
modelscopeProvider.modelsUrl,
|
||||
"https://api-inference.modelscope.cn/v1/models",
|
||||
"modelsUrl must point at the verified .cn domain"
|
||||
);
|
||||
});
|
||||
|
||||
test("baseUrl guard: modelscope targets the verified api-inference.modelscope.cn domain (not .ai)", () => {
|
||||
assert.equal(
|
||||
modelscopeProvider.baseUrl,
|
||||
"https://api-inference.modelscope.cn/v1/chat/completions"
|
||||
);
|
||||
assert.ok(
|
||||
modelscopeProvider.baseUrl.includes("api-inference.modelscope.cn"),
|
||||
`baseUrl must use the verified .cn domain, got: ${modelscopeProvider.baseUrl}`
|
||||
);
|
||||
assert.ok(
|
||||
!modelscopeProvider.baseUrl.includes("modelscope.ai"),
|
||||
"baseUrl must not regress to the upstream PR's unverified .ai domain"
|
||||
);
|
||||
});
|
||||
@@ -2,9 +2,6 @@ import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const { handleModeration } = await import("../../open-sse/handlers/moderations.ts");
|
||||
const { MODERATION_PROVIDERS, getModerationProvider, parseModerationModel } = await import(
|
||||
"../../open-sse/config/moderationRegistry.ts"
|
||||
);
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
@@ -12,42 +9,6 @@ test.afterEach(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
});
|
||||
|
||||
test("MODERATION_PROVIDERS registers mistral with the Mistral moderations base URL", () => {
|
||||
const provider = getModerationProvider("mistral");
|
||||
assert.ok(provider);
|
||||
assert.equal(provider.baseUrl, "https://api.mistral.ai/v1/moderations");
|
||||
assert.ok(provider.models.some((m: { id: string }) => m.id === "mistral-moderation-latest"));
|
||||
assert.ok(MODERATION_PROVIDERS.mistral);
|
||||
});
|
||||
|
||||
test("parseModerationModel routes mistral moderation models to the mistral provider", () => {
|
||||
assert.deepEqual(parseModerationModel("mistral/mistral-moderation-latest"), {
|
||||
provider: "mistral",
|
||||
model: "mistral-moderation-latest",
|
||||
});
|
||||
assert.deepEqual(parseModerationModel("mistral-moderation-latest"), {
|
||||
provider: "mistral",
|
||||
model: "mistral-moderation-latest",
|
||||
});
|
||||
});
|
||||
|
||||
test("handleModeration proxies mistral moderation requests to the mistral endpoint", async () => {
|
||||
let captured: any;
|
||||
globalThis.fetch = async (url: any, options: any = {}) => {
|
||||
captured = { url: String(url), headers: options.headers };
|
||||
return Response.json({ id: "modr-mistral", results: [{ flagged: false }] });
|
||||
};
|
||||
|
||||
const response = await handleModeration({
|
||||
body: { model: "mistral/mistral-moderation-latest", input: "check this" },
|
||||
credentials: { apiKey: "sk-mistral" },
|
||||
});
|
||||
|
||||
assert.equal(captured.url, "https://api.mistral.ai/v1/moderations");
|
||||
assert.equal(captured.headers.Authorization, "Bearer sk-mistral");
|
||||
assert.equal(response.status, 200);
|
||||
});
|
||||
|
||||
test("handleModeration requires input", async () => {
|
||||
const response = await handleModeration({
|
||||
body: { model: "openai/omni-moderation-latest" },
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
// Split-guard for the muse-spark-web Meta AI response-parser extraction.
|
||||
// The pure SSE/JSON parsing + content/reasoning/error extraction lives in
|
||||
// muse-spark-web/response-parser.ts (no host state/fetch/auth). Host imports it back.
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const EXE = join(HERE, "../../open-sse/executors");
|
||||
const HOST = join(EXE, "muse-spark-web.ts");
|
||||
const LEAF = join(EXE, "muse-spark-web/response-parser.ts");
|
||||
|
||||
test("leaf hosts the parser and does not import the host", () => {
|
||||
const src = readFileSync(LEAF, "utf8");
|
||||
for (const sym of ["parseMetaAiResponseText", "parseMetaSseFrames", "isRecord"]) {
|
||||
assert.match(src, new RegExp(`export function ${sym}\\b`));
|
||||
}
|
||||
assert.doesNotMatch(src, /from "\.\.\/muse-spark-web\.ts"/);
|
||||
});
|
||||
|
||||
test("host imports the parser back from the leaf", () => {
|
||||
const host = readFileSync(HOST, "utf8");
|
||||
assert.match(host, /from "\.\/muse-spark-web\/response-parser\.ts"/);
|
||||
});
|
||||
|
||||
test("parseMetaAiResponseText tolerates empty input", async () => {
|
||||
const { parseMetaAiResponseText } =
|
||||
await import("../../open-sse/executors/muse-spark-web/response-parser.ts");
|
||||
const out = parseMetaAiResponseText("", false);
|
||||
assert.equal(typeof out, "object");
|
||||
});
|
||||
@@ -37,15 +37,6 @@ test("next config exposes standalone build settings and canonical rewrites", asy
|
||||
"fumadocs-ui",
|
||||
"fumadocs-core",
|
||||
]);
|
||||
// #6062: `ws` and its native masking helpers must stay external so the
|
||||
// copilot-m365-web executor keeps a working WebSocket masking path at runtime
|
||||
// (bundling ws breaks `bufferutil` → `TypeError: b.mask is not a function`).
|
||||
for (const pkg of ["ws", "bufferutil", "utf-8-validate"]) {
|
||||
assert.ok(
|
||||
nextConfig.serverExternalPackages.includes(pkg),
|
||||
`expected serverExternalPackages to externalize "${pkg}" (#6062)`
|
||||
);
|
||||
}
|
||||
assert.equal(headers[0].source, "/:path*");
|
||||
assert.match(securityHeaders["Content-Security-Policy"], /default-src 'self'/);
|
||||
assert.match(securityHeaders["Content-Security-Policy"], /frame-ancestors 'none'/);
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const { APIKEY_PROVIDERS } = await import("../../src/shared/constants/providers.ts");
|
||||
const { REGISTRY: providerRegistry } = await import("../../open-sse/config/providerRegistry.ts");
|
||||
|
||||
test("Nube.sh is registered as an API-key provider with the canonical identity", () => {
|
||||
const nube = APIKEY_PROVIDERS.nube;
|
||||
assert.ok(nube, "APIKEY_PROVIDERS.nube must be defined");
|
||||
assert.equal(nube.id, "nube");
|
||||
assert.equal(nube.alias, "nube");
|
||||
assert.equal(nube.name, "Nube.sh");
|
||||
assert.equal(nube.website, "https://nube.sh");
|
||||
assert.equal(typeof nube.textIcon, "string");
|
||||
});
|
||||
|
||||
test("Nube.sh registry entry uses OpenAI format with bearer apikey auth", () => {
|
||||
const entry = providerRegistry.nube;
|
||||
assert.ok(entry, "providerRegistry.nube must be defined");
|
||||
assert.equal(entry.id, "nube");
|
||||
assert.equal(entry.format, "openai");
|
||||
assert.equal(entry.executor, "default");
|
||||
assert.equal(entry.authType, "apikey");
|
||||
assert.equal(entry.authHeader, "bearer");
|
||||
assert.equal(entry.baseUrl, "https://ai.nube.sh/api/v1/chat/completions");
|
||||
});
|
||||
|
||||
test("Nube.sh ships no fabricated model IDs and relies on passthrough enumeration", () => {
|
||||
const entry = providerRegistry.nube;
|
||||
// The live catalog is only reachable with a valid key (401 unauthenticated), so we do
|
||||
// NOT hardcode unverifiable model IDs — models pass through and are enumerated live.
|
||||
assert.equal(entry.passthroughModels, true);
|
||||
assert.deepEqual(entry.models, []);
|
||||
assert.equal(entry.modelsUrl, "https://ai.nube.sh/api/v1/models");
|
||||
});
|
||||
@@ -1,314 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
// Ported from upstream 9router#1195: NVIDIA NIM image generation (FLUX models).
|
||||
// NVIDIA already exists as a CHAT provider (integrate.api.nvidia.com,
|
||||
// OpenAI-compatible) — image generation is a distinct host (ai.api.nvidia.com)
|
||||
// with a native NIM body shape, so it gets its own `nvidia-nim` format/handler
|
||||
// (handleNvidiaNimImageGeneration) rather than reusing the OpenAI image path.
|
||||
|
||||
import { handleImageGeneration } from "../../open-sse/handlers/imageGeneration.ts";
|
||||
import { getImageProvider } from "../../open-sse/config/imageRegistry.ts";
|
||||
import {
|
||||
buildNvidiaNimRequestBody,
|
||||
normalizeNvidiaNimImages,
|
||||
} from "../../open-sse/handlers/imageGeneration/providers/nvidiaNim.ts";
|
||||
|
||||
test("nvidia is registered as an nvidia-nim image provider with the 4 FLUX models", () => {
|
||||
const cfg = getImageProvider("nvidia");
|
||||
assert.ok(cfg, "expected an IMAGE_PROVIDERS entry for nvidia");
|
||||
assert.equal(cfg.format, "nvidia-nim");
|
||||
assert.equal(cfg.baseUrl, "https://ai.api.nvidia.com/v1/genai");
|
||||
assert.equal(cfg.authType, "apikey");
|
||||
assert.equal(cfg.authHeader, "bearer");
|
||||
|
||||
const ids = cfg.models.map((m) => m.id);
|
||||
assert.deepEqual(ids, [
|
||||
"black-forest-labs/flux.1-dev",
|
||||
"black-forest-labs/flux.1-schnell",
|
||||
"black-forest-labs/flux.1-kontext-dev",
|
||||
"black-forest-labs/flux.2-klein-4b",
|
||||
]);
|
||||
});
|
||||
|
||||
test("handleImageGeneration(nvidia/flux.1-schnell): URL construction + minimal body shaping", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
let capturedUrl;
|
||||
let capturedOptions;
|
||||
|
||||
globalThis.fetch = async (url, options) => {
|
||||
capturedUrl = String(url);
|
||||
capturedOptions = options;
|
||||
return new Response(
|
||||
JSON.stringify({ artifacts: [{ base64: "base64nvidia", finishReason: "SUCCESS" }] }),
|
||||
{ status: 200, headers: { "content-type": "application/json" } }
|
||||
);
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await handleImageGeneration({
|
||||
body: {
|
||||
model: "nvidia/black-forest-labs/flux.1-schnell",
|
||||
prompt: "A neon city",
|
||||
width: 1344,
|
||||
height: 1024,
|
||||
seed: 7,
|
||||
steps: 4,
|
||||
},
|
||||
credentials: { apiKey: "nv-token" },
|
||||
log: null,
|
||||
});
|
||||
|
||||
assert.equal(result.success, true);
|
||||
assert.equal(capturedUrl, "https://ai.api.nvidia.com/v1/genai/black-forest-labs/flux.1-schnell");
|
||||
assert.equal(capturedOptions.method, "POST");
|
||||
assert.equal(capturedOptions.headers.Authorization, "Bearer nv-token");
|
||||
assert.equal(capturedOptions.headers.Accept, "application/json");
|
||||
|
||||
const requestBody = JSON.parse(capturedOptions.body);
|
||||
assert.deepEqual(requestBody, {
|
||||
prompt: "A neon city",
|
||||
width: 1344,
|
||||
height: 1024,
|
||||
seed: 7,
|
||||
steps: 4,
|
||||
});
|
||||
|
||||
assert.equal(result.data.data[0].b64_json, "base64nvidia");
|
||||
assert.equal(result.data.data[0].finish_reason, "SUCCESS");
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("handleImageGeneration(nvidia/flux.2-klein-4b): sends edit input image as an array", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
globalThis.fetch = async () =>
|
||||
new Response(JSON.stringify({ artifacts: [{ base64: "base64nvidiaedit" }] }), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await handleImageGeneration({
|
||||
body: {
|
||||
model: "nvidia/black-forest-labs/flux.2-klein-4b",
|
||||
prompt: "Make the frog wear tiny glasses",
|
||||
image: "data:image/png;example_id,0",
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
seed: 0,
|
||||
steps: 4,
|
||||
},
|
||||
credentials: { apiKey: "nv-token" },
|
||||
log: null,
|
||||
});
|
||||
|
||||
assert.equal(result.success, true);
|
||||
assert.equal(result.data.data[0].b64_json, "base64nvidiaedit");
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("buildNvidiaNimRequestBody(flux.2-klein-4b): image sent as an array, width/height passthrough", () => {
|
||||
const req = buildNvidiaNimRequestBody("black-forest-labs/flux.2-klein-4b", {
|
||||
prompt: "Make the frog wear tiny glasses",
|
||||
image: "data:image/png;example_id,0",
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
seed: 0,
|
||||
steps: 4,
|
||||
});
|
||||
assert.deepEqual(req, {
|
||||
prompt: "Make the frog wear tiny glasses",
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
image: ["data:image/png;example_id,0"],
|
||||
seed: 0,
|
||||
steps: 4,
|
||||
});
|
||||
});
|
||||
|
||||
test("buildNvidiaNimRequestBody(flux.1-dev): omits input image in base mode", () => {
|
||||
const req = buildNvidiaNimRequestBody("black-forest-labs/flux.1-dev", {
|
||||
prompt: "A simple coffee shop interior",
|
||||
mode: "base",
|
||||
image: "data:image/png;example_id,0",
|
||||
cfg_scale: 1.1,
|
||||
width: 768,
|
||||
height: 1344,
|
||||
seed: 0,
|
||||
steps: 50,
|
||||
});
|
||||
assert.deepEqual(req, {
|
||||
prompt: "A simple coffee shop interior",
|
||||
mode: "base",
|
||||
width: 768,
|
||||
height: 1344,
|
||||
cfg_scale: 1.1,
|
||||
seed: 0,
|
||||
steps: 50,
|
||||
});
|
||||
});
|
||||
|
||||
test("buildNvidiaNimRequestBody(flux.1-dev): drops out-of-range dimensions and non-positive cfg_scale", () => {
|
||||
const req = buildNvidiaNimRequestBody("black-forest-labs/flux.1-dev", {
|
||||
prompt: "A simple coffee shop interior",
|
||||
mode: "base",
|
||||
image: "data:image/png;example_id,0",
|
||||
cfg_scale: 0,
|
||||
width: 1792, // out of the 768-1344 range
|
||||
height: 1024,
|
||||
seed: 0,
|
||||
steps: 50,
|
||||
});
|
||||
assert.deepEqual(req, {
|
||||
prompt: "A simple coffee shop interior",
|
||||
mode: "base",
|
||||
seed: 0,
|
||||
steps: 50,
|
||||
});
|
||||
assert.ok(!("width" in req) && !("height" in req) && !("cfg_scale" in req));
|
||||
});
|
||||
|
||||
test("buildNvidiaNimRequestBody(flux.1-dev): sends control image as a string for non-base modes", () => {
|
||||
const req = buildNvidiaNimRequestBody("black-forest-labs/flux.1-dev", {
|
||||
prompt: "A simple coffee shop interior",
|
||||
mode: "depth",
|
||||
image: "data:image/png;example_id,0",
|
||||
cfg_scale: 3.5,
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
seed: 0,
|
||||
steps: 50,
|
||||
});
|
||||
assert.deepEqual(req, {
|
||||
prompt: "A simple coffee shop interior",
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
mode: "depth",
|
||||
image: "data:image/png;example_id,0",
|
||||
cfg_scale: 3.5,
|
||||
seed: 0,
|
||||
steps: 50,
|
||||
});
|
||||
});
|
||||
|
||||
test("buildNvidiaNimRequestBody(flux.1-kontext-dev): uses aspect_ratio instead of width/height", () => {
|
||||
const req = buildNvidiaNimRequestBody("black-forest-labs/flux.1-kontext-dev", {
|
||||
prompt: "Now the mouse is holding pizza instead",
|
||||
image: "data:image/png;example_id,0",
|
||||
aspect_ratio: "match_input_image",
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
steps: 30,
|
||||
cfg_scale: 3.5,
|
||||
seed: 0,
|
||||
});
|
||||
assert.deepEqual(req, {
|
||||
prompt: "Now the mouse is holding pizza instead",
|
||||
image: "data:image/png;example_id,0",
|
||||
aspect_ratio: "match_input_image",
|
||||
cfg_scale: 3.5,
|
||||
seed: 0,
|
||||
steps: 30,
|
||||
});
|
||||
});
|
||||
|
||||
test("handleImageGeneration(nvidia/flux.1-kontext-dev): requires an input image, does not fetch", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
let fetchCalled = false;
|
||||
globalThis.fetch = async () => {
|
||||
fetchCalled = true;
|
||||
throw new Error("fetch should not be called without an input image");
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await handleImageGeneration({
|
||||
body: {
|
||||
model: "nvidia/black-forest-labs/flux.1-kontext-dev",
|
||||
prompt: "Now the mouse is holding pizza instead",
|
||||
},
|
||||
credentials: { apiKey: "nv-token" },
|
||||
log: null,
|
||||
});
|
||||
|
||||
assert.equal(result.success, false);
|
||||
assert.equal(result.status, 400);
|
||||
assert.match(result.error, /requires an input image/i);
|
||||
assert.equal(fetchCalled, false);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("normalizeNvidiaNimImages: accepts artifacts[], images[], data[], and single-value shapes", () => {
|
||||
assert.deepEqual(normalizeNvidiaNimImages({ artifacts: [{ base64: "a1" }] }).data, [
|
||||
{ b64_json: "a1" },
|
||||
]);
|
||||
assert.deepEqual(normalizeNvidiaNimImages({ images: ["b1", "b2"] }).data, [
|
||||
{ b64_json: "b1" },
|
||||
{ b64_json: "b2" },
|
||||
]);
|
||||
assert.deepEqual(normalizeNvidiaNimImages({ data: [{ b64_json: "c1" }] }).data, [
|
||||
{ b64_json: "c1" },
|
||||
]);
|
||||
assert.deepEqual(normalizeNvidiaNimImages({ image: "d1" }).data, [{ b64_json: "d1" }]);
|
||||
assert.deepEqual(normalizeNvidiaNimImages({ result: { image: "e1" } }).data, [
|
||||
{ b64_json: "e1" },
|
||||
]);
|
||||
// Already-OpenAI-shaped responses pass through untouched.
|
||||
const passthrough = { created: 123, data: [{ b64_json: "f1" }] };
|
||||
assert.deepEqual(normalizeNvidiaNimImages(passthrough), passthrough);
|
||||
// Unrecognized shape -> empty data, never throws.
|
||||
assert.deepEqual(normalizeNvidiaNimImages({ nonsense: true }).data, []);
|
||||
});
|
||||
|
||||
test("handleImageGeneration(nvidia): upstream error body never leaks a stack trace", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = async () => {
|
||||
throw new Error(`boom at /home/user/secret/path/imageGeneration.ts:123:45`);
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await handleImageGeneration({
|
||||
body: {
|
||||
model: "nvidia/black-forest-labs/flux.1-schnell",
|
||||
prompt: "test",
|
||||
},
|
||||
credentials: { apiKey: "nv-token" },
|
||||
log: null,
|
||||
});
|
||||
|
||||
assert.equal(result.success, false);
|
||||
assert.equal(result.status, 502);
|
||||
assert.ok(!result.error.includes("/home/"), "error must not leak an absolute path/stack");
|
||||
assert.ok(!result.error.includes(":123:45"), "error must not leak a stack trace location");
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("handleImageGeneration(nvidia): upstream non-2xx response is surfaced with its status", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = async () =>
|
||||
new Response("rate limited", { status: 429, headers: { "content-type": "text/plain" } });
|
||||
|
||||
try {
|
||||
const result = await handleImageGeneration({
|
||||
body: {
|
||||
model: "nvidia/black-forest-labs/flux.1-schnell",
|
||||
prompt: "test",
|
||||
},
|
||||
credentials: { apiKey: "nv-token" },
|
||||
log: null,
|
||||
});
|
||||
|
||||
assert.equal(result.success, false);
|
||||
assert.equal(result.status, 429);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
@@ -1,188 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const { POST, OPTIONS } = await import("../../src/app/api/v1/ocr/route.ts");
|
||||
const { handleOcr } = await import("../../open-sse/handlers/ocr.ts");
|
||||
const { OCR_PROVIDERS, getOcrProvider, parseOcrModel, getAllOcrModels } =
|
||||
await import("../../open-sse/config/ocrRegistry.ts");
|
||||
const { v1OcrSchema } = await import("../../src/shared/validation/schemas/apiV1.ts");
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
test.afterEach(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
});
|
||||
|
||||
function ocrRequest(body: string) {
|
||||
return new Request("http://localhost/v1/ocr", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body,
|
||||
});
|
||||
}
|
||||
|
||||
// ── Registry ───────────────────────────────────────────────────────────────
|
||||
|
||||
test("OCR_PROVIDERS registers mistral with the Mistral OCR base URL", () => {
|
||||
assert.equal(OCR_PROVIDERS.mistral.baseUrl, "https://api.mistral.ai/v1/ocr");
|
||||
const provider = getOcrProvider("mistral");
|
||||
assert.ok(provider);
|
||||
assert.equal(provider.authHeader, "bearer");
|
||||
assert.ok(provider.models.some((m: { id: string }) => m.id === "mistral-ocr-latest"));
|
||||
});
|
||||
|
||||
test("parseOcrModel routes bare and prefixed mistral models to the mistral provider", () => {
|
||||
assert.deepEqual(parseOcrModel("mistral-ocr-latest"), {
|
||||
provider: "mistral",
|
||||
model: "mistral-ocr-latest",
|
||||
});
|
||||
assert.deepEqual(parseOcrModel("mistral/mistral-ocr-latest"), {
|
||||
provider: "mistral",
|
||||
model: "mistral-ocr-latest",
|
||||
});
|
||||
// Unknown model → no provider resolved
|
||||
assert.deepEqual(parseOcrModel("mystery-model"), {
|
||||
provider: null,
|
||||
model: "mystery-model",
|
||||
});
|
||||
});
|
||||
|
||||
test("getAllOcrModels exposes the mistral OCR model with a provider prefix", () => {
|
||||
const models = getAllOcrModels();
|
||||
assert.ok(models.some((m: { id: string }) => m.id === "mistral/mistral-ocr-latest"));
|
||||
});
|
||||
|
||||
// ── Schema (Zod, Rule #7) ────────────────────────────────────────────────────
|
||||
|
||||
test("v1OcrSchema rejects a body without a document", () => {
|
||||
const result = v1OcrSchema.safeParse({ model: "mistral-ocr-latest" });
|
||||
assert.equal(result.success, false);
|
||||
});
|
||||
|
||||
test("v1OcrSchema accepts a document_url document object", () => {
|
||||
const result = v1OcrSchema.safeParse({
|
||||
model: "mistral-ocr-latest",
|
||||
document: { type: "document_url", document_url: "https://example.com/a.pdf" },
|
||||
});
|
||||
assert.equal(result.success, true);
|
||||
});
|
||||
|
||||
test("v1OcrSchema accepts an image_url document object", () => {
|
||||
const result = v1OcrSchema.safeParse({
|
||||
document: { type: "image_url", image_url: "https://example.com/a.png" },
|
||||
});
|
||||
assert.equal(result.success, true);
|
||||
});
|
||||
|
||||
// ── Route (public /v1/ocr entry point) ───────────────────────────────────────
|
||||
|
||||
test("POST /v1/ocr returns 400 for invalid JSON without leaking a stack trace", async () => {
|
||||
const response = await POST(ocrRequest("not json at all"));
|
||||
const body = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 400);
|
||||
assert.equal(body.error.message, "Invalid JSON body");
|
||||
// Rule #12 — error responses must never leak stack traces.
|
||||
assert.ok(!body.error.message.includes("at /"));
|
||||
});
|
||||
|
||||
test("OPTIONS /v1/ocr answers the CORS preflight", async () => {
|
||||
const response = await OPTIONS();
|
||||
assert.equal(response.status, 200);
|
||||
assert.match(response.headers.get("access-control-allow-methods") || "", /OPTIONS/);
|
||||
});
|
||||
|
||||
// ── Handler ──────────────────────────────────────────────────────────────────
|
||||
|
||||
test("handleOcr requires a document", async () => {
|
||||
const response = await handleOcr({
|
||||
body: { model: "mistral-ocr-latest" },
|
||||
credentials: { apiKey: "sk-test" },
|
||||
});
|
||||
const payload = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 400);
|
||||
assert.equal(payload.error.message, "document is required");
|
||||
});
|
||||
|
||||
test("handleOcr rejects unknown OCR models", async () => {
|
||||
const response = await handleOcr({
|
||||
body: { model: "mystery/ocr", document: { document_url: "x" } },
|
||||
credentials: { apiKey: "sk-test" },
|
||||
});
|
||||
const payload = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 400);
|
||||
assert.match(payload.error.message, /No OCR provider found/);
|
||||
});
|
||||
|
||||
test("handleOcr requires credentials for the resolved provider", async () => {
|
||||
const response = await handleOcr({
|
||||
body: { document: { document_url: "x" } },
|
||||
credentials: null,
|
||||
});
|
||||
const payload = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 401);
|
||||
assert.equal(payload.error.message, "No credentials for OCR provider: mistral");
|
||||
});
|
||||
|
||||
test("handleOcr proxies a successful request to the mistral OCR endpoint", async () => {
|
||||
let captured: any;
|
||||
globalThis.fetch = async (url: any, options: any = {}) => {
|
||||
captured = {
|
||||
url: String(url),
|
||||
headers: options.headers,
|
||||
body: JSON.parse(String(options.body || "{}")),
|
||||
};
|
||||
return Response.json({ pages: [{ index: 0, markdown: "hello" }] });
|
||||
};
|
||||
|
||||
const response = await handleOcr({
|
||||
body: { document: { type: "document_url", document_url: "https://example.com/a.pdf" } },
|
||||
credentials: { apiKey: "sk-mistral" },
|
||||
});
|
||||
|
||||
assert.equal(captured.url, "https://api.mistral.ai/v1/ocr");
|
||||
assert.equal(captured.headers.Authorization, "Bearer sk-mistral");
|
||||
// model defaults to mistral-ocr-latest and the document is forwarded upstream.
|
||||
assert.equal(captured.body.model, "mistral-ocr-latest");
|
||||
assert.deepEqual(captured.body.document, {
|
||||
type: "document_url",
|
||||
document_url: "https://example.com/a.pdf",
|
||||
});
|
||||
assert.equal(response.status, 200);
|
||||
assert.deepEqual(await response.json(), { pages: [{ index: 0, markdown: "hello" }] });
|
||||
});
|
||||
|
||||
test("handleOcr passes upstream error payloads through with the upstream status", async () => {
|
||||
globalThis.fetch = async () =>
|
||||
new Response('{"error":"bad request"}', {
|
||||
status: 422,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
|
||||
const response = await handleOcr({
|
||||
body: { model: "mistral/mistral-ocr-latest", document: { document_url: "x" } },
|
||||
credentials: { apiKey: "sk-test" },
|
||||
});
|
||||
|
||||
assert.equal(response.status, 422);
|
||||
assert.equal(await response.text(), '{"error":"bad request"}');
|
||||
});
|
||||
|
||||
test("handleOcr returns a sanitized 500 when the upstream request throws", async () => {
|
||||
globalThis.fetch = async () => {
|
||||
throw new Error("socket closed");
|
||||
};
|
||||
|
||||
const response = await handleOcr({
|
||||
body: { model: "mistral-ocr-latest", document: { document_url: "x" } },
|
||||
credentials: { apiKey: "sk-test" },
|
||||
});
|
||||
const payload = (await response.json()) as any;
|
||||
|
||||
assert.equal(response.status, 500);
|
||||
assert.match(payload.error.message, /OCR request failed: socket closed/);
|
||||
assert.ok(!payload.error.message.includes("at /"));
|
||||
});
|
||||
@@ -1,56 +0,0 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
// Split-guard for the openai-responses request-translator extraction.
|
||||
// Pure shared primitives live in `openai-responses/helpers.ts`; the chat->Responses
|
||||
// direction (`openaiToOpenAIResponsesRequest`) lives in `openai-responses/toResponses.ts`.
|
||||
// The host keeps `openaiResponsesToOpenAIRequest` + both register() calls and re-exports
|
||||
// the moved function so external importers (tests) keep working unchanged.
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const REQ = join(HERE, "../../open-sse/translator/request");
|
||||
const HOST = join(REQ, "openai-responses.ts");
|
||||
const HELPERS = join(REQ, "openai-responses/helpers.ts");
|
||||
const TO_RESPONSES = join(REQ, "openai-responses/toResponses.ts");
|
||||
|
||||
test("helpers leaf is pure (no host import) and exports the shared primitives", () => {
|
||||
const src = readFileSync(HELPERS, "utf8");
|
||||
assert.doesNotMatch(src, /from "\.\.\/openai-responses\.ts"/);
|
||||
for (const sym of ["toRecord", "toString", "clampCallId", "normalizeVerbosity"]) {
|
||||
assert.match(src, new RegExp(`export (function|const) ${sym}\\b`));
|
||||
}
|
||||
});
|
||||
|
||||
test("toResponses leaf hosts the chat->Responses direction and imports helpers, not the host", () => {
|
||||
const src = readFileSync(TO_RESPONSES, "utf8");
|
||||
assert.match(src, /export function openaiToOpenAIResponsesRequest\(/);
|
||||
assert.match(src, /from "\.\/helpers\.ts"/);
|
||||
assert.doesNotMatch(src, /from "\.\.\/openai-responses\.ts"/);
|
||||
});
|
||||
|
||||
test("host re-exports the moved function and keeps both register() directions", () => {
|
||||
const src = readFileSync(HOST, "utf8");
|
||||
assert.match(
|
||||
src,
|
||||
/export \{ openaiToOpenAIResponsesRequest \} from "\.\/openai-responses\/toResponses\.ts"/
|
||||
);
|
||||
assert.match(src, /export function openaiResponsesToOpenAIRequest\(/);
|
||||
assert.match(src, /register\(FORMATS\.OPENAI_RESPONSES, FORMATS\.OPENAI,/);
|
||||
assert.match(src, /register\(FORMATS\.OPENAI, FORMATS\.OPENAI_RESPONSES,/);
|
||||
});
|
||||
|
||||
test("both directions are callable via the host module", async () => {
|
||||
const mod = await import("../../open-sse/translator/request/openai-responses.ts");
|
||||
assert.equal(typeof mod.openaiResponsesToOpenAIRequest, "function");
|
||||
assert.equal(typeof mod.openaiToOpenAIResponsesRequest, "function");
|
||||
// chat->Responses basic shape: wraps into { input: [...], stream: true }.
|
||||
const out = mod.openaiToOpenAIResponsesRequest(
|
||||
"gpt-4",
|
||||
{ messages: [{ role: "user", content: "hi" }] },
|
||||
true,
|
||||
null
|
||||
) as Record<string, unknown>;
|
||||
assert.ok(Array.isArray(out.input));
|
||||
});
|
||||
@@ -1,37 +1,14 @@
|
||||
/**
|
||||
* TDD regression for #5312 (FIX D / RC-D) and #5945.
|
||||
* TDD regression for #5312 (FIX D / RC-D): openai-to-claude reconstructed a Claude
|
||||
* `thinking` block from signature-less `reasoning_content` and stamped it with the
|
||||
* fabricated DEFAULT_THINKING_CLAUDE_SIGNATURE. Anthropic validates signatures and
|
||||
* rejects the fake one with 400 "Invalid signature in thinking block" — and
|
||||
* claudeHelper's latest-assistant guard preserves the block verbatim, so the fake
|
||||
* signature leaks upstream.
|
||||
*
|
||||
* #5312 (FIX D / RC-D): openai-to-claude reconstructed a Claude `thinking` block from
|
||||
* signature-less `reasoning_content` and stamped it with the fabricated
|
||||
* DEFAULT_THINKING_CLAUDE_SIGNATURE. Anthropic validates signatures and rejects the
|
||||
* fake one with 400 "Invalid signature in thinking block" — and claudeHelper's
|
||||
* latest-assistant guard preserves the block verbatim, so the fake signature leaks
|
||||
* upstream.
|
||||
*
|
||||
* Fix (#5312): when a precursor thinking block IS required by Anthropic's schema
|
||||
* (assistant turn has tool_use AND the outbound request has extended thinking
|
||||
* enabled), emit a signature-less `redacted_thinking` placeholder (matching what
|
||||
* prepareClaudeRequest produces downstream) instead of a fabricated-signature
|
||||
* `thinking` block. A REAL part.signature must always be preserved verbatim — never
|
||||
* overwritten with the default.
|
||||
*
|
||||
* #5945 (over-correction of #5312): the original #5312 fix injected the
|
||||
* redacted_thinking placeholder UNCONDITIONALLY whenever ANY assistant history
|
||||
* message carried non-empty `reasoning_content` — regardless of whether the current
|
||||
* outbound request has thinking enabled, and regardless of whether that assistant
|
||||
* turn even contains a `tool_use` block (the only case Anthropic's schema actually
|
||||
* requires a preceding thinking/redacted_thinking block). This fabricated a content
|
||||
* block the client never sent; reported by dev-cj: Claude Sonnet 5 via the "Pi"
|
||||
* harness detected the extra block and refused the turn as prompt injection.
|
||||
*
|
||||
* Fix (#5945): gate the injection on BOTH (a) the assistant turn containing a
|
||||
* tool_use block and (b) the outbound request having extended thinking enabled.
|
||||
* Otherwise `reasoning_content` is dropped silently — it carries no useful signal
|
||||
* for a plain-text replay turn, and the client never asked for it to appear.
|
||||
*
|
||||
* These two fixes are not in tension: #5312 legitimately fixed a real Anthropic 400
|
||||
* for the case the redacted_thinking block IS required; #5945 narrows the trigger to
|
||||
* exactly that case instead of firing for every reasoning_content-bearing message.
|
||||
* Fix: emit a signature-less `redacted_thinking` placeholder (matching what
|
||||
* prepareClaudeRequest produces downstream). A REAL part.signature must always be
|
||||
* preserved verbatim — never overwritten with the default.
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
@@ -43,7 +20,7 @@ const { DEFAULT_THINKING_CLAUDE_SIGNATURE } = await import(
|
||||
"../../open-sse/config/defaultThinkingSignature.ts"
|
||||
);
|
||||
|
||||
test("#5945: reasoning_content on a plain-text assistant turn (no tool_use, thinking not requested) yields NO redacted_thinking/thinking block", () => {
|
||||
test("#5312 RC-D: signature-less reasoning_content yields no fabricated-signature thinking block", () => {
|
||||
const result = openaiToClaudeRequest(
|
||||
"claude-opus-4-8",
|
||||
{
|
||||
@@ -51,7 +28,6 @@ test("#5945: reasoning_content on a plain-text assistant turn (no tool_use, thin
|
||||
{ role: "user", content: "hello" },
|
||||
{ role: "assistant", reasoning_content: "thinking about it", content: "hi there" },
|
||||
],
|
||||
// no body.thinking / body.reasoning_effort — thinking is NOT enabled for this request.
|
||||
},
|
||||
false
|
||||
);
|
||||
@@ -59,101 +35,24 @@ test("#5945: reasoning_content on a plain-text assistant turn (no tool_use, thin
|
||||
const assistant = result.messages.find((m) => m.role === "assistant");
|
||||
assert.ok(assistant, "expected assistant message");
|
||||
|
||||
// No fabricated block at all — reasoning_content must be dropped silently, exactly
|
||||
// like OPENAI_INCOMPATIBLE_ECHO_FIELDS drops other echo-only fields.
|
||||
assert.equal(
|
||||
assistant.content.find((b) => b && (b.type === "thinking" || b.type === "redacted_thinking")),
|
||||
undefined,
|
||||
"must NOT fabricate a thinking/redacted_thinking block the client never sent"
|
||||
);
|
||||
assert.deepEqual(
|
||||
assistant.content.map((b) => b.type),
|
||||
["text"],
|
||||
"assistant content should contain only the real text block"
|
||||
);
|
||||
});
|
||||
|
||||
test("#5945: reasoning_content + tool_use, but thinking NOT enabled on the outbound request, yields NO injection", () => {
|
||||
const result = openaiToClaudeRequest(
|
||||
"claude-opus-4-8",
|
||||
{
|
||||
messages: [
|
||||
{ role: "user", content: "hello" },
|
||||
{
|
||||
role: "assistant",
|
||||
reasoning_content: "thinking about it",
|
||||
tool_calls: [
|
||||
{
|
||||
id: "call_1",
|
||||
type: "function",
|
||||
function: { name: "get_weather", arguments: "{}" },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
// no body.thinking / body.reasoning_effort — thinking is NOT enabled.
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
const assistant = result.messages.find((m) => m.role === "assistant");
|
||||
assert.ok(assistant, "expected assistant message");
|
||||
assert.equal(
|
||||
assistant.content.find((b) => b && (b.type === "thinking" || b.type === "redacted_thinking")),
|
||||
undefined,
|
||||
"must NOT inject a precursor thinking block when the request itself has thinking disabled"
|
||||
);
|
||||
assert.ok(
|
||||
assistant.content.some((b) => b.type === "tool_use"),
|
||||
"tool_use block must still be present"
|
||||
);
|
||||
});
|
||||
|
||||
test("#5312: reasoning_content + tool_use + thinking ENABLED still gets a signature-less redacted_thinking precursor (the legitimate #5312 400-fix case)", () => {
|
||||
const result = openaiToClaudeRequest(
|
||||
"claude-opus-4-8",
|
||||
{
|
||||
thinking: { type: "enabled", budget_tokens: 4096 },
|
||||
messages: [
|
||||
{ role: "user", content: "hello" },
|
||||
{
|
||||
role: "assistant",
|
||||
reasoning_content: "thinking about it",
|
||||
tool_calls: [
|
||||
{
|
||||
id: "call_1",
|
||||
type: "function",
|
||||
function: { name: "get_weather", arguments: "{}" },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
const assistant = result.messages.find((m) => m.role === "assistant");
|
||||
assert.ok(assistant, "expected assistant message");
|
||||
|
||||
// No block may carry the fabricated default signature on a `thinking`-typed block.
|
||||
// No block may carry the fabricated default signature.
|
||||
const fake = assistant.content.find(
|
||||
(b) => b && b.type === "thinking" && b.signature === DEFAULT_THINKING_CLAUDE_SIGNATURE
|
||||
(b) => b && b.signature === DEFAULT_THINKING_CLAUDE_SIGNATURE
|
||||
);
|
||||
assert.equal(fake, undefined, "must NOT emit a `thinking` block with the fabricated signature");
|
||||
assert.equal(fake, undefined, "must NOT emit a thinking block with the fabricated signature");
|
||||
|
||||
// It becomes a redacted_thinking placeholder (Anthropic accepts without sig check),
|
||||
// and it must precede the tool_use block.
|
||||
assert.equal(assistant.content[0].type, "redacted_thinking", "must be the precursor block");
|
||||
assert.equal(assistant.content[0].data, DEFAULT_THINKING_CLAUDE_SIGNATURE);
|
||||
// No `thinking`-typed block at all from signature-less reasoning_content.
|
||||
assert.equal(
|
||||
assistant.content[0].signature,
|
||||
assistant.content.find((b) => b && b.type === "thinking"),
|
||||
undefined,
|
||||
"redacted_thinking must not carry a signature"
|
||||
);
|
||||
assert.ok(
|
||||
assistant.content.some((b) => b.type === "tool_use"),
|
||||
"tool_use block must still be present"
|
||||
"signature-less reasoning_content must not produce a `thinking` block"
|
||||
);
|
||||
|
||||
// It becomes a redacted_thinking placeholder (Anthropic accepts without sig check).
|
||||
const redacted = assistant.content.find((b) => b && b.type === "redacted_thinking");
|
||||
assert.ok(redacted, "expected a redacted_thinking placeholder");
|
||||
assert.equal(redacted.data, DEFAULT_THINKING_CLAUDE_SIGNATURE);
|
||||
assert.equal(redacted.signature, undefined, "redacted_thinking must not carry a signature");
|
||||
});
|
||||
|
||||
test("#5312 RC-D: a REAL thinking signature is preserved verbatim", () => {
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
// Split-guard for the openai-to-claude thinking-budget extraction.
|
||||
// `fitThinkingToMaxTokens` (+ its private helpers safeCapMaxOutputTokens / MIN_*)
|
||||
// live in the pure leaf `openai-to-claude/thinkingBudget.ts`; the host re-exports
|
||||
// the public symbol so external importers (tests) keep working unchanged.
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const REQ = join(HERE, "../../open-sse/translator/request");
|
||||
const HOST = join(REQ, "openai-to-claude.ts");
|
||||
const LEAF = join(REQ, "openai-to-claude/thinkingBudget.ts");
|
||||
|
||||
test("leaf hosts fitThinkingToMaxTokens and does not import the host", () => {
|
||||
const leaf = readFileSync(LEAF, "utf8");
|
||||
assert.match(leaf, /export function fitThinkingToMaxTokens\(/);
|
||||
assert.match(leaf, /function safeCapMaxOutputTokens\(/);
|
||||
assert.doesNotMatch(leaf, /from "\.\.\/openai-to-claude\.ts"/);
|
||||
});
|
||||
|
||||
test("host re-exports fitThinkingToMaxTokens from the leaf", () => {
|
||||
const host = readFileSync(HOST, "utf8");
|
||||
assert.match(
|
||||
host,
|
||||
/export \{ fitThinkingToMaxTokens \} from "\.\/openai-to-claude\/thinkingBudget\.ts"/
|
||||
);
|
||||
});
|
||||
|
||||
test("re-exported fitThinkingToMaxTokens is callable via the host module and behaves", async () => {
|
||||
const mod = await import("../../open-sse/translator/request/openai-to-claude.ts");
|
||||
assert.equal(typeof mod.fitThinkingToMaxTokens, "function");
|
||||
// No budgeted thinking → max_tokens floored to >= 1, thinking passed through.
|
||||
const out = mod.fitThinkingToMaxTokens("gpt-4o-mini", 0, undefined);
|
||||
assert.equal(out.thinking, undefined);
|
||||
assert.ok(out.maxTokens >= 1);
|
||||
});
|
||||
@@ -1,44 +0,0 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
// Split-guard for the openai-to-kiro message-helper extraction.
|
||||
// The pure tool/message helpers (parseToolInput / normalizeKiroToolSchema /
|
||||
// serializeToolResultContent) live in the leaf `openai-to-kiro/messageHelpers.ts`;
|
||||
// the host imports them back for convertMessages. They were module-private, so the
|
||||
// public export set is unchanged (no re-export needed).
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const REQ = join(HERE, "../../open-sse/translator/request");
|
||||
const HOST = join(REQ, "openai-to-kiro.ts");
|
||||
const LEAF = join(REQ, "openai-to-kiro/messageHelpers.ts");
|
||||
|
||||
test("leaf hosts the pure helpers and does not import the host", () => {
|
||||
const src = readFileSync(LEAF, "utf8");
|
||||
for (const sym of ["parseToolInput", "normalizeKiroToolSchema", "serializeToolResultContent"]) {
|
||||
assert.match(src, new RegExp(`export function ${sym}\\b`));
|
||||
}
|
||||
assert.doesNotMatch(src, /from "\.\.\/openai-to-kiro\.ts"/);
|
||||
});
|
||||
|
||||
test("host imports the helpers back from the leaf", () => {
|
||||
const src = readFileSync(HOST, "utf8");
|
||||
assert.match(src, /from "\.\/openai-to-kiro\/messageHelpers\.ts"/);
|
||||
// buildKiroPayload stays exported on the host module.
|
||||
assert.match(src, /export function buildKiroPayload\(/);
|
||||
});
|
||||
|
||||
test("normalizeKiroToolSchema strips empty required arrays and additionalProperties", async () => {
|
||||
const { normalizeKiroToolSchema } =
|
||||
await import("../../open-sse/translator/request/openai-to-kiro/messageHelpers.ts");
|
||||
const out = normalizeKiroToolSchema({
|
||||
type: "object",
|
||||
required: [],
|
||||
additionalProperties: false,
|
||||
properties: { a: { type: "string" } },
|
||||
});
|
||||
assert.equal("required" in out, false);
|
||||
assert.equal("additionalProperties" in out, false);
|
||||
assert.deepEqual(out.properties, { a: { type: "string" } });
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user