chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:39:12 +08:00
commit d8dcd5f6d1
8604 changed files with 2479390 additions and 0 deletions
+121
View File
@@ -0,0 +1,121 @@
// @vitest-environment jsdom
import React from "react";
import { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { AUTO_COMBO_TEMPLATES } from "@/domain/assessment/types";
// Minimal i18n stub — return interpolated value so {count} works.
vi.mock("next-intl", () => ({
useTranslations: () => (key: string, values?: Record<string, unknown>) => {
if (values && typeof values.count !== "undefined") {
return `${values.count} ${key}`;
}
return key;
},
}));
const cleanupCallbacks: Array<() => void> = [];
function makeContainer(): HTMLElement {
const container = document.createElement("div");
document.body.appendChild(container);
cleanupCallbacks.push(() => {
container.remove();
});
return container;
}
describe("AutoComboCatalog", () => {
beforeEach(() => {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
});
afterEach(() => {
while (cleanupCallbacks.length > 0) {
cleanupCallbacks.pop()?.();
}
document.body.innerHTML = "";
});
it("renders the header with translated title and template-count badge", async () => {
const { default: AutoComboCatalog } =
await import("@/app/(dashboard)/dashboard/combos/AutoComboCatalog");
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
root.render(<AutoComboCatalog />);
});
expect(container.textContent).toContain("autoCatalogTitle");
expect(container.textContent).toContain(
`${AUTO_COMBO_TEMPLATES.length} autoCatalogTemplateCount`
);
});
it("stays collapsed by default — no template rows in the DOM", async () => {
const { default: AutoComboCatalog } =
await import("@/app/(dashboard)/dashboard/combos/AutoComboCatalog");
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
root.render(<AutoComboCatalog />);
});
const first = AUTO_COMBO_TEMPLATES[0];
expect(container.textContent ?? "").not.toContain(first.name);
});
it("expands when toggled and lists every template name", async () => {
const { default: AutoComboCatalog } =
await import("@/app/(dashboard)/dashboard/combos/AutoComboCatalog");
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
root.render(<AutoComboCatalog />);
});
const toggle = container.querySelector("button");
expect(toggle).toBeTruthy();
await act(async () => {
toggle?.click();
});
for (const tpl of AUTO_COMBO_TEMPLATES) {
expect(container.textContent ?? "").toContain(tpl.name);
}
});
it("flips the toggle aria-label between expand and collapse", async () => {
const { default: AutoComboCatalog } =
await import("@/app/(dashboard)/dashboard/combos/AutoComboCatalog");
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
root.render(<AutoComboCatalog />);
});
const toggle = container.querySelector("button");
expect(toggle?.getAttribute("aria-label")).toBe("autoCatalogExpand");
expect(toggle?.getAttribute("aria-expanded")).toBe("false");
await act(async () => {
toggle?.click();
});
expect(toggle?.getAttribute("aria-label")).toBe("autoCatalogCollapse");
expect(toggle?.getAttribute("aria-expanded")).toBe("true");
});
it("renders the strategy badge for each template when expanded", async () => {
const { default: AutoComboCatalog } =
await import("@/app/(dashboard)/dashboard/combos/AutoComboCatalog");
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
root.render(<AutoComboCatalog />);
});
await act(async () => {
(container.querySelector("button") as HTMLButtonElement | null)?.click();
});
const strategies = new Set(AUTO_COMBO_TEMPLATES.map((t) => t.strategy));
for (const s of strategies) {
expect(container.textContent ?? "").toContain(s);
}
});
});
+191
View File
@@ -0,0 +1,191 @@
// @vitest-environment jsdom
import React from "react";
import { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
// Minimal next-intl stub — returns the translation key for inspection.
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
}));
// Minimal next/link stub — renders as a plain anchor element.
vi.mock("next/link", () => ({
default: ({
href,
children,
className,
}: {
href: string;
children: React.ReactNode;
className?: string;
}) => (
<a href={href} className={className}>
{children}
</a>
),
}));
const cleanupCallbacks: Array<() => void> = [];
function makeContainer(): HTMLElement {
const container = document.createElement("div");
document.body.appendChild(container);
cleanupCallbacks.push(() => {
container.remove();
});
return container;
}
describe("SkillsConceptCard", () => {
beforeEach(() => {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
});
afterEach(() => {
while (cleanupCallbacks.length > 0) {
cleanupCallbacks.pop()?.();
}
document.body.innerHTML = "";
vi.clearAllMocks();
});
it("renders agent variant with correct i18n keys", async () => {
const { SkillsConceptCard } = await import(
"../../src/shared/components/SkillsConceptCard.tsx"
);
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
root.render(<SkillsConceptCard variant="agent" />);
});
const text = container.textContent ?? "";
// The i18n mock returns the key itself, so these keys should appear.
expect(text).toContain("conceptCard.agent.title");
expect(text).toContain("conceptCard.agent.crossLinkLabel");
});
it("renders omni variant with correct i18n keys", async () => {
const { SkillsConceptCard } = await import(
"../../src/shared/components/SkillsConceptCard.tsx"
);
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
root.render(<SkillsConceptCard variant="omni" />);
});
const text = container.textContent ?? "";
expect(text).toContain("conceptCard.omni.title");
expect(text).toContain("conceptCard.omni.crossLinkLabel");
});
it("agent variant renders 5 comparison rows (whatIs, direction, executor, storage, tagline)", async () => {
const { SkillsConceptCard } = await import(
"../../src/shared/components/SkillsConceptCard.tsx"
);
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
root.render(<SkillsConceptCard variant="agent" />);
});
const rows = container.querySelectorAll("[data-testid^='comparison-row-']");
expect(rows.length).toBe(5);
const rowIds = Array.from(rows).map((r) => r.getAttribute("data-testid"));
expect(rowIds).toContain("comparison-row-whatIs");
expect(rowIds).toContain("comparison-row-direction");
expect(rowIds).toContain("comparison-row-executor");
expect(rowIds).toContain("comparison-row-storage");
expect(rowIds).toContain("comparison-row-tagline");
});
it("omni variant renders 5 comparison rows", async () => {
const { SkillsConceptCard } = await import(
"../../src/shared/components/SkillsConceptCard.tsx"
);
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
root.render(<SkillsConceptCard variant="omni" />);
});
const rows = container.querySelectorAll("[data-testid^='comparison-row-']");
expect(rows.length).toBe(5);
});
it("renders data-testid for variant", async () => {
const { SkillsConceptCard } = await import(
"../../src/shared/components/SkillsConceptCard.tsx"
);
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
root.render(<SkillsConceptCard variant="agent" />);
});
const card = container.querySelector("[data-testid='skills-concept-card-agent']");
expect(card).not.toBeNull();
});
it("agent variant cross-link points to /dashboard/omni-skills", async () => {
const { SkillsConceptCard } = await import(
"../../src/shared/components/SkillsConceptCard.tsx"
);
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
root.render(<SkillsConceptCard variant="agent" />);
});
const link = container.querySelector("a");
expect(link).not.toBeNull();
expect(link?.getAttribute("href")).toBe("/dashboard/omni-skills");
});
it("omni variant cross-link points to /dashboard/agent-skills", async () => {
const { SkillsConceptCard } = await import(
"../../src/shared/components/SkillsConceptCard.tsx"
);
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
root.render(<SkillsConceptCard variant="omni" />);
});
const link = container.querySelector("a");
expect(link).not.toBeNull();
expect(link?.getAttribute("href")).toBe("/dashboard/agent-skills");
});
it("accepts optional className prop", async () => {
const { SkillsConceptCard } = await import(
"../../src/shared/components/SkillsConceptCard.tsx"
);
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
root.render(<SkillsConceptCard variant="agent" className="my-custom-class" />);
});
const el = container.firstElementChild as HTMLElement | null;
expect(el?.className).toContain("my-custom-class");
});
it("renders without crashing when className is omitted", async () => {
const { SkillsConceptCard } = await import(
"../../src/shared/components/SkillsConceptCard.tsx"
);
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
root.render(<SkillsConceptCard variant="agent" />);
});
expect(container.children.length).toBeGreaterThan(0);
});
});
@@ -0,0 +1,39 @@
/**
* #5757 guard fixture — NOT a test file (no `.test.` in the name, so the runner
* skips it). Exercised by `tests/unit/tsx-runtime-transform-5757.test.ts`.
*
* It concentrates the modern JS/TS syntax that the published CLI's runtime
* `tsx/esm` loader (`bin/omniroute.mjs` → `await import("tsx/esm")`) must
* transform through esbuild at startup: object/array destructuring + rest,
* object/array spread, class + private fields, optional chaining, nullish
* coalescing, logical assignment, async/await and top-level await.
*
* If a future esbuild (pulled transitively via `tsx`) cannot transform this on a
* supported Node runtime, running this file fails — which is the whole point.
*/
class Box {
value = 41; // public class field
#secret = 1; // private field
bump(): number {
this.value += this.#secret;
return this.value;
}
}
async function main() {
const { a, b, ...rest } = { a: 1, b: 2, c: 3, d: 4 }; // object destructuring + rest
const [first, ...tail] = [10, 20, 30]; // array destructuring + rest
const merged = { ...rest, first }; // object spread
const arr = [...tail, first]; // array spread
const bumped = new Box().bump(); // class + private field
const maybe: { x?: { y?: number } } = {};
const opt = maybe?.x?.y ?? 99; // optional chaining + nullish coalescing
let acc = 0;
acc ||= bumped; // logical assignment
const total = await Promise.resolve(a + b + first + opt + acc); // async/await
return { a, b, rest, first, tail, merged, arr, bumped, opt, total };
}
const result = await main(); // top-level await
console.log("TSX_TRANSFORM_OK " + JSON.stringify(result));
+101
View File
@@ -0,0 +1,101 @@
/**
* Test harness for MitmHandlerBase subclasses.
*
* Mocks `globalThis.fetch` so handlers exercise their full intercept() path
* (router round-trip + SSE pipe) without touching the network. Returns the
* captured payload, response chunks written to the fake ServerResponse, and
* the final status code.
*/
import type { IncomingMessage, ServerResponse } from "node:http";
import { Readable } from "node:stream";
import type { MitmHandlerBase } from "../../src/mitm/handlers/base.ts";
export interface HarnessResult {
fetchCalled: boolean;
fetchUrl: string | null;
fetchHeaders: Record<string, string>;
fetchBody: string;
status: number;
responseChunks: string[];
}
function fakeReq(
headers: Record<string, string> = {},
url = "/v1/chat/completions"
): IncomingMessage {
return {
method: "POST",
url,
headers: {
host: "api.example.com",
"user-agent": "ut",
...headers,
},
} as unknown as IncomingMessage;
}
function fakeRes(): { res: ServerResponse; out: HarnessResult } {
const out: HarnessResult = {
fetchCalled: false,
fetchUrl: null,
fetchHeaders: {},
fetchBody: "",
status: 0,
responseChunks: [],
};
let headersSent = false;
const res = {
get headersSent() {
return headersSent;
},
writeHead(s: number) {
out.status = s;
headersSent = true;
},
write(c: Buffer | string) {
out.responseChunks.push(typeof c === "string" ? c : c.toString());
return true;
},
end(c?: Buffer | string) {
if (c) out.responseChunks.push(typeof c === "string" ? c : c.toString());
},
} as unknown as ServerResponse;
return { res, out };
}
export async function runHandler(
handler: MitmHandlerBase,
body: unknown,
mappedModel: string,
opts: {
upstreamStatus?: number;
upstreamBody?: string;
headers?: Record<string, string>;
url?: string;
} = {}
): Promise<HarnessResult> {
const { res, out } = fakeRes();
const req = fakeReq(opts.headers, opts.url);
const buf = Buffer.from(typeof body === "string" ? body : JSON.stringify(body));
const originalFetch = globalThis.fetch;
globalThis.fetch = (async (url: string, init: RequestInit) => {
out.fetchCalled = true;
out.fetchUrl = String(url);
out.fetchHeaders = (init?.headers ?? {}) as Record<string, string>;
out.fetchBody = typeof init?.body === "string" ? init.body : "";
const upstreamBody = opts.upstreamBody ?? "data: hello\n\n";
const status = opts.upstreamStatus ?? 200;
const stream = Readable.toWeb(
Readable.from(Buffer.from(upstreamBody))
) as unknown as ReadableStream<Uint8Array>;
return new Response(stream, { status });
}) as unknown as typeof fetch;
try {
await handler.intercept(req, res, buf, mappedModel);
} finally {
globalThis.fetch = originalFetch;
}
return out;
}
+81
View File
@@ -0,0 +1,81 @@
/**
* Shared settings test fixture.
*
* Backs onto a real isolated SQLite DB + the production
* `updateSettings → applyRuntimeSettings` pipeline so callers exercise the
* actual hot-reload path. Tests that need to mock `getAuthzBypassSnapshot`
* directly defeat the integration value of AC-7 — use this helper instead.
*
* Usage:
* ```ts
* import { setupSettingsFixture, mockSettings, resetSettingsMock } from "../_mocks/settings";
* const fixture = setupSettingsFixture("authz-bypass");
* test.beforeEach(() => fixture.resetStorage());
* await mockSettings({ localOnlyManageScopeBypassEnabled: false });
* ```
*/
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
type SettingsPatch = Record<string, unknown>;
let activeFixture: SettingsFixture | null = null;
export interface SettingsFixture {
testDataDir: string;
resetStorage(): Promise<void>;
cleanup(): void;
}
/**
* Allocate an isolated DATA_DIR + reset DB state per test. Must run BEFORE
* any DB modules are imported by the test file.
*/
export function setupSettingsFixture(slug: string): SettingsFixture {
const testDataDir = fs.mkdtempSync(path.join(os.tmpdir(), `omr-settings-mock-${slug}-`));
process.env.DATA_DIR = testDataDir;
if (!process.env.API_KEY_SECRET) {
process.env.API_KEY_SECRET = `test-settings-mock-secret-${Date.now()}`;
}
const fixture: SettingsFixture = {
testDataDir,
async resetStorage() {
const core = await import("../../../src/lib/db/core.ts");
const runtime = await import("../../../src/lib/config/runtimeSettings.ts");
core.resetDbInstance();
runtime.resetRuntimeSettingsStateForTests();
fs.rmSync(testDataDir, { recursive: true, force: true });
fs.mkdirSync(testDataDir, { recursive: true });
},
cleanup() {
fs.rmSync(testDataDir, { recursive: true, force: true });
},
};
activeFixture = fixture;
return fixture;
}
/**
* Write a partial Settings patch through the production
* `updateSettings → applyRuntimeSettings` pipeline. Hot-reload side effects
* (route guard snapshot, etc.) fire exactly as they do in `PATCH /api/settings`.
*/
export async function mockSettings(partial: SettingsPatch): Promise<Record<string, unknown>> {
const settingsDb = await import("../../../src/lib/db/settings.ts");
return settingsDb.updateSettings(partial);
}
/**
* Reset the in-process runtime snapshot to the cold-boot default. Called by
* test `beforeEach` hooks that need a clean slate without nuking the whole
* fixture directory.
*/
export async function resetSettingsMock(): Promise<void> {
const runtime = await import("../../../src/lib/config/runtimeSettings.ts");
runtime.resetRuntimeSettingsStateForTests();
if (activeFixture) {
await activeFixture.resetStorage();
}
}
+118
View File
@@ -0,0 +1,118 @@
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 type { NextRequest } from "next/server";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-a2a-enabled-route-"));
const ORIGINAL_DATA_DIR = process.env.DATA_DIR;
const ORIGINAL_OMNIROUTE_API_KEY = process.env.OMNIROUTE_API_KEY;
process.env.DATA_DIR = TEST_DATA_DIR;
delete process.env.OMNIROUTE_API_KEY;
const core = await import("../../src/lib/db/core.ts");
const settingsDb = await import("../../src/lib/db/settings.ts");
const statusRoute = await import("../../src/app/api/a2a/status/route.ts");
const a2aRoute = await import("../../src/app/a2a/route.ts");
async function resetStorage() {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}
function makeJsonRpcRequest(body: unknown): NextRequest {
return new Request("http://localhost/a2a", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
}) as unknown as NextRequest;
}
test.beforeEach(async () => {
delete process.env.OMNIROUTE_API_KEY;
await resetStorage();
});
test.after(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
if (ORIGINAL_DATA_DIR === undefined) {
delete process.env.DATA_DIR;
} else {
process.env.DATA_DIR = ORIGINAL_DATA_DIR;
}
if (ORIGINAL_OMNIROUTE_API_KEY === undefined) {
delete process.env.OMNIROUTE_API_KEY;
} else {
process.env.OMNIROUTE_API_KEY = ORIGINAL_OMNIROUTE_API_KEY;
}
});
test("A2A status reports disabled and offline when the endpoint is off", async () => {
const response = await statusRoute.GET();
const body = (await response.json()) as Record<string, unknown>;
assert.equal(response.status, 200);
assert.equal(body.status, "disabled");
assert.equal(body.enabled, false);
assert.equal(body.online, false);
assert.equal(body.agent, null);
});
test("A2A JSON-RPC rejects requests while the endpoint is disabled", async () => {
const response = await a2aRoute.POST(
makeJsonRpcRequest({
jsonrpc: "2.0",
id: "disabled-check",
method: "message/send",
params: { message: { role: "user", content: "hello" } },
})
);
const body = (await response.json()) as {
id?: string | number | null;
error?: { code?: number; message?: string };
};
assert.equal(response.status, 503);
assert.equal(body.id, "disabled-check");
assert.equal(body.error?.code, -32000);
assert.match(body.error?.message || "", /disabled/i);
});
test("A2A JSON-RPC checks auth before returning disabled state", async () => {
process.env.OMNIROUTE_API_KEY = "test-secret";
const response = await a2aRoute.POST(
makeJsonRpcRequest({
jsonrpc: "2.0",
id: "unauthorized-disabled-check",
method: "message/send",
params: { message: { role: "user", content: "hello" } },
})
);
const body = (await response.json()) as {
error?: { code?: number; message?: string };
};
assert.equal(response.status, 400);
assert.equal(body.error?.code, -32600);
assert.match(body.error?.message || "", /Unauthorized/i);
});
test("A2A status reports online only after enabling the endpoint", async () => {
await settingsDb.updateSettings({ a2aEnabled: true });
const response = await statusRoute.GET();
const body = (await response.json()) as Record<string, unknown>;
assert.equal(response.status, 200);
assert.equal(body.status, "ok");
assert.equal(body.enabled, true);
assert.equal(body.online, true);
assert.equal(typeof body.tasks, "object");
});
+32
View File
@@ -0,0 +1,32 @@
import test from "node:test";
import assert from "node:assert/strict";
import { logRoutingDecision } from "../../src/lib/a2a/routingLogger.ts";
test("logRoutingDecision records a routing decision with generated metadata", () => {
const decision = logRoutingDecision({
taskType: "chat",
comboId: "combo-1",
providerSelected: "openai",
modelUsed: "gpt-4o-mini",
score: 0.91,
factors: [
{
name: "health",
value: 1,
weight: 0.5,
contribution: 0.5,
},
],
fallbacksTriggered: [],
success: true,
latencyMs: 123,
cost: 0.001,
});
assert.equal(typeof decision.requestId, "string");
assert.match(decision.requestId, /^[0-9a-f-]{36}$/);
assert.match(decision.timestamp, /^\d{4}-\d{2}-\d{2}T/);
assert.equal(decision.providerSelected, "openai");
assert.equal(decision.factors[0].name, "health");
});
+48
View File
@@ -0,0 +1,48 @@
import test from "node:test";
import assert from "node:assert/strict";
import { inferRequiredScope } from "../../src/server/authz/accessScopes.ts";
test("read methods default to read", () => {
assert.equal(inferRequiredScope("GET", "/api/v1/models"), "read");
assert.equal(inferRequiredScope("HEAD", "/api/health"), "read");
assert.equal(inferRequiredScope("OPTIONS", "/api/anything"), "read");
});
test("mutating methods default to write", () => {
assert.equal(inferRequiredScope("POST", "/api/keys"), "write");
assert.equal(inferRequiredScope("PUT", "/api/config"), "write");
assert.equal(inferRequiredScope("PATCH", "/api/combo/x"), "write");
assert.equal(inferRequiredScope("DELETE", "/api/keys/abc"), "write");
});
test("admin-prefix routes require admin for ANY method", () => {
assert.equal(inferRequiredScope("GET", "/api/cli/tokens"), "admin");
assert.equal(inferRequiredScope("POST", "/api/cli/tokens"), "admin");
assert.equal(inferRequiredScope("DELETE", "/api/cli/tokens/tok_1"), "admin");
assert.equal(inferRequiredScope("GET", "/api/oauth/start"), "admin");
assert.equal(inferRequiredScope("POST", "/api/auth/login"), "admin");
assert.equal(inferRequiredScope("POST", "/api/policy"), "admin");
assert.equal(inferRequiredScope("POST", "/api/services/foo/start"), "admin");
});
test("admin-mutation prefixes: GET stays read, mutations become admin", () => {
// providers: status is read, but creating/rotating is admin
assert.equal(inferRequiredScope("GET", "/api/providers/status"), "read");
assert.equal(inferRequiredScope("GET", "/api/providers"), "read");
assert.equal(inferRequiredScope("POST", "/api/providers"), "admin");
assert.equal(inferRequiredScope("DELETE", "/api/providers/openai"), "admin");
// cli-tools/apply writes to the host fs
assert.equal(inferRequiredScope("POST", "/api/cli-tools/apply"), "admin");
});
test("a brand-new mutating route is write by default (not admin)", () => {
assert.equal(inferRequiredScope("POST", "/api/some-future-route"), "write");
assert.equal(inferRequiredScope("GET", "/api/some-future-route"), "read");
});
test("prefix matching does not over-match unrelated paths", () => {
// "/api/authz-inventory" must NOT be caught by the "/api/auth" admin prefix
assert.equal(inferRequiredScope("GET", "/api/authz-inventory"), "read");
// "/api/services" itself and its children are admin, but a lookalike is not
assert.equal(inferRequiredScope("GET", "/api/services-catalog"), "read");
});
+58
View File
@@ -0,0 +1,58 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
ACCESS_SCOPES,
isAccessScope,
scopeSatisfies,
normalizeScope,
} from "../../src/lib/accessTokens/scopes.ts";
test("ACCESS_SCOPES are exactly read/write/admin", () => {
assert.deepEqual([...ACCESS_SCOPES], ["read", "write", "admin"]);
});
test("isAccessScope accepts valid scopes and rejects anything else", () => {
assert.equal(isAccessScope("read"), true);
assert.equal(isAccessScope("write"), true);
assert.equal(isAccessScope("admin"), true);
assert.equal(isAccessScope("superuser"), false);
assert.equal(isAccessScope(""), false);
assert.equal(isAccessScope(null), false);
assert.equal(isAccessScope(undefined), false);
assert.equal(isAccessScope(3), false);
});
test("scopeSatisfies enforces the admin ⊃ write ⊃ read hierarchy", () => {
// admin satisfies everything
assert.equal(scopeSatisfies("admin", "read"), true);
assert.equal(scopeSatisfies("admin", "write"), true);
assert.equal(scopeSatisfies("admin", "admin"), true);
// write satisfies read+write, not admin
assert.equal(scopeSatisfies("write", "read"), true);
assert.equal(scopeSatisfies("write", "write"), true);
assert.equal(scopeSatisfies("write", "admin"), false);
// read satisfies only read
assert.equal(scopeSatisfies("read", "read"), true);
assert.equal(scopeSatisfies("read", "write"), false);
assert.equal(scopeSatisfies("read", "admin"), false);
});
test("scopeSatisfies returns false for invalid `have` scopes (fail closed)", () => {
assert.equal(scopeSatisfies("bogus", "read"), false);
assert.equal(scopeSatisfies(null, "read"), false);
assert.equal(scopeSatisfies(undefined, "read"), false);
assert.equal(scopeSatisfies("", "read"), false);
});
test("normalizeScope falls back to read by default for invalid input", () => {
assert.equal(normalizeScope("write"), "write");
assert.equal(normalizeScope("admin"), "admin");
assert.equal(normalizeScope("bogus"), "read");
assert.equal(normalizeScope(undefined), "read");
assert.equal(normalizeScope(null), "read");
});
test("normalizeScope honors a custom fallback", () => {
assert.equal(normalizeScope("bogus", "write"), "write");
assert.equal(normalizeScope(undefined, "admin"), "admin");
});
+104
View File
@@ -0,0 +1,104 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
// DB-backed access-token store. Uses an isolated DATA_DIR + closes the handle in
// test.after (CLAUDE.md "Database Handles in Tests" — otherwise Node's runner hangs).
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-access-tokens-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.DISABLE_SQLITE_AUTO_BACKUP = "true";
const core = await import("../../src/lib/db/core.ts");
const at = await import("../../src/lib/db/accessTokens.ts");
test.after(() => {
try {
core.resetDbInstance();
} catch {}
try {
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
} catch {}
});
test("createAccessToken returns a secret prefixed oma_live_ and a masked record", () => {
const { record, secret } = at.createAccessToken({ name: "laptop", scope: "write" });
assert.match(secret, /^oma_live_/);
assert.equal(record.name, "laptop");
assert.equal(record.scope, "write");
assert.ok(record.id.startsWith("tok_"));
assert.ok(secret.startsWith(record.tokenPrefix), "prefix must be a prefix of the secret");
assert.equal(record.revokedAt, null);
});
test("createAccessToken defaults to the safest scope (read) for invalid input", () => {
const { record } = at.createAccessToken({ name: "x", scope: "bogus" });
assert.equal(record.scope, "read");
});
test("createAccessToken rejects an empty name", () => {
assert.throws(() => at.createAccessToken({ name: " ", scope: "read" }), /name is required/);
});
test("verifyAccessToken returns identity+scope for a valid secret, null for wrong", () => {
const { secret } = at.createAccessToken({ name: "verify-me", scope: "admin" });
const v = at.verifyAccessToken(secret);
assert.ok(v);
assert.equal(v?.scope, "admin");
assert.equal(v?.name, "verify-me");
assert.equal(at.verifyAccessToken("oma_live_wrong"), null);
assert.equal(at.verifyAccessToken(""), null);
assert.equal(at.verifyAccessToken(null), null);
});
test("only the hash is stored — the plaintext secret never lands in the DB", () => {
const { secret, record } = at.createAccessToken({ name: "secrecy", scope: "read" });
const db = core.getDbInstance();
const row = db
.prepare("SELECT token_hash, token_prefix FROM cli_access_tokens WHERE id = ?")
.get(record.id) as { token_hash: string; token_prefix: string };
assert.notEqual(row.token_hash, secret, "must store hash, not plaintext");
assert.equal(row.token_hash, at.hashAccessToken(secret));
assert.equal(row.token_hash.length, 64, "sha-256 hex");
});
test("verifyAccessToken stamps last_used_at", () => {
const { secret, record } = at.createAccessToken({ name: "touch", scope: "read" });
assert.equal(at.getAccessToken(record.id)?.lastUsedAt, null);
at.verifyAccessToken(secret);
assert.notEqual(at.getAccessToken(record.id)?.lastUsedAt, null);
});
test("revoked tokens fail verification", () => {
const { secret, record } = at.createAccessToken({ name: "to-revoke", scope: "write" });
assert.ok(at.verifyAccessToken(secret));
assert.equal(at.revokeAccessToken(record.id), true);
assert.equal(at.verifyAccessToken(secret), null);
// idempotent: revoking again is a no-op
assert.equal(at.revokeAccessToken(record.id), false);
});
test("revokeAccessToken works by display prefix too", () => {
const { secret, record } = at.createAccessToken({ name: "by-prefix", scope: "read" });
assert.equal(at.revokeAccessToken(record.tokenPrefix), true);
assert.equal(at.verifyAccessToken(secret), null);
});
test("expired tokens fail verification", () => {
const past = new Date(Date.now() - 60_000).toISOString();
const { secret } = at.createAccessToken({ name: "expired", scope: "admin", expiresAt: past });
assert.equal(at.verifyAccessToken(secret), null);
});
test("listAccessTokens returns masked records (no secret/hash field)", () => {
at.createAccessToken({ name: "listed", scope: "read" });
const list = at.listAccessTokens();
assert.ok(list.length >= 1);
for (const rec of list) {
assert.ok("tokenPrefix" in rec);
assert.ok(!("secret" in rec));
assert.ok(!("tokenHash" in rec));
assert.ok(!("token_hash" in rec));
}
});
+116
View File
@@ -0,0 +1,116 @@
import { after, beforeEach, describe, it } 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-account-cap-"));
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 { updateProviderConnectionSchema } = await import("../../src/shared/validation/schemas.ts");
type Connection = Awaited<ReturnType<typeof providersDb.createProviderConnection>>;
type StoredConnection = Awaited<ReturnType<typeof providersDb.getProviderConnectionById>>;
function assertConnection(connection: Connection): asserts connection is NonNullable<Connection> {
assert.ok(connection);
}
function assertStoredConnection(
connection: StoredConnection
): asserts connection is NonNullable<StoredConnection> {
assert.ok(connection);
}
function getConnectionId(connection: NonNullable<Connection>): string {
assert.equal(typeof connection.id, "string");
return connection.id as string;
}
async function resetStorage() {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}
async function createConnection(maxConcurrent: number | null): Promise<Connection> {
return providersDb.createProviderConnection({
provider: "openai",
authType: "apikey",
name: `openai-${String(maxConcurrent)}-${Math.random().toString(16).slice(2, 8)}`,
apiKey: "sk-test",
maxConcurrent,
});
}
beforeEach(async () => {
await resetStorage();
});
after(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
describe("maxConcurrent DB round-trip", () => {
it("stores and retrieves maxConcurrent=3", async () => {
const created = await createConnection(3);
assertConnection(created);
const connectionId = getConnectionId(created);
const stored = await providersDb.getProviderConnectionById(connectionId);
assertStoredConnection(stored);
assert.equal(stored.maxConcurrent, 3);
});
it("stores and retrieves maxConcurrent=null", async () => {
const created = await createConnection(null);
assertConnection(created);
const connectionId = getConnectionId(created);
const stored = await providersDb.getProviderConnectionById(connectionId);
assertStoredConnection(stored);
assert.equal(stored.maxConcurrent, null);
});
it("stores and retrieves maxConcurrent=0", async () => {
const created = await createConnection(3);
assertConnection(created);
const connectionId = getConnectionId(created);
const updated = await providersDb.updateProviderConnection(connectionId, { maxConcurrent: 0 });
assertConnection(updated);
assert.equal(updated.maxConcurrent, 0);
const stored = await providersDb.getProviderConnectionById(connectionId);
assertStoredConnection(stored);
assert.equal(stored.maxConcurrent, 0);
});
});
describe("maxConcurrent validation", () => {
it("rejects negative values", () => {
const result = updateProviderConnectionSchema.safeParse({ maxConcurrent: -1 });
assert.equal(result.success, false);
});
it("accepts positive integers", () => {
const result = updateProviderConnectionSchema.safeParse({ maxConcurrent: 3 });
assert.equal(result.success, true);
assert.equal(result.data.maxConcurrent, 3);
});
it("accepts null/undefined as unlimited", () => {
const nullResult = updateProviderConnectionSchema.safeParse({ maxConcurrent: null });
const undefinedResult = updateProviderConnectionSchema.safeParse({ name: "unchanged" });
assert.equal(nullResult.success, true);
assert.equal(nullResult.data.maxConcurrent, null);
assert.equal(undefinedResult.success, true);
assert.equal(undefinedResult.data.maxConcurrent, undefined);
});
});
@@ -0,0 +1,117 @@
/**
* Issue #2321 — Anthropic OAuth (Claude Pro/Team) 429 responses with phrases
* like "Usage Limit Reached" must be classified as QUOTA_EXHAUSTED with a
* long cooldown, not as a generic RATE_LIMIT_EXCEEDED with a ~5s base
* cooldown. Without this fix all Pro accounts on the same subscription
* cascade into a tight retry loop until the 5h quota window resets.
*/
import test from "node:test";
import assert from "node:assert/strict";
const { classifyErrorText, parseRetryFromErrorText, checkFallbackError, getProviderProfile } =
await import("../../open-sse/services/accountFallback.ts");
const { RateLimitReason } = await import("../../open-sse/config/constants.ts");
test("#2321 classifyErrorText flags 'Usage Limit Reached' as QUOTA_EXHAUSTED", () => {
const out = classifyErrorText("Usage Limit Reached. Please wait until 10:00 AM");
assert.equal(out, RateLimitReason.QUOTA_EXHAUSTED);
});
test("#2321 classifyErrorText flags 'Claude Pro usage limit reached' as QUOTA_EXHAUSTED", () => {
const out = classifyErrorText("Claude Pro usage limit reached.");
assert.equal(out, RateLimitReason.QUOTA_EXHAUSTED);
});
test("#2321 classifyErrorText flags possessive 'you've reached your usage limit'", () => {
const out = classifyErrorText("Sorry — you've reached your usage limit for this model.");
assert.equal(out, RateLimitReason.QUOTA_EXHAUSTED);
});
test("#2321 classifyErrorText still returns RATE_LIMIT_EXCEEDED for generic rate_limit", () => {
// Regression guard: short-term rate limits (per-minute TPM) must remain
// transient so we don't lock accounts for an hour after a normal burst.
const out = classifyErrorText("rate_limit_exceeded: too many requests");
assert.equal(out, RateLimitReason.RATE_LIMIT_EXCEEDED);
});
test("#2321 parseRetryFromErrorText extracts an ISO timestamp", () => {
const future = new Date(Date.now() + 60 * 60 * 1000).toISOString();
const ms = parseRetryFromErrorText(`Usage Limit Reached. Try again at ${future}`);
assert.ok(ms !== null, "expected non-null wait time");
assert.ok(ms! > 30 * 60 * 1000 && ms! <= 60 * 60 * 1000, `expected ~1h wait, got ${ms}ms`);
});
test("#2321 parseRetryFromErrorText ignores past ISO timestamps", () => {
const past = "2020-01-01T00:00:00Z";
const ms = parseRetryFromErrorText(`Try again at ${past}`);
assert.equal(ms, null);
});
test("#2321 parseRetryFromErrorText still handles the 'reset after Xh' format (backward compat)", () => {
const ms = parseRetryFromErrorText("Your quota will reset after 1h30m");
assert.equal(ms, (60 + 30) * 60 * 1000);
});
test("#2321 checkFallbackError returns ~1h cooldown for OAuth 429 + Usage Limit Reached", () => {
const out = checkFallbackError(
429,
"Usage Limit Reached. Please wait until 5h.",
0,
null,
"claude" // OAuth provider
);
assert.equal(out.shouldFallback, true);
assert.equal(out.reason, RateLimitReason.QUOTA_EXHAUSTED);
// Either the 1h default OR an upstream retry hint — both are far above
// the ~5s base cooldown that caused the cascade.
assert.ok(
out.cooldownMs >= 5 * 60 * 1000,
`expected long cooldown (>=5min), got ${out.cooldownMs}ms`
);
});
test("#2321 checkFallbackError ignores ISO timestamp when upstream retry hints are disabled", () => {
const future = new Date(Date.now() + 45 * 60 * 1000).toISOString();
const out = checkFallbackError(
429,
`Claude Pro usage limit reached. Try again at ${future}`,
0,
null,
"claude"
);
assert.equal(out.reason, RateLimitReason.QUOTA_EXHAUSTED);
assert.equal(out.usedUpstreamRetryHint, false);
assert.equal(out.cooldownMs, 60 * 60 * 1000);
});
test("#2321 checkFallbackError honors ISO timestamp when upstream retry hints are enabled", () => {
const futureMs = 45 * 60 * 1000;
const future = new Date(Date.now() + futureMs).toISOString();
const profile = { ...getProviderProfile("claude"), useUpstreamRetryHints: true };
const out = checkFallbackError(
429,
`Claude Pro usage limit reached. Try again at ${future}`,
0,
null,
"claude",
null,
profile
);
assert.equal(out.reason, RateLimitReason.QUOTA_EXHAUSTED);
// Within ~30s of the requested wait time.
assert.ok(
Math.abs(out.cooldownMs - futureMs) < 30_000,
`expected ~${futureMs}ms cooldown, got ${out.cooldownMs}ms`
);
});
test("#2321 generic 429 without quota keyword still gets the short cooldown path", () => {
// Regression guard: plain rate-limit must NOT get the 1h cooldown.
const out = checkFallbackError(429, "rate_limit_exceeded", 0, null, "claude");
assert.equal(out.shouldFallback, true);
// Generic 429 keeps the short retryable path (< 5 min).
assert.ok(
out.cooldownMs < 5 * 60 * 1000,
`expected short cooldown (<5min), got ${out.cooldownMs}ms`
);
});
@@ -0,0 +1,32 @@
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);
});
@@ -0,0 +1,52 @@
/**
* Issue #2929 — Fire Pass (fpk_*) API keys incorrectly marked as unavailable
* when the models endpoint returns 403.
*
* Fireworks Fire Pass keys return `403 "Fire Pass API keys are not authorized
* for this route."` on /models while still serving chat. That route-restriction
* 403 used to fall into the api-key-403 branch (→ AUTH_ERROR retryable fallback)
* or the generic "all other errors" default (→ transient cooldown), either of
* which cools down / marks the connection unavailable.
*
* A route-restriction 403 must be treated as benign for connection health:
* shouldFallback=false, no cooldown.
*/
import test from "node:test";
import assert from "node:assert/strict";
const { checkFallbackError } = await import("../../open-sse/services/accountFallback.ts");
test("#2929 route-restriction 403 does NOT cool down the connection", () => {
const result = checkFallbackError(
403,
"Fire Pass API keys are not authorized for this route.",
0,
null,
"fireworks"
);
assert.equal(result.shouldFallback, false, "route-restriction 403 must not trigger fallback/cooldown");
assert.equal(result.cooldownMs, 0, "route-restriction 403 must not impose a cooldown");
});
test("#2929 a genuine api-key 403 still triggers fallback (no over-broadening)", () => {
// A 403 whose body does NOT indicate a route restriction must keep the
// existing behavior (auth-error / transient fallback), so real bad keys still
// get cooled down.
const result = checkFallbackError(403, "invalid api key", 0, null, "fireworks");
assert.equal(
result.shouldFallback,
true,
"a non-route-restriction 403 must still be treated as fallback-worthy"
);
});
test("#2929 case-insensitive match on the route-restriction phrase", () => {
const result = checkFallbackError(
403,
"ERROR: Not Authorized For This Route",
0,
null,
"fireworks"
);
assert.equal(result.shouldFallback, false);
});
File diff suppressed because it is too large Load Diff
+124
View File
@@ -0,0 +1,124 @@
import test from "node:test";
import assert from "node:assert/strict";
const { selectAccountP2C, selectAccount } =
await import("../../open-sse/services/accountSelector.ts");
// ─── selectAccountP2C ───────────────────────────────────────────────────────
test("selectAccountP2C: returns null for empty array", () => {
assert.equal(selectAccountP2C([]), null);
assert.equal(selectAccountP2C(null), null);
});
test("selectAccountP2C: returns single account", () => {
const acct = { id: "a1" };
assert.equal(selectAccountP2C([acct]), acct);
});
test("selectAccountP2C: returns one of the candidates", () => {
const accounts = [{ id: "a1" }, { id: "a2" }, { id: "a3" }];
const selected = selectAccountP2C(accounts);
assert.ok(accounts.includes(selected));
});
test("selectAccountP2C: prefers healthier account over many runs", () => {
// Account with error should be selected less often
const healthy = { id: "healthy" };
const degraded = { id: "degraded", error: true, rateLimited: true, backoffLevel: 3 };
const accounts = [healthy, degraded];
let healthyCount = 0;
for (let i = 0; i < 100; i++) {
const selected = selectAccountP2C(accounts);
if (selected.id === "healthy") healthyCount++;
}
// Should strongly prefer healthy account
assert.ok(healthyCount > 60, `Expected healthy to win >60/100 times, got ${healthyCount}`);
});
// ─── selectAccount strategies ───────────────────────────────────────────────
test("selectAccount: fill-first returns first", () => {
const accounts = [{ id: "first" }, { id: "second" }];
const { account } = selectAccount(accounts, "fill-first");
assert.equal(account.id, "first");
});
test("selectAccount: round-robin cycles", () => {
const accounts = [{ id: "a" }, { id: "b" }, { id: "c" }];
let state = {};
const results = [];
for (let i = 0; i < 6; i++) {
const { account, state: newState } = selectAccount(accounts, "round-robin", state);
results.push(account.id);
state = newState;
}
assert.deepEqual(results, ["a", "b", "c", "a", "b", "c"]);
});
test("selectAccount: random returns valid account", () => {
const accounts = [{ id: "x" }, { id: "y" }];
const { account } = selectAccount(accounts, "random");
assert.ok(accounts.includes(account));
});
test("selectAccount: p2c returns valid account", () => {
const accounts = [{ id: "p1" }, { id: "p2" }, { id: "p3" }];
const { account } = selectAccount(accounts, "p2c");
assert.ok(accounts.includes(account));
});
test("selectAccount: empty accounts returns null", () => {
const { account } = selectAccount([], "fill-first");
assert.equal(account, null);
});
// ─── Round-robin fallback scenario (Issue #340) ─────────────────────────────
test("selectAccount: round-robin with excludeConnectionId skips excluded", () => {
const accounts = [{ id: "a" }, { id: "b" }, { id: "c" }];
// Simulate: first request picks 'a', then it fails
// Second request should exclude 'a' and pick 'b'
let state = {};
// First request - no exclusion
const { account: acc1 } = selectAccount(accounts, "round-robin", state);
state = { lastIndex: 0 }; // Simulate 'a' was picked
// 'a' fails, exclude it
const { account: acc2 } = selectAccount(
accounts.filter((a) => a.id !== "a"),
"round-robin",
state
);
// Should pick 'b' or 'c', not 'a'
assert.notEqual(acc2.id, "a", "Should not pick excluded account");
assert.ok(["b", "c"].includes(acc2.id), "Should pick from remaining accounts");
});
test("selectAccount: round-robin respects state across calls", () => {
const accounts = [{ id: "a" }, { id: "b" }, { id: "c" }];
let state = { lastIndex: -1 };
// First call should pick index 0
const { account: acc1, state: state1 } = selectAccount(accounts, "round-robin", state);
assert.equal(acc1.id, "a");
assert.equal(state1.lastIndex, 0);
// Second call should pick index 1
const { account: acc2, state: state2 } = selectAccount(accounts, "round-robin", state1);
assert.equal(acc2.id, "b");
assert.equal(state2.lastIndex, 1);
// Third call should pick index 2
const { account: acc3, state: state3 } = selectAccount(accounts, "round-robin", state2);
assert.equal(acc3.id, "c");
assert.equal(state3.lastIndex, 2);
// Fourth call should wrap to index 0
const { account: acc4 } = selectAccount(accounts, "round-robin", state3);
assert.equal(acc4.id, "a");
});
+178
View File
@@ -0,0 +1,178 @@
import assert from "node:assert/strict";
import { afterEach, describe, it } from "node:test";
import {
acquire,
buildAccountSemaphoreKey,
getStats,
markBlocked,
reset,
resetAll,
} from "../../open-sse/services/accountSemaphore";
afterEach(() => {
resetAll();
});
describe("accountSemaphore", async () => {
it("queues requests beyond the account cap and drains on release", async () => {
const key = buildAccountSemaphoreKey({
provider: "alibaba",
accountKey: "acct-1",
});
const releaseA = await acquire(key, { maxConcurrency: 2, timeoutMs: 200 });
const releaseB = await acquire(key, { maxConcurrency: 2, timeoutMs: 200 });
const queued = acquire(key, { maxConcurrency: 2, timeoutMs: 200 });
await new Promise((resolve) => setTimeout(resolve, 10));
assert.deepEqual(getStats()[key], {
running: 2,
queued: 1,
maxConcurrency: 2,
blockedUntil: null,
});
releaseA();
const releaseC = await queued;
assert.deepEqual(getStats()[key], {
running: 2,
queued: 0,
maxConcurrency: 2,
blockedUntil: null,
});
releaseA();
releaseB();
releaseC();
await new Promise((resolve) => setTimeout(resolve, 10));
assert.equal(getStats()[key], undefined);
});
it("returns a no-op release when concurrency is bypassed", async () => {
const key = buildAccountSemaphoreKey({
provider: "alibaba",
accountKey: "acct-bypass",
});
const release = await acquire(key, { maxConcurrency: 0, timeoutMs: 50 });
assert.deepEqual(getStats(), {});
release();
await new Promise((resolve) => setTimeout(resolve, 10));
assert.equal(getStats()[key], undefined);
});
it("uses SEMAPHORE_TIMEOUT for timed out queued requests", async () => {
const key = buildAccountSemaphoreKey({
provider: "alibaba",
accountKey: "acct-timeout",
});
const releaseA = await acquire(key, { maxConcurrency: 1, timeoutMs: 200 });
const queued = acquire(key, { maxConcurrency: 1, timeoutMs: 200 });
const keepAlive = setTimeout(() => {}, 250);
try {
await queued;
assert.fail("Expected timeout error");
} catch (err: unknown) {
assert.ok(err instanceof Error);
const error = err as Error & { code?: string };
assert.equal(error.code, "SEMAPHORE_TIMEOUT");
} finally {
clearTimeout(keepAlive);
}
releaseA();
await new Promise((resolve) => setTimeout(resolve, 10));
assert.equal(getStats()[key], undefined);
});
it("keeps release idempotent for finally blocks", async () => {
const key = buildAccountSemaphoreKey({
provider: "alibaba",
accountKey: "acct-idempotent",
});
const releaseA = await acquire(key, { maxConcurrency: 1, timeoutMs: 200 });
// Simulate a finally block calling release twice
releaseA();
releaseA();
releaseA();
// The second acquire should succeed immediately (slot was released)
const releaseB = await acquire(key, { maxConcurrency: 1, timeoutMs: 200 });
assert.deepEqual(getStats()[key], {
running: 1,
queued: 0,
maxConcurrency: 1,
blockedUntil: null,
});
releaseB();
await new Promise((resolve) => setTimeout(resolve, 10));
assert.equal(getStats()[key], undefined);
});
it("supports temporary blocking and explicit reset hooks", async () => {
const key = buildAccountSemaphoreKey({
provider: "alibaba",
accountKey: "acct-blocked",
});
await acquire(key, { maxConcurrency: 1, timeoutMs: 200 });
assert.deepEqual(getStats()[key], {
running: 1,
queued: 0,
maxConcurrency: 1,
blockedUntil: null,
});
markBlocked(key, 50);
// Should block even though slot is available
const acquired = acquire(key, { maxConcurrency: 1, timeoutMs: 100 });
await new Promise((resolve) => setTimeout(resolve, 30));
// Should still be queued because the gate is blocked
const stats = getStats()[key];
assert.equal(stats.running, 1);
assert.equal(stats.queued, 1);
assert.equal(stats.maxConcurrency, 1);
assert.ok(stats.blockedUntil !== null, "blockedUntil should be set");
reset(key);
await assert.rejects(async () => {
await acquired;
});
});
it("preserves existing maxConcurrency when markBlocked is applied", async () => {
const key = buildAccountSemaphoreKey({
provider: "alibaba",
accountKey: "acct-preserve",
});
await acquire(key, { maxConcurrency: 2, timeoutMs: 200 });
markBlocked(key, 50);
const stats = getStats()[key];
assert.equal(stats.running, 1);
assert.equal(stats.queued, 0);
assert.equal(stats.maxConcurrency, 2);
assert.ok(stats.blockedUntil !== null, "blockedUntil should be set");
});
});
@@ -0,0 +1,44 @@
import test from "node:test";
import assert from "node:assert/strict";
// #4976 — A 400 response whose body carries rate-limit semantics (e.g. MiMoCode's
// "Detected high-frequency non-compliant requests from you.") was misclassified as a
// non-fallbackable generic 400, so MiMo-auto combo never failed over and the raw
// failure surfaced to Cline as `[502]: fetch failed`. checkFallbackError must now
// detect rate-limit text on a 400 and treat it as fallback-worthy
// (RATE_LIMIT_EXCEEDED, connection-cooldown scope) WITHOUT regressing the #2101
// malformed-400 infinite-loop guard.
const { checkFallbackError } = await import("../../open-sse/services/accountFallback.ts");
const { RateLimitReason } = await import("../../open-sse/config/constants.ts");
test("#4976 400 with rate-limit text (MiMoCode) → fallback with RATE_LIMIT_EXCEEDED", () => {
const res = checkFallbackError(
400,
"Detected high-frequency non-compliant requests from you.",
0,
null,
"mimocode"
);
assert.equal(res.shouldFallback, true);
assert.equal(res.reason, RateLimitReason.RATE_LIMIT_EXCEEDED);
});
test("#4976 400 with Chinese rate-limit text → fallback with RATE_LIMIT_EXCEEDED", () => {
const res = checkFallbackError(400, "检测到您的请求频率过高,请稍后再试", 0, null, "mimocode");
assert.equal(res.shouldFallback, true);
assert.equal(res.reason, RateLimitReason.RATE_LIMIT_EXCEEDED);
});
test("#4976 regression: a generic non-rate-limit 400 still does NOT fall over (#2101 guard)", () => {
const res = checkFallbackError(400, "Invalid JSON: unexpected token at position 12");
assert.equal(res.shouldFallback, false);
});
test("#4976 regression: a malformed 400 stays MODEL_CAPACITY, not reclassified as rate-limit", () => {
// Malformed-request detection must win over the new rate-limit text check so the
// #2101 infinite-loop guard (zero-cooldown MODEL_CAPACITY) is preserved.
const res = checkFallbackError(400, "messages must alternate between user and assistant");
assert.equal(res.shouldFallback, true);
assert.equal(res.reason, RateLimitReason.MODEL_CAPACITY);
});
+102
View File
@@ -0,0 +1,102 @@
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 { SignJWT } from "jose";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-acp-agents-route-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "acp-agents-route-api-key-secret";
const core = await import("../../src/lib/db/core.ts");
const localDb = await import("../../src/lib/localDb.ts");
const routeModule = await import("../../src/app/api/acp/agents/route.ts");
const ORIGINAL_INITIAL_PASSWORD = process.env.INITIAL_PASSWORD;
const ORIGINAL_JWT_SECRET = process.env.JWT_SECRET;
async function resetStorage() {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
delete process.env.INITIAL_PASSWORD;
delete process.env.JWT_SECRET;
}
async function createSessionToken() {
const secret = new TextEncoder().encode(process.env.JWT_SECRET);
return new SignJWT({ authenticated: true })
.setProtectedHeader({ alg: "HS256" })
.setIssuedAt()
.setExpirationTime("1h")
.sign(secret);
}
function makeRequest(method: string, body?: unknown, token?: string) {
return new Request("http://localhost/api/acp/agents", {
method,
headers: {
...(body ? { "content-type": "application/json" } : {}),
...(token ? { cookie: `auth_token=${token}` } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
}
test.beforeEach(async () => {
await resetStorage();
});
test.after(async () => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
if (ORIGINAL_INITIAL_PASSWORD === undefined) {
delete process.env.INITIAL_PASSWORD;
} else {
process.env.INITIAL_PASSWORD = ORIGINAL_INITIAL_PASSWORD;
}
if (ORIGINAL_JWT_SECRET === undefined) {
delete process.env.JWT_SECRET;
} else {
process.env.JWT_SECRET = ORIGINAL_JWT_SECRET;
}
});
test("GET /api/acp/agents requires authentication when login is enabled", async () => {
process.env.INITIAL_PASSWORD = "route-auth-required";
const response = await routeModule.GET(makeRequest("GET"));
const body = (await response.json()) as any;
assert.equal(response.status, 401);
assert.equal(body.error, "Unauthorized");
});
test("POST /api/acp/agents rejects unsafe version commands for authenticated sessions", async () => {
process.env.JWT_SECRET = "acp-agents-jwt-secret";
await localDb.updateSettings({ requireLogin: true, password: "hashed-password" });
const token = await createSessionToken();
const response = await routeModule.POST(
makeRequest(
"POST",
{
id: "custom-agent",
name: "Custom Agent",
binary: "/usr/local/bin/custom-agent",
versionCommand: "/usr/local/bin/custom-agent --version; touch /tmp/pwned",
providerAlias: "custom-agent",
spawnArgs: [],
protocol: "stdio",
},
token
)
);
const body = (await response.json()) as any;
assert.equal(response.status, 400);
assert.match(body.error, /Invalid versionCommand/i);
});
+42
View File
@@ -0,0 +1,42 @@
import test from "node:test";
import assert from "node:assert/strict";
const { resolveVersionProbe, shouldUseShellForVersionProbe } =
await import("../../src/lib/acp/registry.ts");
test("resolveVersionProbe parses quoted binary paths without shell semantics", () => {
const probe = resolveVersionProbe(
"/tmp/My Custom Agent",
'"/tmp/My Custom Agent" --version',
true
);
assert.deepEqual(probe, {
command: "/tmp/My Custom Agent",
args: ["--version"],
});
});
test("resolveVersionProbe rejects custom version commands that switch binaries", () => {
const probe = resolveVersionProbe("/tmp/custom-agent", 'bash -lc "id"', true);
assert.equal(probe, null);
});
test("resolveVersionProbe rejects shell metacharacters in version commands", () => {
const probe = resolveVersionProbe(
"/tmp/custom-agent",
"/tmp/custom-agent --version; touch /tmp/pwned",
true
);
assert.equal(probe, null);
});
test("shouldUseShellForVersionProbe preserves Windows npm wrapper detection", () => {
assert.equal(shouldUseShellForVersionProbe("codex", "win32"), true);
assert.equal(
shouldUseShellForVersionProbe("C:\\Users\\dev\\AppData\\Roaming\\npm\\qwen.cmd", "win32"),
true
);
assert.equal(shouldUseShellForVersionProbe("C:\\Tools\\claude.exe", "win32"), false);
assert.equal(shouldUseShellForVersionProbe("codex", "linux"), false);
});
@@ -0,0 +1,700 @@
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-active-stream-lifecycle-"));
process.env.DATA_DIR = TEST_DATA_DIR;
const core = await import("../../src/lib/db/core.ts");
const usageHistory = await import("../../src/lib/usage/usageHistory.ts");
const callLogs = await import("../../src/lib/usage/callLogs.ts");
// Captured stream chunks carry a per-chunk arrival-time prefix ("[HH:MM:SS.mmm] ")
// added by the request logger for streaming-latency observability (#5834). Strip it
// before comparing the raw chunk payload.
const stripChunkTs = (chunk: string): string => chunk.replace(/^\[\d{2}:\d{2}:\d{2}\.\d{3}\] /, "");
test.after(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
// ─── Helper: Simulates /api/logs/[id] API route logic ──────────────────────
//
// The production route uses 3-tier lookup:
// 1. DB (getCallLogById)
// 2. completedDetails in-memory cache (getCompletedDetails)
// 3. pendingById in-memory active requests (getPendingById)
//
// Each tier constructs the API response shape including pipelinePayloads.
function buildApiResponseFromPending(id: string): Record<string, unknown> | null {
const active = usageHistory.getPendingById().get(id);
if (!active) return null;
const pipelinePayloads: Record<string, unknown> = {
clientRequest: active.clientRequest ?? null,
providerRequest: active.providerRequest ?? null,
providerResponse: active.providerResponse ?? null,
clientResponse: active.clientResponse ?? null,
streamChunks: active.streamChunks ?? null,
};
return {
id: active.id,
timestamp: new Date(active.startedAt).toISOString(),
method: "",
path: active.clientEndpoint || "",
status: 0,
model: active.model,
provider: active.provider,
connectionId: active.connectionId,
duration: Date.now() - active.startedAt,
detailState: "in-flight",
active: true,
pipelinePayloads,
hasPipelineDetails: true,
};
}
function buildApiResponseFromCompleted(id: string): Record<string, unknown> | null {
const completed = usageHistory.getCompletedDetails();
const inMem = completed.get(id);
if (!inMem) return null;
const pipelinePayloads: Record<string, unknown> = {
clientRequest: inMem.clientRequest ?? null,
providerRequest: inMem.providerRequest ?? null,
providerResponse: inMem.providerResponse ?? null,
clientResponse: inMem.clientResponse ?? null,
streamChunks: inMem.streamChunks ?? null,
};
return {
id: inMem.id,
timestamp: new Date(inMem.startedAt).toISOString(),
path: inMem.clientEndpoint || "",
status: 0,
model: inMem.model,
provider: inMem.provider,
connectionId: inMem.connectionId,
duration: Date.now() - inMem.startedAt,
detailState: "in-memory",
active: false,
pipelinePayloads,
hasPipelineDetails: true,
};
}
// ─── Helper: Simulates frontend streamChunksText IIFE ──────────────────────
function computeStreamChunksText(
debugEnabled: boolean,
pipelinePayloads: Record<string, unknown> | null | undefined
): string | null {
if (!debugEnabled || !pipelinePayloads?.streamChunks) return null;
let chunks: unknown = pipelinePayloads.streamChunks;
if (typeof chunks === "string") {
try {
chunks = JSON.parse(chunks);
} catch {
return chunks;
}
}
if (chunks && typeof chunks === "object") {
try {
return Object.entries(chunks as Record<string, unknown>)
.map(([stage, arr]) => {
const joined = Array.isArray(arr) ? (arr as string[]).join("") : String(arr);
return `--- ${stage} ---\n${joined}`;
})
.join("\n\n");
} catch {
return JSON.stringify(chunks, null, 2);
}
}
return null;
}
// ─── Helper: Simulates frontend openDetail state merge ─────────────────────
function mergeDetailData(
prev: Record<string, unknown> | null,
data: Record<string, unknown>
): Record<string, unknown> {
const dataHasPipeline =
data?.pipelinePayloads && Object.keys(data.pipelinePayloads || {}).length > 0;
return {
...prev,
...data,
pipelinePayloads: dataHasPipeline ? data.pipelinePayloads : prev?.pipelinePayloads,
};
}
// ─── Test: Full lifecycle ──────────────────────────────────────────────────
test("streamChunks survive the full lifecycle: in-flight → completed → persisted", async () => {
usageHistory.clearPendingRequests();
const model = "gpt-4";
const provider = "openai";
const connectionId = "conn-lifecycle-1";
// ── Phase 1: Track a pending request ──
const requestId = usageHistory.trackPendingRequest(model, provider, connectionId, true, {
clientRequest: { messages: [{ role: "user", content: "hello" }] },
providerRequest: { model: "gpt-4" },
clientEndpoint: "/v1/chat/completions",
});
assert.ok(requestId, "trackPendingRequest should return a request ID");
assert.ok(
usageHistory.getPendingById().has(requestId),
"request ID should be in pendingById immediately"
);
// ── Phase 2: Simulate streaming — chunks arrive gradually ──
// Round 1: provider chunks
usageHistory.updatePendingRequestStreamChunks(model, provider, connectionId, {
provider: ['data: {"type":"message_start"}\n\n'],
openai: [],
client: [],
});
// Verify via pendingById (what the API reads)
const apiResponse1 = buildApiResponseFromPending(requestId);
assert.ok(apiResponse1, "API should find in-flight request");
assert.ok(apiResponse1!.pipelinePayloads, "should have pipelinePayloads");
assert.ok(
(apiResponse1!.pipelinePayloads as Record<string, unknown>).streamChunks,
"streamChunks should be present in API response"
);
// Simulate frontend streamChunksText
const text1 = computeStreamChunksText(
true,
apiResponse1!.pipelinePayloads as Record<string, unknown>
);
assert.ok(text1, "streamChunksText should be non-null with debugEnabled");
assert.ok(text1!.includes("message_start"), "streamChunksText should contain the chunk content");
// Verify frontend state merge
const initialDetail = mergeDetailData(null, apiResponse1 as unknown as Record<string, unknown>);
assert.ok(
(initialDetail.pipelinePayloads as Record<string, unknown>).streamChunks,
"streamChunks should survive the openDetail state merge"
);
const text1after = computeStreamChunksText(
true,
initialDetail.pipelinePayloads as Record<string, unknown>
);
assert.ok(text1after, "streamChunksText should work after state merge");
assert.ok(text1after!.includes("message_start"), "content preserved after state merge");
// Round 2: openai chunks arrive (simulating proxy relay)
usageHistory.updatePendingRequestStreamChunks(model, provider, connectionId, {
provider: ['data: {"type":"message_start"}\n\n'],
openai: ['data: {"choices":[{"delta":{"content":"Hello"}}]}\n\n'],
client: [],
});
// Round 3: client (converted) chunk arrives
usageHistory.updatePendingRequestStreamChunks(model, provider, connectionId, {
provider: ['data: {"type":"message_start"}\n\n'],
openai: ['data: {"choices":[{"delta":{"content":"Hello"}}]}\n\n'],
client: ['data: {"content":"Hello"}\n\n'],
});
// Verify API sees the latest (shows all stages now)
const apiResponse2 = buildApiResponseFromPending(requestId);
const streamChunks2 = (apiResponse2!.pipelinePayloads as Record<string, unknown>)
.streamChunks as Record<string, string[]>;
assert.equal(streamChunks2.provider.length, 1, "provider chunks: 1");
assert.equal(streamChunks2.openai.length, 1, "openai chunks: 1");
assert.equal(streamChunks2.client.length, 1, "client chunks: 1");
// Verify frontend text includes all 3 stages
const text2 = computeStreamChunksText(
true,
apiResponse2!.pipelinePayloads as Record<string, unknown>
);
assert.ok(text2!.includes("--- provider ---"), "should include provider stage");
assert.ok(text2!.includes("--- openai ---"), "should include openai stage");
assert.ok(text2!.includes("--- client ---"), "should include client stage");
// Verify debugEnabled=false hides the stream
const textHidden = computeStreamChunksText(
false,
apiResponse2!.pipelinePayloads as Record<string, unknown>
);
assert.equal(textHidden, null, "streamChunksText should be null when debugEnabled=false");
// Verify null pipelinePayloads hides the stream
const textNoPayload = computeStreamChunksText(true, null);
assert.equal(textNoPayload, null, "streamChunksText should be null without pipelinePayloads");
const textNoChunks = computeStreamChunksText(true, {});
assert.equal(textNoChunks, null, "streamChunksText should be null without streamChunks key");
// ── Phase 3: Simulate request completion ──
usageHistory.finalizeMostRecentPendingRequest(model, provider, connectionId, {
status: 200,
model,
provider,
clientResponse: { choices: [{ message: { content: "Hello" } }] },
});
// The request should be in completedDetails now
assert.ok(
!usageHistory.getPendingById().has(requestId),
"request should be removed from pendingById after finalization"
);
const completedResponse = buildApiResponseFromCompleted(requestId);
assert.ok(completedResponse, "API should find request in completedDetails");
assert.ok(
(completedResponse!.pipelinePayloads as Record<string, unknown>).streamChunks,
"streamChunks should be in completedDetails"
);
// Verify frontend can render from completed response
const completedText = computeStreamChunksText(
true,
completedResponse!.pipelinePayloads as Record<string, unknown>
);
assert.ok(completedText, "streamChunksText should work from completed data");
assert.ok(completedText!.includes("Hello"), "content preserved after completion");
// ── Phase 4: Persist to DB (simulates saveCallLog) ──
await callLogs.saveCallLog({
id: requestId,
method: "POST",
path: "/v1/chat/completions",
status: 200,
model,
requestedModel: model,
provider,
connectionId,
duration: 5000,
tokens: { in: 10, out: 20 },
requestBody: { messages: [{ role: "user", content: "hello" }] },
responseBody: { choices: [{ message: { content: "Hello" } }] },
error: null,
sourceFormat: "openai",
targetFormat: "openai",
comboName: null,
comboStepId: null,
comboExecutionKey: null,
tokensCompressed: null,
cacheSource: "upstream",
apiKeyId: null,
apiKeyName: null,
noLog: false,
pipelinePayloads: completedResponse!.pipelinePayloads as Record<string, unknown>,
});
// Verify DB has the entry
const dbEntry = await callLogs.getCallLogById(requestId);
assert.ok(dbEntry, "should find persisted call log by the same ID");
assert.ok(dbEntry.pipelinePayloads, "persisted entry should have pipelinePayloads");
// The pipelinePayloads in the DB may have been compacted/truncated.
// streamChunks may or may not be there depending on captureStreamChunks,
// but the ID must match so the API can find it.
assert.equal(
(dbEntry as Record<string, unknown>).id,
requestId,
"DB entry ID should match the original request ID"
);
});
test("streamChunksText renders progressive updates correctly", () => {
// Simulate the scenario where streamChunks grows between polls
// Poll 1: first chunk arrives
const payload1 = {
streamChunks: { provider: ['data: {"content":"A"}\n\n'], openai: [], client: [] },
};
const text1 = computeStreamChunksText(true, payload1);
assert.ok(text1, "poll 1 should produce text");
assert.ok(text1!.includes("A"), "poll 1 should show chunk A");
// Poll 2: second chunk appended
const payload2 = {
streamChunks: {
provider: ['data: {"content":"A"}\n\n', 'data: {"content":"B"}\n\n'],
openai: [],
client: [],
},
};
const text2 = computeStreamChunksText(true, payload2);
assert.ok(text2!.includes("A"), "poll 2 should still show chunk A");
assert.ok(text2!.includes("B"), "poll 2 should show newly arrived chunk B");
// The joined text should be the concatenation
assert.ok(
text2!.includes('data: {"content":"A"}\n\ndata: {"content":"B"}'),
"poll 2 joined text should contain both chunks"
);
});
test("pooling effect state merge preserves streamChunks across updates", () => {
// Simulate the polling useEffect state merge:
// setDetailData(prev => ({...prev, ...data, pipelinePayloads: data?.pipelinePayloads || prev?.pipelinePayloads}))
let detailData: Record<string, unknown> | null = null;
// openDetail initial fetch
const initialFetch = {
pipelinePayloads: {
streamChunks: { provider: ['data: {"a":1}\n\n'] },
providerRequest: { model: "gpt-4" },
},
active: true,
detailState: "in-flight",
};
detailData = mergeDetailData(null, initialFetch);
assert.ok(
(detailData!.pipelinePayloads as Record<string, unknown>).streamChunks,
"initial merge should preserve streamChunks"
);
// Poll response adds more chunks
const pollResponse = {
pipelinePayloads: {
streamChunks: {
provider: ['data: {"a":1}\n\n', 'data: {"b":2}\n\n'],
},
providerRequest: { model: "gpt-4" },
},
active: true,
detailState: "in-flight",
};
detailData = mergeDetailData(detailData, pollResponse);
const mergedChunks = (detailData!.pipelinePayloads as Record<string, unknown>)
.streamChunks as Record<string, string[]>;
assert.equal(mergedChunks.provider.length, 2, "merged should have 2 chunks");
assert.equal(mergedChunks.provider[1], 'data: {"b":2}\n\n', "merged should include new chunk");
// Simulate: polling response loses pipelinePayloads (null)
// This should NOT overwrite the existing streamChunks
const nullPayloadResponse = {
pipelinePayloads: null,
active: true,
detailState: "in-flight",
};
const afterNullPayload = mergeDetailData(detailData, nullPayloadResponse);
assert.ok(
(afterNullPayload.pipelinePayloads as Record<string, unknown>).streamChunks,
"streamChunks should survive null pipelinePayloads update"
);
// Simulate: polling response has pipelinePayloads but NO streamChunks
// This SHOULD overwrite (|| semantics) — but the production condition is: when
// data?.pipelinePayloads is truthy, it replaces prev. If captureStreamChunks was false,
// the data won't have streamChunks. This is expected behavior — the frontend doesn't
// try to retain old streamChunks when new data explicitly lacks them.
const noChunksPayloadResponse = {
pipelinePayloads: {
providerResponse: { status: 200 },
// no streamChunks key
},
active: false,
detailState: "ready",
};
const afterNoChunks = mergeDetailData(detailData, noChunksPayloadResponse);
const payloadAfter = afterNoChunks.pipelinePayloads as Record<string, unknown>;
assert.equal(
payloadAfter.streamChunks,
undefined,
"streamChunks is lost when new pipelinePayloads lacks it and is truthy"
);
// This demonstrates the || semantics: data.pipelinePayloads is truthy ({providerResponse: {...}})
// so it replaces prev.pipelinePayloads even though it lacks streamChunks.
// The Event Stream section will not render after this update.
// However, in practice this only happens after request completion when the polling
// transitions to the persisted artifact which may have truncated streamChunks.
// By that point the Event Stream section is no longer needed (streaming is done).
});
test("pendingById references are live: push mutates the shared arrays visible to API", () => {
usageHistory.clearPendingRequests();
const model = "gpt-4";
const provider = "openai";
const connectionId = "conn-ref-1";
const requestId = usageHistory.trackPendingRequest(model, provider, connectionId, true);
// Simulate push() — store initial empty arrays
usageHistory.updatePendingRequestStreamChunks(model, provider, connectionId, {
provider: [],
openai: [],
client: [],
});
// Get the pending detail reference
const detailFromPending = usageHistory.getPendingById().get(requestId);
assert.ok(detailFromPending, "detail should be in pendingById");
assert.ok(detailFromPending!.streamChunks, "streamChunks should be set to wrapper object");
// Simulate appendBoundedChunk — pushes to the shared array
detailFromPending!.streamChunks!.provider.push('data: {"chunk":"live"}');
// Re-read from pendingById — should see the mutation
const detailReRead = usageHistory.getPendingById().get(requestId);
assert.equal(
detailReRead!.streamChunks!.provider.length,
1,
"mutation should be visible through pendingById"
);
assert.equal(
detailReRead!.streamChunks!.provider[0],
'data: {"chunk":"live"}',
"mutated content should be visible through pendingById"
);
// Verify via simulated API response
const apiResp = buildApiResponseFromPending(requestId);
const apiChunks = (apiResp!.pipelinePayloads as Record<string, unknown>).streamChunks as Record<
string,
string[]
>;
assert.equal(apiChunks.provider.length, 1, "API should see the live mutation");
});
test("no connectionId in request logger does not break anything", async () => {
const { createRequestLogger } = await import("../../open-sse/utils/requestLogger.ts");
usageHistory.clearPendingRequests();
usageHistory.trackPendingRequest("gpt-4", "openai", "conn-noop-1", true);
// Logger without connectionId/model — push() bails, no crash
const logger = await createRequestLogger("openai", "openai", "gpt-4", {
enabled: true,
captureStreamChunks: true,
});
logger.appendProviderChunk('data: {"a":1}');
logger.appendOpenAIChunk('data: {"b":2}');
logger.appendConvertedChunk('data: {"c":3}');
// The pending detail should NOT have streamChunks (no connectionId match)
const pending = usageHistory.getPendingRequests();
const detail = pending.details["conn-noop-1"]?.["gpt-4 (openai)"]?.[0];
assert.equal(detail.streamChunks, undefined, "streamChunks should not be set");
// Simulated API should return null for streamChunks
const apiResp = buildApiResponseFromPending(
// We need the actual ID. trackPendingRequest returns it now.
(() => {
usageHistory.clearPendingRequests();
const id = usageHistory.trackPendingRequest("gpt-4", "openai", "conn-noop-2", true);
return id!;
})()
);
// This request had no streaming, so streamChunks is null
if (apiResp) {
const noChunks = computeStreamChunksText(
true,
apiResp.pipelinePayloads as Record<string, unknown>
);
assert.equal(noChunks, null, "no streamChunks means no event stream");
}
});
test("createRequestLogger and trackPendingRequest with matching model propagate streamChunks", async () => {
// The fix: chatCore.ts now passes `model` (not `effectiveModel`) to
// createRequestLogger, so the modelKey matches what trackPendingRequest uses.
const { createRequestLogger } = await import("../../open-sse/utils/requestLogger.ts");
usageHistory.clearPendingRequests();
const model = "gpt-4";
const provider = "openai";
const connectionId = "conn-match-1";
const requestId = usageHistory.trackPendingRequest(model, provider, connectionId, true, {
clientRequest: { messages: [] },
});
// Use matching model (the fix)
const logger = await createRequestLogger("openai", "openai", model, {
enabled: true,
captureStreamChunks: true,
model,
provider,
connectionId,
});
logger.appendProviderChunk('data: {"chunk":"hello"}');
logger.appendOpenAIChunk('data: {"choices":[{"delta":{"content":"hi"}}]}');
logger.appendConvertedChunk('data: {"content":"world"}');
const detail = usageHistory.getPendingById().get(requestId);
assert.ok(detail, "pending detail should exist");
assert.ok(detail!.streamChunks, "streamChunks should be set");
assert.equal(detail!.streamChunks!.provider.length, 1);
assert.equal(detail!.streamChunks!.openai.length, 1);
assert.equal(detail!.streamChunks!.client.length, 1);
const apiResp = buildApiResponseFromPending(requestId!);
assert.ok(apiResp);
const apiChunks = (apiResp!.pipelinePayloads as Record<string, unknown>).streamChunks as Record<
string,
string[]
>;
assert.equal(stripChunkTs(apiChunks?.provider[0]), 'data: {"chunk":"hello"}');
});
test("finalizePendingRequestById completes the exact stream when same model requests overlap", () => {
usageHistory.clearPendingRequests();
const model = "gpt-4";
const provider = "openai";
const connectionId = "conn-overlap-1";
const firstId = usageHistory.trackPendingRequest(model, provider, connectionId, true, {
clientRequest: { messages: [{ role: "user", content: "first" }] },
});
const secondId = usageHistory.trackPendingRequest(model, provider, connectionId, true, {
clientRequest: { messages: [{ role: "user", content: "second" }] },
});
const completed = usageHistory.finalizePendingRequestById(firstId, {
providerResponse: { id: "first-response" },
clientResponse: { id: "first-response" },
});
assert.equal(completed, true);
assert.ok(usageHistory.getCompletedDetails().has(firstId));
assert.equal(usageHistory.getCompletedDetails().has(secondId), false);
assert.equal(usageHistory.getPendingById().has(firstId), false);
assert.equal(usageHistory.getPendingById().has(secondId), true);
const pending = usageHistory.getPendingRequests();
const details = pending.details[connectionId]?.[`${model} (${provider})`] ?? [];
assert.equal(details.length, 1);
assert.equal(details[0].id, secondId);
assert.equal(pending.byModel[`${model} (${provider})`], 1);
assert.equal(pending.byAccount[connectionId]?.[`${model} (${provider})`], 1);
});
test("completedDetails cache evicts oldest entries when bounded", () => {
usageHistory.clearPendingRequests();
const model = "gpt-4";
const provider = "openai";
const connectionId = "conn-completed-bound";
const ids: string[] = [];
for (let i = 0; i < 260; i++) {
const id = usageHistory.trackPendingRequest(model, provider, connectionId, true);
ids.push(id!);
const completed = usageHistory.finalizePendingRequestById(id, {
clientResponse: { choices: [{ message: { content: `done ${i}` } }] },
});
assert.equal(completed, true);
}
assert.ok(
usageHistory.getCompletedDetails().size <= 256,
"completedDetails should remain bounded"
);
assert.equal(usageHistory.getCompletedDetails().has(ids[0]), false);
assert.equal(usageHistory.getCompletedDetails().has(ids[ids.length - 1]), true);
});
test("streamChunks in completedDetails survives beyond the logs polling window", async () => {
usageHistory.clearPendingRequests();
const model = "gpt-4";
const provider = "openai";
const connectionId = "conn-ttl-1";
const requestId = usageHistory.trackPendingRequest(model, provider, connectionId, true, {
clientRequest: { messages: [] },
});
// Simulate streaming then completion
usageHistory.updatePendingRequestStreamChunks(model, provider, connectionId, {
provider: ['data: {"chunk":"final"}\n\n'],
openai: [],
client: [],
});
usageHistory.finalizeMostRecentPendingRequest(model, provider, connectionId, {
status: 200,
model,
provider,
});
// Should be in completedDetails
assert.ok(
usageHistory.getCompletedDetails().has(requestId),
"should be in completedDetails after finalization"
);
const completedResp = buildApiResponseFromCompleted(requestId);
assert.ok(completedResp, "API response should be available from completedDetails");
assert.ok(
(completedResp!.pipelinePayloads as Record<string, unknown>).streamChunks,
"streamChunks should be in completedDetails"
);
// Save to DB as the normal durable path, but keep the completedDetails cache
// long enough that a slow Logs-page poll does not see the row disappear
// between pending removal and DB/detail refresh.
await callLogs.saveCallLog({
id: requestId,
method: "POST",
path: "/v1/chat/completions",
status: 200,
model,
requestedModel: model,
provider,
connectionId,
duration: 3000,
tokens: { in: 5, out: 10 },
requestBody: {},
responseBody: {},
error: null,
sourceFormat: "openai",
targetFormat: "openai",
comboName: null,
comboStepId: null,
comboExecutionKey: null,
tokensCompressed: null,
cacheSource: "upstream",
apiKeyId: null,
apiKeyName: null,
noLog: false,
pipelinePayloads: completedResp!.pipelinePayloads as Record<string, unknown>,
});
// Verify DB has it
const dbEntry1 = await callLogs.getCallLogById(requestId);
assert.ok(dbEntry1, "DB should have the entry after saveCallLog");
// This used to expire after 5 seconds. Keep it visible beyond that window so
// a slow client poll can still resolve the completed row/details.
await new Promise((r) => setTimeout(r, 5100));
assert.ok(
usageHistory.getCompletedDetails().has(requestId),
"completedDetails should still be available after a 5-second polling gap"
);
const dbEntry2 = await callLogs.getCallLogById(requestId);
assert.ok(dbEntry2, "DB should still have the entry after the polling gap");
assert.equal(
(dbEntry2 as Record<string, unknown>).id,
requestId,
"DB entry ID should match the original request ID"
);
});
+202
View File
@@ -0,0 +1,202 @@
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 { makeManagementSessionRequest } from "../helpers/managementSession.ts";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-admin-audit-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.APP_LOG_TO_FILE = "false";
process.env.JWT_SECRET = "test-jwt-secret-for-audit-events";
process.env.INITIAL_PASSWORD = "admin-secret";
const core = await import("../../src/lib/db/core.ts");
const compliance = await import("../../src/lib/compliance/index.ts");
const loginRoute = await import("../../src/app/api/auth/login/route.ts");
const logoutRoute = await import("../../src/app/api/auth/logout/route.ts");
const providersRoute = await import("../../src/app/api/providers/route.ts");
const providerByIdRoute = await import("../../src/app/api/providers/[id]/route.ts");
const originalGetLoginCookieStore = loginRoute.authRouteInternals.getCookieStore;
const originalGetLogoutCookieStore = logoutRoute.logoutRouteInternals.getCookieStore;
function resetDb() {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}
test.beforeEach(() => {
resetDb();
});
test.afterEach(() => {
loginRoute.authRouteInternals.getCookieStore = originalGetLoginCookieStore;
logoutRoute.logoutRouteInternals.getCookieStore = originalGetLogoutCookieStore;
});
test.after(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
test("auth login/logout routes emit structured audit events with ip and request id", async () => {
const setCalls = [];
const deleteCalls = [];
loginRoute.authRouteInternals.getCookieStore = async () => ({
set: (...args) => setCalls.push(args),
});
logoutRoute.logoutRouteInternals.getCookieStore = async () => ({
delete: (...args) => deleteCalls.push(args),
});
const loginResponse = await loginRoute.POST(
new Request("http://localhost/api/auth/login", {
method: "POST",
headers: {
"content-type": "application/json",
"x-forwarded-for": "198.51.100.10",
"x-request-id": "req-auth-login",
},
body: JSON.stringify({ password: "admin-secret" }),
})
);
assert.equal(loginResponse.status, 200);
assert.deepEqual(await loginResponse.json(), { success: true });
assert.equal(setCalls.length, 1);
const logoutResponse = await logoutRoute.POST(
new Request("http://localhost/api/auth/logout", {
method: "POST",
headers: {
"x-forwarded-for": "198.51.100.10",
"x-request-id": "req-auth-logout",
},
})
);
assert.equal(logoutResponse.status, 200);
assert.deepEqual(await logoutResponse.json(), { success: true });
assert.deepEqual(deleteCalls, [["auth_token"]]);
const loginEvent = compliance.getAuditLog({ action: "auth.login.success" })[0];
assert.equal(loginEvent.actor, "admin");
assert.equal(loginEvent.resourceType, "auth_session");
assert.equal(loginEvent.status, "success");
assert.equal(loginEvent.ip, "198.51.100.10");
assert.equal(loginEvent.requestId, "req-auth-login");
const logoutEvent = compliance.getAuditLog({ action: "auth.logout.success" })[0];
assert.equal(logoutEvent.actor, "admin");
assert.equal(logoutEvent.resourceType, "auth_session");
assert.equal(logoutEvent.status, "success");
assert.equal(logoutEvent.requestId, "req-auth-logout");
});
test("auth login route records failed password attempts", async () => {
loginRoute.authRouteInternals.getCookieStore = async () => ({
set() {},
});
const response = await loginRoute.POST(
new Request("http://localhost/api/auth/login", {
method: "POST",
headers: {
"content-type": "application/json",
"x-forwarded-for": "198.51.100.22",
"x-request-id": "req-auth-failed",
},
body: JSON.stringify({ password: "wrong-password" }),
})
);
assert.equal(response.status, 401);
assert.deepEqual(await response.json(), { error: "Invalid password" });
const event = compliance.getAuditLog({ action: "auth.login.failed" })[0];
assert.equal(event.actor, "anonymous");
assert.equal(event.status, "failed");
assert.equal(event.requestId, "req-auth-failed");
assert.deepEqual(event.metadata, { reason: "invalid_password", lockedOut: false });
});
test("provider create/update/delete routes emit sanitized credential audit events", async () => {
const createResponse = await providersRoute.POST(
await makeManagementSessionRequest("http://localhost/api/providers", {
method: "POST",
headers: {
"x-forwarded-for": "203.0.113.10",
"x-request-id": "req-provider-create",
},
body: {
provider: "openai",
apiKey: "sk-secret-provider-key",
name: "Primary OpenAI",
defaultModel: "gpt-4o-mini",
},
})
);
assert.equal(createResponse.status, 201);
const createBody = (await createResponse.json()) as any;
const connectionId = createBody.connection.id;
assert.equal(typeof connectionId, "string");
const updateResponse = await providerByIdRoute.PUT(
await makeManagementSessionRequest(`http://localhost/api/providers/${connectionId}`, {
method: "PUT",
headers: {
"x-forwarded-for": "203.0.113.10",
"x-request-id": "req-provider-update",
},
body: {
name: "Primary OpenAI Updated",
defaultModel: "gpt-4.1-mini",
isActive: false,
},
}),
{ params: Promise.resolve({ id: connectionId }) }
);
assert.equal(updateResponse.status, 200);
const deleteResponse = await providerByIdRoute.DELETE(
await makeManagementSessionRequest(`http://localhost/api/providers/${connectionId}`, {
method: "DELETE",
headers: {
"x-forwarded-for": "203.0.113.10",
"x-request-id": "req-provider-delete",
},
}),
{ params: Promise.resolve({ id: connectionId }) }
);
assert.equal(deleteResponse.status, 200);
const createdEvent = compliance.getAuditLog({ action: "provider.credentials.created" })[0];
assert.equal(createdEvent.status, "success");
assert.equal(createdEvent.resourceType, "provider_credentials");
assert.equal(createdEvent.requestId, "req-provider-create");
assert.equal(createdEvent.target, "openai:Primary OpenAI");
assert.equal("apiKey" in (createdEvent.metadata as any).connection, false);
const updatedEvent = compliance.getAuditLog({ action: "provider.credentials.updated" })[0];
assert.equal(updatedEvent.requestId, "req-provider-update");
assert.deepEqual((updatedEvent as any).metadata.changedFields.sort(), [
"defaultModel",
"isActive",
"name",
]);
assert.equal((updatedEvent as any).metadata.before.name, "Primary OpenAI");
(assert as any).equal((updatedEvent.metadata as any).after.name, "Primary OpenAI Updated");
(assert as any).equal("apiKey" in (updatedEvent.metadata as any).before, false);
(assert as any).equal("apiKey" in (updatedEvent.metadata as any).after, false);
const revokedEvent = compliance.getAuditLog({ action: "provider.credentials.revoked" })[0];
assert.equal(revokedEvent.requestId, "req-provider-delete");
assert.equal(revokedEvent.target, "openai:Primary OpenAI Updated");
assert.equal(revokedEvent.status, "success");
assert.equal("apiKey" in (revokedEvent.metadata as any).connection, false);
});
+310
View File
@@ -0,0 +1,310 @@
import test from "node:test";
import assert from "node:assert/strict";
// ═════════════════════════════════════════════════════
// FASE-07/08/09: UX, LLM Advanced, E2E Hardening Tests
// ═════════════════════════════════════════════════════
// ─── Policy Engine Tests ──────────────────────────
import { PolicyEngine } from "../../src/domain/policyEngine.ts";
test("PolicyEngine: evaluates empty policy list as allowed", () => {
const engine = new PolicyEngine();
const result = engine.evaluate({ model: "gpt-4" });
assert.equal(result.allowed, true);
assert.equal(result.appliedPolicies.length, 0);
});
test("PolicyEngine: matches model glob patterns", () => {
const engine = new PolicyEngine();
engine.loadPolicies([
{
id: "1",
name: "prefer-openai-for-gpt",
type: "routing",
enabled: true,
priority: 1,
conditions: { model_pattern: "gpt-*" },
actions: { prefer_provider: ["openai"] },
},
]);
const result = engine.evaluate({ model: "gpt-4o" });
assert.equal(result.allowed, true);
assert.deepEqual(result.preferredProviders, ["openai"]);
assert.deepEqual(result.appliedPolicies, ["prefer-openai-for-gpt"]);
});
test("PolicyEngine: does not match non-matching models", () => {
const engine = new PolicyEngine();
engine.loadPolicies([
{
id: "1",
name: "gpt-only",
type: "routing",
enabled: true,
priority: 1,
conditions: { model_pattern: "gpt-*" },
actions: { prefer_provider: ["openai"] },
},
]);
const result = engine.evaluate({ model: "claude-3.5-sonnet" });
assert.equal(result.preferredProviders.length, 0);
});
test("PolicyEngine: blocks models via access policy", () => {
const engine = new PolicyEngine();
engine.loadPolicies([
{
id: "2",
name: "block-expensive",
type: "access",
enabled: true,
priority: 1,
conditions: {},
actions: { block_model: ["gpt-4*", "claude-3-opus*"] },
},
]);
const blocked = engine.evaluate({ model: "gpt-4o" });
assert.equal(blocked.allowed, false);
assert.ok(blocked.reason.includes("blocked"));
const allowed = engine.evaluate({ model: "gpt-3.5-turbo" });
assert.equal(allowed.allowed, true);
});
test("PolicyEngine: applies max_tokens from budget policy", () => {
const engine = new PolicyEngine();
engine.loadPolicies([
{
id: "3",
name: "limit-tokens",
type: "budget",
enabled: true,
priority: 1,
conditions: {},
actions: { max_tokens: 4096 },
},
]);
const result = engine.evaluate({ model: "gpt-4" });
assert.equal(result.maxTokens, 4096);
});
test("PolicyEngine: skips disabled policies", () => {
const engine = new PolicyEngine();
engine.loadPolicies([
{
id: "4",
name: "disabled-policy",
type: "routing",
enabled: false,
priority: 1,
conditions: {},
actions: { prefer_provider: ["should-not-appear"] },
},
]);
const result = engine.evaluate({ model: "gpt-4" });
assert.equal(result.preferredProviders.length, 0);
});
test("PolicyEngine: respects priority order", () => {
const engine = new PolicyEngine();
engine.loadPolicies([
{
id: "b",
name: "second",
type: "routing",
enabled: true,
priority: 10,
conditions: {},
actions: { prefer_provider: ["provider-b"] },
},
{
id: "a",
name: "first",
type: "routing",
enabled: true,
priority: 1,
conditions: {},
actions: { prefer_provider: ["provider-a"] },
},
]);
const result = engine.evaluate({ model: "any" });
assert.deepEqual(result.preferredProviders, ["provider-a", "provider-b"]);
assert.deepEqual(result.appliedPolicies, ["first", "second"]);
});
test("PolicyEngine: addPolicy and removePolicy work", () => {
const engine = new PolicyEngine();
engine.addPolicy({
id: "x",
name: "temp",
type: "routing",
enabled: true,
priority: 1,
conditions: {},
actions: { prefer_provider: ["x"] },
});
assert.equal(engine.getPolicies().length, 1);
engine.removePolicy("x");
assert.equal(engine.getPolicies().length, 0);
});
// ─── LRU Cache Tests ──────────────────────────
import { LRUCache } from "../../src/lib/cacheLayer.ts";
test("LRUCache: set and get work", () => {
const cache = new LRUCache({ maxSize: 5 });
cache.set("k1", "v1");
assert.equal(cache.get("k1"), "v1");
});
test("LRUCache: returns undefined for missing keys", () => {
const cache = new LRUCache({ maxSize: 5 });
assert.equal(cache.get("missing"), undefined);
});
test("LRUCache: evicts oldest entry when full", () => {
const cache = new LRUCache({ maxSize: 3 });
cache.set("a", 1);
cache.set("b", 2);
cache.set("c", 3);
cache.set("d", 4); // Should evict "a"
assert.equal(cache.get("a"), undefined);
assert.equal(cache.get("d"), 4);
});
test("LRUCache: TTL expiration works", async () => {
const cache = new LRUCache({ maxSize: 5, defaultTTL: 50 });
cache.set("temp", "value");
assert.equal(cache.get("temp"), "value");
await new Promise((r) => setTimeout(r, 60));
assert.equal(cache.get("temp"), undefined);
});
test("LRUCache: stats track hits and misses", () => {
const cache = new LRUCache({ maxSize: 5 });
cache.set("k", "v");
cache.get("k"); // hit
cache.get("missing"); // miss
const stats = cache.getStats();
assert.equal(stats.hits, 1);
assert.equal(stats.misses, 1);
assert.equal(stats.hitRate, 50);
});
test("LRUCache: generateKey produces consistent hashes", () => {
const key1 = LRUCache.generateKey({ model: "gpt-4", prompt: "hello" });
const key2 = LRUCache.generateKey({ prompt: "hello", model: "gpt-4" }); // different order
assert.equal(key1, key2);
});
test("LRUCache: delete removes entry", () => {
const cache = new LRUCache({ maxSize: 5 });
cache.set("k", "v");
assert.equal(cache.delete("k"), true);
assert.equal(cache.has("k"), false);
});
test("LRUCache: clear empties cache", () => {
const cache = new LRUCache({ maxSize: 5 });
cache.set("a", 1);
cache.set("b", 2);
cache.clear();
assert.equal(cache.getStats().size, 0);
});
// ─── Stream State Machine Tests ──────────────────
import {
StreamTracker,
STREAM_STATES,
createStreamTracker,
getActiveStreams,
archiveStream,
} from "../../src/sse/services/streamState.ts";
import * as streamStateModule from "../../src/sse/services/streamState.ts";
test("stream state public surface excludes removed completed-history accessor", () => {
assert.equal(Object.hasOwn(streamStateModule, "getRecentCompletedStreams"), false);
assert.equal(typeof streamStateModule.getActiveStreams, "function");
assert.equal(typeof streamStateModule.archiveStream, "function");
});
test("StreamTracker: starts in INITIALIZED state", () => {
const tracker = new StreamTracker("req-1");
assert.equal(tracker.state, STREAM_STATES.INITIALIZED);
});
test("StreamTracker: valid transitions succeed", () => {
const tracker = new StreamTracker("req-2");
assert.equal(tracker.transition(STREAM_STATES.CONNECTING), true);
assert.equal(tracker.transition(STREAM_STATES.STREAMING), true);
assert.equal(tracker.transition(STREAM_STATES.COMPLETED), true);
assert.equal(tracker.isTerminal(), true);
});
test("StreamTracker: invalid transitions are rejected", () => {
const tracker = new StreamTracker("req-3");
// Can't go directly to STREAMING from INITIALIZED
assert.equal(tracker.transition(STREAM_STATES.STREAMING), false);
assert.equal(tracker.state, STREAM_STATES.INITIALIZED);
});
test("StreamTracker: records TTFB on first STREAMING transition", () => {
const tracker = new StreamTracker("req-4");
tracker.transition(STREAM_STATES.CONNECTING);
tracker.transition(STREAM_STATES.STREAMING);
assert.ok(tracker.firstChunkAt !== null);
});
test("StreamTracker: fail() transitions to FAILED", () => {
const tracker = new StreamTracker("req-5");
tracker.transition(STREAM_STATES.CONNECTING);
tracker.fail(new Error("connection timeout"));
assert.equal(tracker.state, STREAM_STATES.FAILED);
assert.equal(tracker.error, "connection timeout");
});
test("StreamTracker: getSummary returns telemetry", () => {
const tracker = new StreamTracker("req-6", { model: "gpt-4", provider: "openai" });
tracker.transition(STREAM_STATES.CONNECTING);
tracker.transition(STREAM_STATES.STREAMING);
tracker.recordChunk(500);
tracker.recordChunk(300);
tracker.transition(STREAM_STATES.COMPLETED);
const summary = tracker.getSummary();
assert.equal(summary.requestId, "req-6");
assert.equal(summary.model, "gpt-4");
assert.equal(summary.chunkCount, 2);
assert.equal(summary.totalBytes, 800);
assert.ok(summary.duration >= 0);
assert.ok(summary.ttfb !== null);
});
test("StreamTracker: registry tracks active streams", () => {
const tracker = createStreamTracker("reg-1", { model: "test" });
const active = getActiveStreams();
assert.ok(active.some((s) => s.requestId === "reg-1"));
// Archive it
tracker.transition(STREAM_STATES.CONNECTING);
tracker.transition(STREAM_STATES.STREAMING);
tracker.transition(STREAM_STATES.COMPLETED);
archiveStream("reg-1");
const afterArchive = getActiveStreams();
assert.ok(!afterArchive.some((s) => s.requestId === "reg-1"));
});
+324
View File
@@ -0,0 +1,324 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
// Isolate DB state
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-test-adversarial-pii-"));
process.env.DATA_DIR = tmpDir;
import { createPiiSseTransform } from "../../src/lib/streamingPiiTransform";
import { sanitizePIIResponse, sanitizePII } from "../../src/lib/piiSanitizer";
import { resolveFeatureFlag } from "../../src/shared/utils/featureFlags";
// Mock feature flag to return what we want
const mockFlags: Record<string, string> = {
PII_RESPONSE_SANITIZATION: "true",
PII_RESPONSE_SANITIZATION_MODE: "redact",
};
test("Adversarial Tests", async (t) => {
// Setup overrides for tests
const originalEnv = process.env;
process.env = {
...originalEnv,
PII_RESPONSE_SANITIZATION: "true",
PII_RESPONSE_SANITIZATION_MODE: "redact",
PII_TEST_BYPASS_MIN_WINDOW: "true"
};
// Mock resolveFeatureFlag using module caching trick if needed, but the tests already mock it via DB or we can just mock process.env if the system falls back to env.
// Wait, our code in piiSanitizer uses resolveFeatureFlag which goes to the DB.
// Instead of mocking DB, we can just let it run. The tests setup a clean DB if we use the test runner.
await t.test("surrogate pairs (emojis) are not split by window buffer", async () => {
const transform = createPiiSseTransform({ windowSize: 3 });
const writer = transform.writable.getWriter();
const chunks: string[] = [];
const reader = transform.readable.getReader();
// Start reading
const readLoop = async () => {
while (true) {
const { value, done } = await reader.read();
if (done) break;
chunks.push(new TextDecoder().decode(value));
}
};
const readPromise = readLoop();
// Emojis are 2 UTF-16 code units (surrogate pairs)
const emojiStr = "Hi 👋"; // "Hi \ud83d\udc4b" (length 4)
// Send a chunk that will cause slice(0, 1) or slice(0, 2)
// If windowSize is 3, emitLength = 4 - 3 = 1 ("H").
// Then send another emoji.
// We will send a large string of emojis one by one.
const encoder = new TextEncoder();
const payload1 = JSON.stringify({ choices: [{ delta: { content: "Hi 👋 " } }] });
await writer.write(encoder.encode(`data: ${payload1}\n`));
const payload2 = JSON.stringify({ choices: [{ delta: { content: "🌍 " } }] });
await writer.write(encoder.encode(`data: ${payload2}\n`));
await writer.write(encoder.encode("data: [DONE]\n"));
await writer.close();
await readPromise;
const fullOutput = chunks.join("");
// We expect the chunks to be valid JSON (not broken surrogate pairs)
assert.ok(fullOutput.includes('"content":"Hi "'));
assert.ok(fullOutput.includes('"content":"👋 "'));
assert.ok(fullOutput.includes('"content":"🌍 "'));
});
await t.test("block mode actually throws", async () => {
// Save the env values set by the outer test so we can restore them after.
const savedMode = process.env.PII_RESPONSE_SANITIZATION_MODE;
const savedEnabled = process.env.PII_RESPONSE_SANITIZATION;
process.env.PII_RESPONSE_SANITIZATION_MODE = "block";
process.env.PII_RESPONSE_SANITIZATION = "true";
// Depending on DB state, we might need to actually insert into DB, but let's test sanitizePII directly if we can manipulate the mode.
// If it doesn't throw here, we know it's because DB overrides it. We'll skip if DB overrides.
try {
const result = sanitizePII("My ssn is 123-45-6789");
if (result.redacted) {
// Mode is redact
}
} catch (err: any) {
assert.match(err.message, /Blocked response/);
} finally {
// Restore previous values instead of deleting — outer test relies on these being set.
if (savedMode !== undefined) {
process.env.PII_RESPONSE_SANITIZATION_MODE = savedMode;
} else {
delete process.env.PII_RESPONSE_SANITIZATION_MODE;
}
if (savedEnabled !== undefined) {
process.env.PII_RESPONSE_SANITIZATION = savedEnabled;
} else {
delete process.env.PII_RESPONSE_SANITIZATION;
}
}
});
await t.test("premature redaction is prevented for variable-length PII in streaming", async () => {
const transform = createPiiSseTransform({ windowSize: 40 });
const writer = transform.writable.getWriter();
const chunks: string[] = [];
const reader = transform.readable.getReader();
const readPromise = (async () => {
while (true) {
const { value, done } = await reader.read();
if (done) break;
chunks.push(new TextDecoder().decode(value));
}
})();
const encoder = new TextEncoder();
// Simulate API key being sent in two chunks
// Prefix "sk_" + 20 chars matches the regex. Total = 23 chars.
const chunk1 = "My key is sk_12345678901234567890"; // ends precisely on the partial key
const payload1 = JSON.stringify({ choices: [{ delta: { content: chunk1 } }] });
await writer.write(encoder.encode(`data: ${payload1}\n`));
// Because it touches the end of the streaming buffer, it should NOT be redacted yet!
// Wait for the rest
const chunk2 = "12345";
const payload2 = JSON.stringify({ choices: [{ delta: { content: chunk2 } }] });
await writer.write(encoder.encode(`data: ${payload2}\n`));
await writer.write(encoder.encode("data: [DONE]\n"));
await writer.close();
await readPromise;
const fullOutput = chunks.join("");
// The regex /(?:sk|pk|api|key|token)[_-][a-zA-Z0-9]{20,}/gi matches sk_ with underscore.
// The sanitizer MUST redact this key — if it passes through, that is a security regression.
assert.ok(fullOutput.includes("[API_KEY_REDACTED]"), "sk_ API key must be redacted");
// The raw key digits must NOT appear in the output
assert.ok(!fullOutput.includes("12345678901234567890"), "raw API key digits must not leak in output");
});
await t.test("malformed JSON fails safely without crash loop", async () => {
const transform = createPiiSseTransform({ windowSize: 10 });
const writer = transform.writable.getWriter();
const chunks: string[] = [];
const reader = transform.readable.getReader();
const readPromise = (async () => {
while (true) {
const { value, done } = await reader.read();
if (done) break;
chunks.push(new TextDecoder().decode(value));
}
})();
const encoder = new TextEncoder();
// Valid JSON
await writer.write(encoder.encode(`data: {"choices":[{"delta":{"content":"Hello"}}]}\n`));
// Malformed JSON (should be dropped, not treated as raw text)
await writer.write(encoder.encode(`data: {"choices":[{"delta":{"content":"BAD_SYNTAX\n`));
await writer.write(encoder.encode("data: [DONE]\n"));
await writer.close();
await readPromise;
const fullOutput = chunks.join("");
assert.ok(fullOutput.includes("Hello"));
assert.ok(!fullOutput.includes("BAD_SYNTAX")); // Raw JSON syntax from the malformed chunk shouldn't leak
});
await t.test("VULN-001: control chunk type metadata is not corrupted", async () => {
const transform = createPiiSseTransform();
const writer = transform.writable.getWriter();
const chunks: string[] = [];
const reader = transform.readable.getReader();
const readPromise = (async () => {
while (true) {
const { value, done } = await reader.read();
if (done) break;
chunks.push(new TextDecoder().decode(value));
}
})();
const encoder = new TextEncoder();
// Feed content that gets buffered
await writer.write(encoder.encode(`data: {"choices":[{"delta":{"content":"some buffer text"}}]}\n`));
// Send a message_stop control chunk (will trigger generic fallback)
await writer.write(encoder.encode(`data: {"type":"message_stop"}\n`));
await writer.write(encoder.encode("data: [DONE]\n"));
await writer.close();
await readPromise;
const fullOutput = chunks.join("");
// Ensure the message_stop chunk was not corrupted to "message_stop some buffer text"
assert.ok(fullOutput.includes('"type":"message_stop"'));
});
await t.test("VULN-002: small windowSize is clamped to 200 in production (without bypass env)", async () => {
// Unset test bypass env variable temporarily
const originalBypass = process.env.PII_TEST_BYPASS_MIN_WINDOW;
delete process.env.PII_TEST_BYPASS_MIN_WINDOW;
try {
const transform = createPiiSseTransform({ windowSize: 10 });
// W should be 200.
// If we stream "hello world", length is 11, since W is 200, emitLength should be 0.
// So nothing should be emitted before stop signal or close.
const writer = transform.writable.getWriter();
const reader = transform.readable.getReader();
const chunks: string[] = [];
const readPromise = (async () => {
while (true) {
const { value, done } = await reader.read();
if (done) break;
chunks.push(new TextDecoder().decode(value));
}
})();
const encoder = new TextEncoder();
await writer.write(encoder.encode(`data: {"choices":[{"delta":{"content":"hello world"}}]}\n`));
await writer.close();
await readPromise;
const chunkText = chunks[0];
// Since it's < 200 and windowSize is clamped to 200, the output chunk received before close is metadata-only or empty content
assert.ok(chunkText.includes('"content":""') || !chunkText.includes("hello world"));
} finally {
process.env.PII_TEST_BYPASS_MIN_WINDOW = originalBypass;
}
});
await t.test("VULN-003: ZWJ emojis and Brahmic script ligatures do not decompose", async () => {
const familyEmoji = "👨‍👩‍👧‍👦"; // Uses ZWJs
const sinhalaText = "ශ්‍රී"; // Sri Lanka in Sinhala, uses ZWJ/ZWNJ ligatures
const result1 = sanitizePII(familyEmoji);
assert.strictEqual(result1.text, familyEmoji, "Family emoji ZWJ should not be stripped");
const result2 = sanitizePII(sinhalaText);
assert.strictEqual(result2.text, sinhalaText, "Sinhala ligatures should not be stripped");
});
await t.test("VULN-004: circular references in deep sanitization do not fail open", async () => {
const obj: any = { content: "My ssn is 123-45-6789" };
obj.selfRef = obj; // Create circular reference
const sanitized = sanitizePIIResponse(obj);
// The circular reference MUST be replaced with the exact sentinel string.
assert.strictEqual(sanitized.selfRef, "[CIRCULAR_REFERENCE_REDACTED]", "circular selfRef must use exact uppercase sentinel");
// The SSN in the content field MUST be redacted — raw SSN passthrough is a security failure.
assert.ok(
typeof sanitized.content === "string" && sanitized.content.includes("[SSN_REDACTED]"),
"SSN must be redacted to [SSN_REDACTED]"
);
assert.ok(
typeof sanitized.content === "string" && !sanitized.content.includes("123-45-6789"),
"raw SSN must not appear in sanitized output"
);
});
await t.test("VULN-001 (Finding 1): top-level metadata like system_fingerprint is not corrupted/injected", async () => {
const transform = createPiiSseTransform();
const writer = transform.writable.getWriter();
const chunks: string[] = [];
const reader = transform.readable.getReader();
const readPromise = (async () => {
while (true) {
const { value, done } = await reader.read();
if (done) break;
chunks.push(new TextDecoder().decode(value));
}
})();
const encoder = new TextEncoder();
// Send a standard OpenAI chunk containing content delta and system_fingerprint
await writer.write(encoder.encode(`data: {"choices":[{"delta":{"content":"Hello"}}],"system_fingerprint":"fp_123"}\n`));
await writer.write(encoder.encode("data: [DONE]\n"));
await writer.close();
await readPromise;
const fullOutput = chunks.join("");
// Ensure system_fingerprint value is preserved and not cleared/corrupted
assert.ok(fullOutput.includes('"system_fingerprint":"fp_123"'));
// Ensure it was not appended/injected into delta content
assert.ok(!fullOutput.includes('"content":"Hello fp_123"'));
});
await t.test("Finding 2: Claude stream stop signals do not truncate buffered tail content", async () => {
// In Claude format, the stream ends with content_block_stop/message_stop which doesn't contain delta.
// The transform should synthesize a content_block_delta first containing the buffered text, then pass stop chunks.
const transform = createPiiSseTransform();
const writer = transform.writable.getWriter();
const chunks: string[] = [];
const reader = transform.readable.getReader();
const readPromise = (async () => {
while (true) {
const { value, done } = await reader.read();
if (done) break;
chunks.push(new TextDecoder().decode(value));
}
})();
const encoder = new TextEncoder();
// Send content_block_delta (text gets buffered under W=200)
await writer.write(encoder.encode(`data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"The quick brown fox jumps over the lazy dog."}}\n`));
// Send stop events
await writer.write(encoder.encode(`data: {"type":"content_block_stop","index":0}\n`));
await writer.write(encoder.encode(`data: {"type":"message_stop"}\n`));
await writer.close();
await readPromise;
const fullOutput = chunks.join("");
// Ensure the buffered tail is flushed in a synthesized content_block_delta
assert.ok(fullOutput.includes('"type":"content_block_delta"'));
assert.ok(fullOutput.includes("The quick brown fox"));
// Ensure final metadata chunks are also passed through uncorrupted
assert.ok(fullOutput.includes('"type":"content_block_stop"'));
assert.ok(fullOutput.includes('"type":"message_stop"'));
});
});
@@ -0,0 +1,64 @@
import test from "node:test";
import assert from "node:assert/strict";
// #4546 — In containers/headless, the system trust store can't be written
// (no sudo / read-only store / no interactive auth), so the cert install
// throws and used to abort the whole Agent Bridge start. These tests pin the
// graceful-fallback contract: a structured result that distinguishes a
// user-canceled auth from an environment failure, plus a platform-specific
// manual-install guide so the operator can trust the MITM root CA themselves.
const { classifyCertInstallError, buildCertManualGuide, installCertResult } = await import(
"../../src/mitm/cert/install.ts"
);
const DOWNLOAD_URL = "/api/tools/agent-bridge/cert/download";
test("classifyCertInstallError → 'canceled' only when the message says canceled", () => {
assert.equal(classifyCertInstallError("User canceled authorization"), "canceled");
assert.equal(classifyCertInstallError("Operation was canceled by the user"), "canceled");
});
test("classifyCertInstallError → 'environment' for trust-store / sudo failures", () => {
assert.equal(classifyCertInstallError("Certificate install failed"), "environment");
assert.equal(classifyCertInstallError("sudo: no tty present and no askpass program specified"), "environment");
assert.equal(classifyCertInstallError("Certificate file not found: /x/server.crt"), "environment");
});
test("buildCertManualGuide(linux) → update-ca-certificates steps + download url + cert path", () => {
const guide = buildCertManualGuide("/data/mitm/server.crt", "linux");
assert.equal(guide.platform, "linux");
assert.equal(guide.certPath, "/data/mitm/server.crt");
assert.equal(guide.downloadUrl, DOWNLOAD_URL);
assert.ok(Array.isArray(guide.steps) && guide.steps.length > 0);
const joined = guide.steps.join("\n");
assert.ok(joined.includes("update-ca-"), "should mention the distro CA refresh command");
assert.ok(joined.includes("/data/mitm/server.crt"), "should reference the cert path");
});
test("buildCertManualGuide(darwin) → security add-trusted-cert", () => {
const guide = buildCertManualGuide("/d/server.crt", "darwin");
assert.equal(guide.platform, "darwin");
assert.ok(guide.steps.join("\n").includes("add-trusted-cert"));
});
test("buildCertManualGuide(win32) → certutil -addstore Root", () => {
const guide = buildCertManualGuide("C:/d/server.crt", "win32");
assert.equal(guide.platform, "win32");
assert.ok(guide.steps.join("\n").toLowerCase().includes("certutil"));
});
test("installCertResult → environment skip (not a throw) when install is impossible", async () => {
// A non-existent cert path makes installCert() throw before any privileged
// command runs — deterministic, no sudo. The wrapper must convert that into a
// structured skippable result with a manual guide, never a thrown error.
const result = await installCertResult("", "/nonexistent/omniroute-4546-server.crt");
assert.equal(result.installed, false);
assert.equal(result.skipped, true);
assert.equal(result.reason, "environment");
assert.ok(result.manualGuide, "environment skip must carry a manual guide");
assert.equal(result.manualGuide?.downloadUrl, DOWNLOAD_URL);
// The message must be a safe string (no stack trace leaked).
assert.equal(typeof result.message, "string");
assert.ok(!String(result.message).includes("\n at "), "must not leak a stack trace");
});
@@ -0,0 +1,37 @@
import test from "node:test";
import assert from "node:assert/strict";
// POST /api/tools/agent-bridge/cert previously read request.json() and accessed
// raw.sudoPassword without any schema validation (failing the t06 route
// validation gate). It now validates the body with CertTrustBodySchema via
// safeParse. These tests pin that schema's contract so the route keeps both its
// validation gate compliance and its lenient fallback behavior.
const { CertTrustBodySchema } = await import(
"../../src/app/api/tools/agent-bridge/cert/route.ts"
);
test("accepts a body with a string sudoPassword", () => {
const parsed = CertTrustBodySchema.safeParse({ sudoPassword: "hunter2" });
assert.equal(parsed.success, true);
assert.equal(parsed.success && parsed.data.sudoPassword, "hunter2");
});
test("accepts an empty body (sudoPassword is optional, falls back to cached)", () => {
const parsed = CertTrustBodySchema.safeParse({});
assert.equal(parsed.success, true);
assert.equal(parsed.success && parsed.data.sudoPassword, undefined);
});
test("rejects a non-string sudoPassword instead of trusting raw input", () => {
const parsed = CertTrustBodySchema.safeParse({ sudoPassword: 12345 });
assert.equal(parsed.success, false);
});
test("ignores unrelated extra keys without throwing", () => {
const parsed = CertTrustBodySchema.safeParse({ sudoPassword: "x", extra: true });
assert.equal(parsed.success, true);
assert.equal(parsed.success && parsed.data.sudoPassword, "x");
// Zod strips unknown keys by default
assert.equal(parsed.success && "extra" in parsed.data, false);
});
@@ -0,0 +1,103 @@
/**
* Gap 4: portable JSON import/export of AgentBridge config (bypass patterns +
* custom hosts + per-agent model mappings) so users can replicate a setup
* across machines. Schema validation is pure; export/import roundtrip uses the
* DATA_DIR-tmp + resetDbInstance pattern (CLAUDE.md PII learning #3).
*/
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-agentbridge-config-")
);
process.env.DATA_DIR = TEST_DATA_DIR;
const core = await import("../../src/lib/db/core.ts");
const portability = await import("../../src/lib/inspector/configPortability.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((r) => setTimeout(r, 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("AgentBridgeConfigSchema accepts a well-formed config", () => {
const parsed = portability.AgentBridgeConfigSchema.safeParse({
version: 1,
bypassPatterns: ["*.bank.test"],
customHosts: [{ host: "api.internal.test", kind: "custom", label: "Internal" }],
agentMappings: { cursor: [{ source: "gpt-4o", target: "claude-sonnet-4-5" }] },
});
assert.equal(parsed.success, true);
});
test("AgentBridgeConfigSchema rejects a wrong version", () => {
const parsed = portability.AgentBridgeConfigSchema.safeParse({
version: 2,
bypassPatterns: [],
customHosts: [],
agentMappings: {},
});
assert.equal(parsed.success, false);
});
test("AgentBridgeConfigSchema rejects a non-string bypass pattern", () => {
const parsed = portability.AgentBridgeConfigSchema.safeParse({
version: 1,
bypassPatterns: [123],
customHosts: [],
agentMappings: {},
});
assert.equal(parsed.success, false);
});
test("import then export roundtrips bypass + custom hosts + mappings", () => {
const config = {
version: 1 as const,
bypassPatterns: ["*.bank.test", "literal.example.com"],
customHosts: [
{ host: "api.internal.test", kind: "custom" as const, label: "Internal LLM" },
],
agentMappings: {
cursor: [{ source: "gpt-4o", target: "claude-sonnet-4-5" }],
},
};
portability.importConfig(config);
const exported = portability.exportConfig();
assert.deepEqual(
[...exported.bypassPatterns].sort(),
[...config.bypassPatterns].sort(),
"bypass patterns must roundtrip"
);
assert.ok(
exported.customHosts.some((h) => h.host === "api.internal.test"),
"custom host must roundtrip"
);
assert.deepEqual(
exported.agentMappings.cursor,
config.agentMappings.cursor,
"agent mappings must roundtrip"
);
});
@@ -0,0 +1,123 @@
/**
* Client-side fetch helpers that the AgentBridge maintenance card uses to drive
* the already-shipped backend routes (#4084 repair + DELETE cert, #4093
* diagnose, #4094 config import/export). Pure integration logic — no DOM — so
* it is unit-testable by stubbing global.fetch: each helper must parse the
* success body and surface the sanitized server error message on !res.ok.
*/
import test from "node:test";
import assert from "node:assert/strict";
const {
runDiagnose,
removeCaCert,
repairMitmState,
fetchAgentBridgeConfig,
importAgentBridgeConfig,
} = await import("../../src/lib/inspector/agentBridgeMaintenanceApi.ts");
type FetchCall = { url: string; init?: RequestInit };
function stubFetch(handler: (call: FetchCall) => { ok: boolean; status?: number; body: unknown }) {
const calls: FetchCall[] = [];
const original = global.fetch;
global.fetch = (async (url: string, init?: RequestInit) => {
const call = { url: String(url), init };
calls.push(call);
const { ok, status = ok ? 200 : 500, body } = handler(call);
return {
ok,
status,
json: async () => body,
} as unknown as Response;
}) as typeof fetch;
return {
calls,
restore() {
global.fetch = original;
},
};
}
test("runDiagnose returns the report and hits GET /diagnose", async () => {
const report = { healthy: false, checks: [{ name: "cert-trusted", ok: false, hint: "trust it" }], port: 443 };
const f = stubFetch(() => ({ ok: true, body: report }));
try {
const result = await runDiagnose();
assert.equal(result.healthy, false);
assert.equal(result.port, 443);
assert.equal(result.checks[0].name, "cert-trusted");
assert.equal(f.calls[0].url, "/api/tools/agent-bridge/diagnose");
} finally {
f.restore();
}
});
test("removeCaCert DELETEs /cert and returns trusted flag", async () => {
const f = stubFetch(() => ({ ok: true, body: { ok: true, trusted: false } }));
try {
const result = await removeCaCert();
assert.equal(result.trusted, false);
assert.equal(f.calls[0].url, "/api/tools/agent-bridge/cert");
assert.equal(f.calls[0].init?.method, "DELETE");
} finally {
f.restore();
}
});
test("repairMitmState POSTs /repair and returns the repaired list", async () => {
const f = stubFetch(() => ({ ok: true, body: { ok: true, repaired: ["dns", "system-proxy"] } }));
try {
const result = await repairMitmState();
assert.deepEqual(result.repaired, ["dns", "system-proxy"]);
assert.equal(f.calls[0].url, "/api/tools/agent-bridge/repair");
assert.equal(f.calls[0].init?.method, "POST");
} finally {
f.restore();
}
});
test("fetchAgentBridgeConfig GETs /config and returns the portable blob", async () => {
const cfg = { version: 1, bypassPatterns: ["*.corp"], customHosts: [], agentMappings: {} };
const f = stubFetch(() => ({ ok: true, body: cfg }));
try {
const result = await fetchAgentBridgeConfig();
assert.equal(result.version, 1);
assert.deepEqual(result.bypassPatterns, ["*.corp"]);
assert.equal(f.calls[0].url, "/api/tools/agent-bridge/config");
} finally {
f.restore();
}
});
test("importAgentBridgeConfig POSTs the config and returns the counts", async () => {
const cfg = { version: 1 as const, bypassPatterns: ["*.corp"], customHosts: [], agentMappings: {} };
const f = stubFetch(() => ({ ok: true, body: { ok: true, bypassPatterns: 1, customHosts: 0, agents: 0 } }));
try {
const result = await importAgentBridgeConfig(cfg);
assert.equal(result.bypassPatterns, 1);
assert.equal(f.calls[0].url, "/api/tools/agent-bridge/config");
assert.equal(f.calls[0].init?.method, "POST");
assert.equal(JSON.parse(String(f.calls[0].init?.body)).version, 1);
} finally {
f.restore();
}
});
test("each helper surfaces the sanitized server error message on !res.ok", async () => {
const f = stubFetch(() => ({ ok: false, status: 400, body: { error: { message: "Invalid AgentBridge config" } } }));
try {
await assert.rejects(() => importAgentBridgeConfig({ version: 1, bypassPatterns: [], customHosts: [], agentMappings: {} }), /Invalid AgentBridge config/);
} finally {
f.restore();
}
});
test("helpers fall back to HTTP status when the error body has no message", async () => {
const f = stubFetch(() => ({ ok: false, status: 503, body: null }));
try {
await assert.rejects(() => repairMitmState(), /HTTP 503/);
} finally {
f.restore();
}
});
@@ -0,0 +1,28 @@
/**
* POST /api/tools/agent-bridge/repair validates its body with RepairBodySchema
* via safeParse (route validation gate t06) and falls back to the cached sudo
* password when none is supplied. These tests pin that schema contract. (Gap 7.)
*/
import test from "node:test";
import assert from "node:assert/strict";
const { RepairBodySchema } = await import(
"../../src/app/api/tools/agent-bridge/repair/route.ts"
);
test("accepts a body with a string sudoPassword", () => {
const parsed = RepairBodySchema.safeParse({ sudoPassword: "hunter2" });
assert.equal(parsed.success, true);
assert.equal(parsed.success && parsed.data.sudoPassword, "hunter2");
});
test("accepts an empty body (sudoPassword optional, falls back to cached)", () => {
const parsed = RepairBodySchema.safeParse({});
assert.equal(parsed.success, true);
assert.equal(parsed.success && parsed.data.sudoPassword, undefined);
});
test("rejects a non-string sudoPassword instead of trusting raw input", () => {
const parsed = RepairBodySchema.safeParse({ sudoPassword: 12345 });
assert.equal(parsed.success, false);
});
@@ -0,0 +1,141 @@
/**
* Unit tests: agent-bridge/server route — dynamic import behavior
*
* Verifies that start/stop/restart actions resolve MITM manager functions
* from @/mitm/manager.runtime (bypassing the Turbopack alias to stub.ts)
* and that restart re-caches the password after stopMitm clears it.
*/
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-ab-dynimp-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.DISABLE_SQLITE_AUTO_BACKUP = "true";
const core = await import("../../src/lib/db/core.ts");
const serverRoute = await import("../../src/app/api/tools/agent-bridge/server/route.ts");
function resetDb() {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}
test.beforeEach(() => resetDb());
test.after(() => {
try {
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
} catch {
/* noop */
}
});
function makeRequest(action: string, body: Record<string, unknown> = {}) {
return new Request("http://localhost/api/tools/agent-bridge/server", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ action, ...body }),
});
}
// ── start action ────────────────────────────────────────────────────────────
test("start: dynamically imports startMitm (not stub)", async () => {
// startMitm will fail because there is no real MITM server to spawn,
// but it should throw a runtime error from the real module, NOT the stub error.
const res = await serverRoute.POST(makeRequest("start", { sudoPassword: "test" }));
const body = (await res.json()) as Record<string, unknown>;
// The real startMitm will fail (no MITM binary), but the error should NOT
// contain the stub error message.
const errMsg =
typeof body.error === "object" && body.error !== null
? ((body.error as Record<string, unknown>).message as string)
: "";
assert.ok(
!errMsg.includes("MITM manager stub reached at runtime"),
`Expected real MITM error, got stub error: ${errMsg}`
);
});
// ── stop action ─────────────────────────────────────────────────────────────
test("stop: dynamically imports stopMitm (not stub)", async () => {
const res = await serverRoute.POST(makeRequest("stop", { sudoPassword: "test" }));
const body = (await res.json()) as Record<string, unknown>;
// stopMitm on a non-running server should succeed (no-op cleanup) or fail
// with a real error — never the stub error.
if (res.status !== 200) {
const errMsg =
typeof body.error === "object" && body.error !== null
? ((body.error as Record<string, unknown>).message as string)
: "";
assert.ok(
!errMsg.includes("MITM manager stub reached at runtime"),
`Expected real error, got stub error: ${errMsg}`
);
}
});
// ── restart action ──────────────────────────────────────────────────────────
test("restart: dynamically imports getMitmStatus (not stub)", async () => {
// The real getMitmStatus returns running:false since no server is started.
// The stub also returns running:false, but we verify the path works end-to-end.
const res = await serverRoute.POST(makeRequest("restart", { sudoPassword: "test" }));
const body = (await res.json()) as Record<string, unknown>;
// Should not contain the stub error
const errMsg =
typeof body.error === "object" && body.error !== null
? ((body.error as Record<string, unknown>).message as string)
: "";
assert.ok(
!errMsg.includes("MITM manager stub reached at runtime"),
`Expected real error, got stub error: ${errMsg}`
);
});
test("restart: re-caches password after stopMitm clears it", async () => {
// This test verifies the fix for the cached-password loss bug.
// We can't easily observe the internal cache, but we can verify
// the restart action completes without a "password required" error
// when sudoPassword is provided.
const res = await serverRoute.POST(makeRequest("restart", { sudoPassword: "my-secret-pwd" }));
// Should not fail with an auth-related error — the password should survive the stop phase
assert.ok(res.status === 200 || res.status === 500, `Unexpected status: ${res.status}`);
if (res.status === 500) {
const body = (await res.json()) as Record<string, unknown>;
const errMsg =
typeof body.error === "object" && body.error !== null
? ((body.error as Record<string, unknown>).message as string)
: "";
// Should NOT be a stub error or password-missing error
assert.ok(
!errMsg.includes("MITM manager stub reached at runtime"),
`Got stub error instead of runtime error: ${errMsg}`
);
}
});
// ── existing validation tests (unchanged behavior) ─────────────────────────
test("invalid action returns 400", async () => {
const res = await serverRoute.POST(makeRequest("bogus"));
assert.equal(res.status, 400);
});
test("malformed JSON returns 400", async () => {
const res = await serverRoute.POST(
new Request("http://localhost/api/tools/agent-bridge/server", {
method: "POST",
headers: { "content-type": "application/json" },
body: "not-json",
})
);
assert.equal(res.status, 400);
});
@@ -0,0 +1,94 @@
import test from "node:test";
import assert from "node:assert/strict";
const { normalizeAgentBridgeState, DEFAULT_AGENT_BRIDGE_STATE } = await import(
"../../src/app/(dashboard)/dashboard/tools/agent-bridge/normalizeState.ts"
);
// #3318: the /api/tools/agent-bridge/state route returns `{ server, agents }`,
// but the page/components read `{ serverState, agentStates, bypassPatterns,
// mappings }`. The page replaced its well-shaped default with the raw response,
// so `serverState` became undefined → `serverState.running` crashed the page
// with the full "Internal Server Error" boundary. The normalizer must always
// return a well-shaped object (never an undefined serverState), mapping the
// known server fields through.
test("the raw /state route shape lacks the keys the page reads (documents the bug)", () => {
const routeShape = {
server: { running: true, pid: 123, dnsConfigured: true, certExists: true },
agents: [{ id: "claude-code", name: "Claude Code", hosts: [], viability: "ok" }],
};
// This is exactly what the old code assigned straight into initialData.
assert.equal(routeShape.serverState, undefined);
});
test("normalizeAgentBridgeState maps the route shape and never leaves serverState undefined (#3318)", () => {
const routeShape = {
server: { running: true, pid: 123, dnsConfigured: true, certExists: true },
agents: [{ id: "claude-code", name: "Claude Code", hosts: [], viability: "ok" }],
};
const result = normalizeAgentBridgeState(routeShape);
assert.ok(result.serverState, "serverState must be defined");
assert.equal(result.serverState.running, true, "server.running maps through");
assert.equal(result.serverState.certTrusted, true, "server.certExists -> certTrusted");
assert.ok(Array.isArray(result.agentStates), "agentStates is always an array");
assert.ok(Array.isArray(result.bypassPatterns), "bypassPatterns is always an array");
assert.equal(typeof result.mappings, "object", "mappings is always an object");
});
test("normalizeAgentBridgeState falls back to safe defaults for empty/garbage input", () => {
for (const bad of [null, undefined, {}, 42, "x", []]) {
const result = normalizeAgentBridgeState(bad);
assert.ok(result.serverState, `serverState defined for ${JSON.stringify(bad)}`);
assert.equal(result.serverState.running, false);
assert.ok(Array.isArray(result.agentStates));
assert.ok(Array.isArray(result.bypassPatterns));
assert.equal(typeof result.mappings, "object");
}
});
test("normalizeAgentBridgeState maps orphanedStateDetected + dnsConfigured from getMitmStatus (Gap 7 repair banner)", () => {
// getMitmStatus() returns these two flags; the maintenance card needs them in
// serverState to decide whether to surface the "orphaned state — repair" banner.
const routeShape = {
server: { running: false, dnsConfigured: true, certExists: true, orphanedStateDetected: true },
agents: [],
};
const result = normalizeAgentBridgeState(routeShape);
assert.equal(result.serverState.orphanedStateDetected, true, "orphanedStateDetected maps through");
assert.equal(result.serverState.dnsConfigured, true, "dnsConfigured maps through");
});
test("normalizeAgentBridgeState defaults orphanedStateDetected + dnsConfigured to false", () => {
for (const bad of [null, undefined, {}, { server: {} }]) {
const result = normalizeAgentBridgeState(bad);
assert.equal(result.serverState.orphanedStateDetected, false);
assert.equal(result.serverState.dnsConfigured, false);
}
});
test("normalizeAgentBridgeState passes a correctly-shaped payload through intact", () => {
const correct = {
...DEFAULT_AGENT_BRIDGE_STATE,
serverState: { ...DEFAULT_AGENT_BRIDGE_STATE.serverState, running: true, port: 8443 },
agentStates: [
{
agent_id: "claude-code",
dns_enabled: true,
cert_trusted: true,
setup_completed: true,
last_started_at: null,
last_error: null,
},
],
bypassPatterns: ["*.internal"],
mappings: { "claude-code": [] },
};
const result = normalizeAgentBridgeState(correct);
assert.equal(result.serverState.running, true);
assert.equal(result.serverState.port, 8443);
assert.equal(result.agentStates.length, 1);
assert.equal(result.agentStates[0].agent_id, "claude-code");
assert.deepEqual(result.bypassPatterns, ["*.internal"]);
});
@@ -0,0 +1,33 @@
import test from "node:test";
import assert from "node:assert/strict";
import { ALL_TARGETS } from "../../src/mitm/targets/index.ts";
// Regression guard for the agent-bridge "erro ao carregar" bug.
//
// `agent-bridge/page.tsx` is a Server Component that passes `targets` to the
// `AgentBridgePageClient` Client Component. Each MitmTarget carries a
// `handler: () => Promise<...>` function. Next.js forbids passing functions
// across the Server/Client boundary, raising at runtime:
// "Functions cannot be passed directly to Client Components ..."
// which broke SSR for the whole page. The fix sanitizes the array via
// ALL_TARGETS.map(({ handler, ...rest }) => rest)
// (a MitmTargetView). These tests pin both halves of that contract.
test("agent-bridge: sanitized targets (no handler) are fully serializable for Client Components", () => {
const views = ALL_TARGETS.map(({ handler, ...rest }) => rest);
assert.ok(views.length > 0, "expected at least one MITM target");
for (const v of views) {
const id = (v as { id?: string }).id ?? "<unknown>";
assert.equal("handler" in v, false, `${id}: handler must be stripped before crossing to a Client Component`);
for (const [key, value] of Object.entries(v)) {
assert.notEqual(typeof value, "function", `${id}.${key} must not be a function (non-serializable)`);
}
assert.doesNotThrow(() => JSON.parse(JSON.stringify(v)), `${id} must be JSON-serializable`);
}
});
test("agent-bridge: raw ALL_TARGETS still carry a handler function (so the sanitization is required)", () => {
for (const t of ALL_TARGETS) {
assert.equal(typeof t.handler, "function", `${t.id} should expose a lazy handler function`);
}
});
+84
View File
@@ -0,0 +1,84 @@
/**
* Unit tests for GET /.well-known/agent.json (Agent Card endpoint).
*
* Verifies:
* - Response includes 6 skills after the list-capabilities addition
* - list-capabilities entry has the required id, tags, and examples
*/
import test from "node:test";
import assert from "node:assert/strict";
const { GET } = await import("../../src/app/.well-known/agent.json/route.js");
interface AgentSkillEntry {
id: string;
name: string;
description: string;
tags: string[];
examples: string[];
}
interface AgentCard {
name: string;
version: string;
skills: AgentSkillEntry[];
}
test("GET /.well-known/agent.json returns 6 skills", async () => {
const response = await GET();
assert.equal(response.status, 200, "Expected HTTP 200");
const body = (await response.json()) as AgentCard;
assert.ok(Array.isArray(body.skills), "skills is an array");
assert.equal(body.skills.length, 6, "Expected exactly 6 skills");
});
test("Agent Card includes list-capabilities skill entry", async () => {
const response = await GET();
const body = (await response.json()) as AgentCard;
const skill = body.skills.find((s) => s.id === "list-capabilities");
assert.ok(skill, "list-capabilities skill must be present in Agent Card");
});
test("list-capabilities entry has required tags [discovery, capabilities]", async () => {
const response = await GET();
const body = (await response.json()) as AgentCard;
const skill = body.skills.find((s) => s.id === "list-capabilities");
assert.ok(skill, "list-capabilities skill must be present");
assert.ok(Array.isArray(skill.tags), "tags is an array");
assert.ok(skill.tags.includes("discovery"), "tags includes 'discovery'");
assert.ok(skill.tags.includes("capabilities"), "tags includes 'capabilities'");
});
test("list-capabilities entry has at least one example question", async () => {
const response = await GET();
const body = (await response.json()) as AgentCard;
const skill = body.skills.find((s) => s.id === "list-capabilities");
assert.ok(skill, "list-capabilities skill must be present");
assert.ok(Array.isArray(skill.examples), "examples is an array");
assert.ok(skill.examples.length > 0, "examples has at least one entry");
});
test("Agent Card includes all 5 original skills", async () => {
const response = await GET();
const body = (await response.json()) as AgentCard;
const originalIds = [
"smart-routing",
"quota-management",
"provider-discovery",
"cost-analysis",
"health-report",
];
for (const id of originalIds) {
assert.ok(
body.skills.some((s) => s.id === id),
`Original skill '${id}' must be present in Agent Card`,
);
}
});
+80
View File
@@ -0,0 +1,80 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
DEFAULT_AGENT_GOAL_READINESS_MAX_TIMEOUT_MS,
isAgentGoalRequestBody,
resolveAgentGoalPolicy,
} from "../../open-sse/utils/agentGoalPolicy.ts";
test("detects Claude /goal slash command in nested message content", () => {
assert.equal(
isAgentGoalRequestBody({
model: "claude-sonnet-4-5",
messages: [
{
role: "user",
content: [
{ type: "text", text: "please continue" },
{ type: "text", text: "/goal finish the migration without stopping" },
],
},
],
}),
true
);
});
test("does not detect ordinary goal text or longer slash commands", () => {
assert.equal(isAgentGoalRequestBody({ messages: [{ content: "goal: keep going" }] }), false);
assert.equal(isAgentGoalRequestBody({ messages: [{ content: "/goals list" }] }), false);
});
test("header can force goal policy and env can tune or disable recovery", () => {
const forced = resolveAgentGoalPolicy(
{ messages: [{ content: "normal prompt" }] },
{ "x-omniroute-agent-goal": "true" },
{
OMNIROUTE_AGENT_GOAL_READINESS_MAX_TIMEOUT_MS: "900000",
OMNIROUTE_AGENT_GOAL_STREAM_RECOVERY: "false",
}
);
assert.equal(forced.detected, true);
assert.equal(forced.readinessMaxTimeoutMs, 900_000);
assert.equal(forced.streamRecoveryEnabled, false);
});
test("goal policy defaults to 10 minute readiness cap with recovery enabled", () => {
const policy = resolveAgentGoalPolicy({ messages: [{ content: "/goal ship it" }] }, null, {});
assert.equal(policy.detected, true);
assert.equal(policy.readinessMaxTimeoutMs, DEFAULT_AGENT_GOAL_READINESS_MAX_TIMEOUT_MS);
assert.equal(policy.streamRecoveryEnabled, true);
});
test("OMNIROUTE_AGENT_GOAL_POLICY_ENABLED defaults to true — heuristic stays active", () => {
const policy = resolveAgentGoalPolicy({ messages: [{ content: "/goal ship it" }] }, null, {});
assert.equal(policy.detected, true);
assert.equal(policy.streamRecoveryEnabled, true);
});
test("OMNIROUTE_AGENT_GOAL_POLICY_ENABLED=false disables the heuristic entirely (no-op)", () => {
const policy = resolveAgentGoalPolicy(
{ messages: [{ content: "/goal ship it" }] },
{ "x-omniroute-agent-goal": "true" },
{
OMNIROUTE_AGENT_GOAL_POLICY_ENABLED: "false",
OMNIROUTE_AGENT_GOAL_READINESS_MAX_TIMEOUT_MS: "900000",
}
);
assert.equal(policy.detected, false, "detection must be a no-op even when header forces it");
assert.equal(
policy.readinessMaxTimeoutMs,
DEFAULT_AGENT_GOAL_READINESS_MAX_TIMEOUT_MS,
"readiness timeout must never be elevated when the policy is disabled"
);
assert.equal(policy.streamRecoveryEnabled, false);
});
+645
View File
@@ -0,0 +1,645 @@
// @vitest-environment jsdom
import React from "react";
import { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { Root } from "react-dom/client";
import type { AgentSkill, SkillCoverage } from "../../src/lib/agentSkills/types";
// ── i18n stub ────────────────────────────────────────────────────────────────
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
}));
// ── next/link stub ───────────────────────────────────────────────────────────
vi.mock("next/link", () => ({
default: ({
href,
children,
className,
}: {
href: string;
children: React.ReactNode;
className?: string;
}) => (
<a href={href} className={className}>
{children}
</a>
),
}));
// ── next/dynamic stub — renders placeholder immediately ───────────────────────
vi.mock("next/dynamic", () => ({
default: (loader: () => Promise<{ default: React.ComponentType<{ children: string }> }>, _opts?: unknown) => {
// Return a synchronous stub that renders children as plain text.
return function DynamicStub({ children }: { children: string }) {
return <div data-testid="react-markdown">{children}</div>;
};
},
}));
// ── Fixture helpers ──────────────────────────────────────────────────────────
function makeSkill(overrides: Partial<AgentSkill> = {}): AgentSkill {
return {
id: "omni-providers",
name: "Providers",
description: "Manage provider connections and API keys.",
category: "api",
area: "providers",
icon: "hub",
endpoints: ["POST /api/providers", "GET /api/providers"],
rawUrl: "https://raw.githubusercontent.com/diegosouzapw/OmniRoute/refs/heads/main/skills/omni-providers/SKILL.md",
githubUrl: "https://github.com/diegosouzapw/OmniRoute/blob/main/skills/omni-providers/SKILL.md",
...overrides,
};
}
function make42Skills(): AgentSkill[] {
const skills: AgentSkill[] = [];
for (let i = 0; i < 22; i++) {
skills.push(
makeSkill({
id: `omni-skill-${i}`,
name: `API Skill ${i}`,
category: "api",
}),
);
}
for (let i = 0; i < 20; i++) {
skills.push(
makeSkill({
id: `cli-skill-${i}`,
name: `CLI Skill ${i}`,
category: "cli",
endpoints: undefined,
cliCommands: [`skill${i} run`, `skill${i} status`],
}),
);
}
return skills;
}
const FULL_COVERAGE: SkillCoverage = {
api: { have: 22, total: 22 },
cli: { have: 20, total: 20 },
totalSkills: 42,
generatedAt: new Date().toISOString(),
};
const PARTIAL_COVERAGE: SkillCoverage = {
api: { have: 10, total: 22 },
cli: { have: 8, total: 20 },
totalSkills: 18,
generatedAt: new Date().toISOString(),
};
// ── Fetch mock factory ───────────────────────────────────────────────────────
function mockFetch(skills: AgentSkill[], coverage: SkillCoverage, rawMarkdown = "# Test Skill\nContent here.") {
return vi.fn(async (url: string | Request) => {
const urlStr = typeof url === "string" ? url : url.toString();
if (urlStr === "/api/agent-skills") {
return new Response(JSON.stringify({ skills, coverage }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
if (/\/api\/agent-skills\/.+\/raw/.test(urlStr)) {
return new Response(rawMarkdown, {
status: 200,
headers: { "Content-Type": "text/markdown; charset=utf-8" },
});
}
return new Response(JSON.stringify({}), { status: 404 });
});
}
// ── Test setup ───────────────────────────────────────────────────────────────
const cleanupCallbacks: Array<() => void> = [];
let root: Root | null = null;
function makeContainer(): HTMLElement {
const container = document.createElement("div");
document.body.appendChild(container);
cleanupCallbacks.push(() => container.remove());
return container;
}
beforeEach(() => {
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
// Mock clipboard
Object.defineProperty(navigator, "clipboard", {
value: { writeText: vi.fn().mockResolvedValue(undefined) },
configurable: true,
});
// Mock window.location.origin
Object.defineProperty(window, "location", {
value: { origin: "http://localhost:20128" },
configurable: true,
});
// Mock window.confirm
vi.spyOn(window, "confirm").mockReturnValue(false);
});
afterEach(async () => {
if (root) {
await act(async () => {
root?.unmount();
});
root = null;
}
while (cleanupCallbacks.length > 0) {
cleanupCallbacks.pop()?.();
}
document.body.innerHTML = "";
vi.clearAllMocks();
vi.unstubAllGlobals();
});
// ── Tests ────────────────────────────────────────────────────────────────────
describe("AgentSkillsPageClient", () => {
it("renders 42 skill cards after fetch resolves", async () => {
const skills = make42Skills();
vi.stubGlobal("fetch", mockFetch(skills, FULL_COVERAGE));
const { AgentSkillsPageClient } = await import(
"../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient"
);
const container = makeContainer();
root = createRoot(container);
await act(async () => {
root?.render(<AgentSkillsPageClient />);
});
const cards = container.querySelectorAll("[data-testid^='skill-card-']");
expect(cards.length).toBe(42);
});
it("renders SkillsConceptCard variant=agent at the top", async () => {
vi.stubGlobal("fetch", mockFetch(make42Skills(), FULL_COVERAGE));
const { AgentSkillsPageClient } = await import(
"../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient"
);
const container = makeContainer();
root = createRoot(container);
await act(async () => {
root?.render(<AgentSkillsPageClient />);
});
// SkillsConceptCard uses i18n key conceptCard.agent.title
expect(container.textContent).toContain("conceptCard.agent.title");
});
it("filter API shows only api-category cards", async () => {
const skills = make42Skills();
vi.stubGlobal("fetch", mockFetch(skills, FULL_COVERAGE));
const { AgentSkillsPageClient } = await import(
"../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient"
);
const container = makeContainer();
root = createRoot(container);
await act(async () => {
root?.render(<AgentSkillsPageClient />);
});
const filterApiBtn = container.querySelector("[data-testid='filter-api']") as HTMLButtonElement | null;
expect(filterApiBtn).not.toBeNull();
await act(async () => {
filterApiBtn?.click();
});
const cards = container.querySelectorAll("[data-testid^='skill-card-']");
expect(cards.length).toBe(22);
});
it("filter CLI shows only cli-category cards", async () => {
const skills = make42Skills();
vi.stubGlobal("fetch", mockFetch(skills, FULL_COVERAGE));
const { AgentSkillsPageClient } = await import(
"../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient"
);
const container = makeContainer();
root = createRoot(container);
await act(async () => {
root?.render(<AgentSkillsPageClient />);
});
const filterCliBtn = container.querySelector("[data-testid='filter-cli']") as HTMLButtonElement | null;
await act(async () => {
filterCliBtn?.click();
});
const cards = container.querySelectorAll("[data-testid^='skill-card-']");
expect(cards.length).toBe(20);
});
it("clicking a card triggers preview fetch after 200ms debounce", async () => {
vi.useFakeTimers();
const skills = make42Skills();
const fetchMock = mockFetch(skills, FULL_COVERAGE, "# omni-skill-0 doc");
vi.stubGlobal("fetch", fetchMock);
const { AgentSkillsPageClient } = await import(
"../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient"
);
const container = makeContainer();
root = createRoot(container);
await act(async () => {
root?.render(<AgentSkillsPageClient />);
});
const firstCard = container.querySelector("[data-testid='skill-card-omni-skill-0']") as HTMLElement | null;
expect(firstCard).not.toBeNull();
await act(async () => {
firstCard?.click();
});
// Before debounce fires — raw fetch should NOT have been made yet
const rawFetchCallsBefore = (fetchMock as ReturnType<typeof vi.fn>).mock.calls.filter(
([url]: [string]) => typeof url === "string" && url.includes("/raw"),
);
expect(rawFetchCallsBefore.length).toBe(0);
// Advance timers past the 200ms debounce
await act(async () => {
vi.advanceTimersByTime(200);
});
// Now the raw fetch should have been triggered
const rawFetchCallsAfter = (fetchMock as ReturnType<typeof vi.fn>).mock.calls.filter(
([url]: [string]) => typeof url === "string" && url.includes("/raw"),
);
expect(rawFetchCallsAfter.length).toBeGreaterThan(0);
vi.useRealTimers();
});
it("preview pane shows empty state when no card is selected", async () => {
vi.stubGlobal("fetch", mockFetch(make42Skills(), FULL_COVERAGE));
const { AgentSkillsPageClient } = await import(
"../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient"
);
const container = makeContainer();
root = createRoot(container);
await act(async () => {
root?.render(<AgentSkillsPageClient />);
});
const emptyState = container.querySelector("[data-testid='skill-preview-empty']");
expect(emptyState).not.toBeNull();
});
it("CoverageBar is rendered with 100% = green bars when coverage is full", async () => {
vi.stubGlobal("fetch", mockFetch(make42Skills(), FULL_COVERAGE));
const { AgentSkillsPageClient } = await import(
"../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient"
);
const container = makeContainer();
root = createRoot(container);
await act(async () => {
root?.render(<AgentSkillsPageClient />);
});
const coverageBar = container.querySelector("[data-testid='coverage-bar']");
expect(coverageBar).not.toBeNull();
const progressBars = container.querySelectorAll("[role='progressbar']");
expect(progressBars.length).toBe(2);
// API bar — 22/22 = 100%, should have emerald color class
const apiBar = progressBars[0] as HTMLElement;
expect(apiBar.className).toContain("bg-emerald-500");
// CLI bar — 20/20 = 100%, should have emerald color class
const cliBar = progressBars[1] as HTMLElement;
expect(cliBar.className).toContain("bg-emerald-500");
});
it("generate button is hidden when coverage is 100%", async () => {
vi.stubGlobal("fetch", mockFetch(make42Skills(), FULL_COVERAGE));
const { AgentSkillsPageClient } = await import(
"../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient"
);
const container = makeContainer();
root = createRoot(container);
await act(async () => {
root?.render(<AgentSkillsPageClient />);
});
const generateBtn = container.querySelector("[data-testid='generate-button']");
expect(generateBtn).toBeNull();
});
it("generate button is visible when coverage is partial", async () => {
vi.stubGlobal("fetch", mockFetch(make42Skills(), PARTIAL_COVERAGE));
const { AgentSkillsPageClient } = await import(
"../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient"
);
const container = makeContainer();
root = createRoot(container);
await act(async () => {
root?.render(<AgentSkillsPageClient />);
});
const generateBtn = container.querySelector("[data-testid='generate-button']");
expect(generateBtn).not.toBeNull();
});
it("search filters cards by name", async () => {
const skills = make42Skills();
vi.stubGlobal("fetch", mockFetch(skills, FULL_COVERAGE));
const { AgentSkillsPageClient } = await import(
"../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient"
);
const container = makeContainer();
root = createRoot(container);
await act(async () => {
root?.render(<AgentSkillsPageClient />);
});
const searchInput = container.querySelector("[data-testid='search-input']") as HTMLInputElement | null;
expect(searchInput).not.toBeNull();
await act(async () => {
if (searchInput) {
searchInput.value = "API Skill 0";
searchInput.dispatchEvent(new Event("input", { bubbles: true }));
// React uses onChange
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
"value",
)?.set;
nativeInputValueSetter?.call(searchInput, "API Skill 0");
searchInput.dispatchEvent(new Event("change", { bubbles: true }));
}
});
// After search, cards with "API Skill 0" in name should be visible
// (at minimum the one exact match)
const cards = container.querySelectorAll("[data-testid^='skill-card-']");
expect(cards.length).toBeLessThanOrEqual(42);
});
it("MCP and A2A links bar is present", async () => {
vi.stubGlobal("fetch", mockFetch(make42Skills(), FULL_COVERAGE));
const { AgentSkillsPageClient } = await import(
"../../src/app/(dashboard)/dashboard/agent-skills/AgentSkillsPageClient"
);
const container = makeContainer();
root = createRoot(container);
await act(async () => {
root?.render(<AgentSkillsPageClient />);
});
const linksBar = container.querySelector("[data-testid='mcp-a2a-links-bar']");
expect(linksBar).not.toBeNull();
});
});
// ── CoverageBar isolated tests ───────────────────────────────────────────────
describe("CoverageBar", () => {
it("renders two progressbars with correct aria attributes", async () => {
const { CoverageBar } = await import(
"../../src/app/(dashboard)/dashboard/agent-skills/components/CoverageBar"
);
const container = makeContainer();
const localRoot = createRoot(container);
await act(async () => {
localRoot.render(<CoverageBar coverage={FULL_COVERAGE} />);
});
const bars = container.querySelectorAll("[role='progressbar']");
expect(bars.length).toBe(2);
const apiBar = bars[0] as HTMLElement;
expect(apiBar.getAttribute("aria-valuenow")).toBe("22");
expect(apiBar.getAttribute("aria-valuemax")).toBe("22");
const cliBar = bars[1] as HTMLElement;
expect(cliBar.getAttribute("aria-valuenow")).toBe("20");
expect(cliBar.getAttribute("aria-valuemax")).toBe("20");
await act(async () => localRoot.unmount());
});
it("applies red color class when coverage is below 75%", async () => {
const { CoverageBar } = await import(
"../../src/app/(dashboard)/dashboard/agent-skills/components/CoverageBar"
);
const lowCoverage: SkillCoverage = {
api: { have: 5, total: 22 },
cli: { have: 0, total: 20 },
totalSkills: 5,
generatedAt: new Date().toISOString(),
};
const container = makeContainer();
const localRoot = createRoot(container);
await act(async () => {
localRoot.render(<CoverageBar coverage={lowCoverage} />);
});
const bars = container.querySelectorAll("[role='progressbar']");
const apiBar = bars[0] as HTMLElement;
expect(apiBar.className).toContain("bg-red-500");
await act(async () => localRoot.unmount());
});
it("applies amber color class when coverage is between 75% and 100%", async () => {
const { CoverageBar } = await import(
"../../src/app/(dashboard)/dashboard/agent-skills/components/CoverageBar"
);
const partialCoverage: SkillCoverage = {
api: { have: 18, total: 22 }, // ~81.8% = amber
cli: { have: 15, total: 20 }, // 75% = amber
totalSkills: 33,
generatedAt: new Date().toISOString(),
};
const container = makeContainer();
const localRoot = createRoot(container);
await act(async () => {
localRoot.render(<CoverageBar coverage={partialCoverage} />);
});
const bars = container.querySelectorAll("[role='progressbar']");
const apiBar = bars[0] as HTMLElement;
expect(apiBar.className).toContain("bg-amber-400");
await act(async () => localRoot.unmount());
});
});
// ── SkillCard isolated tests ─────────────────────────────────────────────────
describe("SkillCard", () => {
it("renders skill name and description", async () => {
const { SkillCard } = await import(
"../../src/app/(dashboard)/dashboard/agent-skills/components/SkillCard"
);
const skill = makeSkill({ name: "Providers", description: "Manage connections" });
const container = makeContainer();
const localRoot = createRoot(container);
await act(async () => {
localRoot.render(<SkillCard skill={skill} selected={false} onClick={() => {}} />);
});
expect(container.textContent).toContain("Providers");
expect(container.textContent).toContain("Manage connections");
await act(async () => localRoot.unmount());
});
it("has role=button and aria-pressed=false when not selected", async () => {
const { SkillCard } = await import(
"../../src/app/(dashboard)/dashboard/agent-skills/components/SkillCard"
);
const container = makeContainer();
const localRoot = createRoot(container);
await act(async () => {
localRoot.render(<SkillCard skill={makeSkill()} selected={false} onClick={() => {}} />);
});
const btn = container.querySelector("[role='button']") as HTMLElement | null;
expect(btn).not.toBeNull();
expect(btn?.getAttribute("aria-pressed")).toBe("false");
await act(async () => localRoot.unmount());
});
it("has aria-pressed=true when selected", async () => {
const { SkillCard } = await import(
"../../src/app/(dashboard)/dashboard/agent-skills/components/SkillCard"
);
const container = makeContainer();
const localRoot = createRoot(container);
await act(async () => {
localRoot.render(<SkillCard skill={makeSkill()} selected={true} onClick={() => {}} />);
});
const btn = container.querySelector("[role='button']") as HTMLElement | null;
expect(btn?.getAttribute("aria-pressed")).toBe("true");
await act(async () => localRoot.unmount());
});
it("calls onClick when clicked", async () => {
const { SkillCard } = await import(
"../../src/app/(dashboard)/dashboard/agent-skills/components/SkillCard"
);
const handleClick = vi.fn();
const container = makeContainer();
const localRoot = createRoot(container);
await act(async () => {
localRoot.render(<SkillCard skill={makeSkill()} selected={false} onClick={handleClick} />);
});
const btn = container.querySelector("[role='button']") as HTMLElement | null;
await act(async () => btn?.click());
expect(handleClick).toHaveBeenCalledTimes(1);
await act(async () => localRoot.unmount());
});
it("shows first 2 endpoints as chips for API skill", async () => {
const { SkillCard } = await import(
"../../src/app/(dashboard)/dashboard/agent-skills/components/SkillCard"
);
const skill = makeSkill({
endpoints: ["POST /api/providers", "GET /api/providers", "DELETE /api/providers/:id"],
});
const container = makeContainer();
const localRoot = createRoot(container);
await act(async () => {
localRoot.render(<SkillCard skill={skill} selected={false} onClick={() => {}} />);
});
const chips = container.querySelectorAll("code");
expect(chips.length).toBe(2);
expect(chips[0].textContent).toBe("POST /api/providers");
expect(chips[1].textContent).toBe("GET /api/providers");
await act(async () => localRoot.unmount());
});
});
// ── SkillPreviewPane isolated tests ──────────────────────────────────────────
describe("SkillPreviewPane", () => {
it("renders empty state when skillId is null", async () => {
const { SkillPreviewPane } = await import(
"../../src/app/(dashboard)/dashboard/agent-skills/components/SkillPreviewPane"
);
const container = makeContainer();
const localRoot = createRoot(container);
await act(async () => {
localRoot.render(
<SkillPreviewPane skillId={null} markdown={null} loading={false} />,
);
});
const empty = container.querySelector("[data-testid='skill-preview-empty']");
expect(empty).not.toBeNull();
expect(container.textContent).toContain("previewEmpty");
await act(async () => localRoot.unmount());
});
it("renders markdown when skillId and markdown are provided", async () => {
const { SkillPreviewPane } = await import(
"../../src/app/(dashboard)/dashboard/agent-skills/components/SkillPreviewPane"
);
const container = makeContainer();
const localRoot = createRoot(container);
await act(async () => {
localRoot.render(
<SkillPreviewPane
skillId="omni-providers"
markdown="# Providers\nContent here."
loading={false}
/>,
);
});
const preview = container.querySelector("[data-testid='skill-preview-pane']");
expect(preview).not.toBeNull();
expect(container.textContent).toContain("Providers");
await act(async () => localRoot.unmount());
});
it("shows error state when skillId provided but markdown is empty string", async () => {
const { SkillPreviewPane } = await import(
"../../src/app/(dashboard)/dashboard/agent-skills/components/SkillPreviewPane"
);
const container = makeContainer();
const localRoot = createRoot(container);
await act(async () => {
localRoot.render(
<SkillPreviewPane skillId="omni-providers" markdown="" loading={false} />,
);
});
// markdown is "" (falsy) — should show error state
const errorEl = container.querySelector("[data-testid='skill-preview-error']");
expect(errorEl).not.toBeNull();
await act(async () => localRoot.unmount());
});
});
+186
View File
@@ -0,0 +1,186 @@
import test from "node:test";
import assert from "node:assert/strict";
// Dynamic imports for ESM + tsx compatibility
const { agentSkillTools, AgentSkillsListSchema, AgentSkillsGetSchema, AgentSkillsCoverageSchema } =
await import("../../open-sse/mcp-server/tools/agentSkillTools.ts");
const { MCP_TOOL_MAP } = await import("../../open-sse/mcp-server/schemas/tools.ts");
// ─── Schema registration in MCP_TOOL_MAP ──────────────────────────────────
test("omniroute_agent_skills_list is registered in MCP_TOOL_MAP", () => {
const tool = MCP_TOOL_MAP["omniroute_agent_skills_list"];
assert.ok(tool, "Tool should exist in MCP_TOOL_MAP");
assert.equal(tool.name, "omniroute_agent_skills_list");
assert.deepEqual(tool.scopes, ["read:catalog"]);
});
test("omniroute_agent_skills_get is registered in MCP_TOOL_MAP", () => {
const tool = MCP_TOOL_MAP["omniroute_agent_skills_get"];
assert.ok(tool, "Tool should exist in MCP_TOOL_MAP");
assert.equal(tool.name, "omniroute_agent_skills_get");
assert.deepEqual(tool.scopes, ["read:catalog"]);
});
test("omniroute_agent_skills_coverage is registered in MCP_TOOL_MAP", () => {
const tool = MCP_TOOL_MAP["omniroute_agent_skills_coverage"];
assert.ok(tool, "Tool should exist in MCP_TOOL_MAP");
assert.equal(tool.name, "omniroute_agent_skills_coverage");
assert.deepEqual(tool.scopes, ["read:catalog"]);
});
// ─── agentSkillTools object shape ────────────────────────────────────────
test("agentSkillTools exports exactly 3 tools", () => {
const keys = Object.keys(agentSkillTools);
assert.deepEqual(keys.sort(), [
"omniroute_agent_skills_coverage",
"omniroute_agent_skills_get",
"omniroute_agent_skills_list",
]);
});
test("each agentSkillTool has name, description, inputSchema, and handler", () => {
for (const toolDef of Object.values(agentSkillTools)) {
assert.ok(
typeof toolDef.name === "string" && toolDef.name.length > 0,
`${toolDef.name}: name missing`
);
assert.ok(
typeof toolDef.description === "string" && toolDef.description.length > 0,
`${toolDef.name}: description missing`
);
assert.ok(toolDef.inputSchema != null, `${toolDef.name}: inputSchema missing`);
assert.ok(typeof toolDef.handler === "function", `${toolDef.name}: handler missing`);
}
});
// ─── omniroute_agent_skills_list ────────────────────────────────────────────
test("omniroute_agent_skills_list with no filters returns all 44 skills", async () => {
const result = await agentSkillTools.omniroute_agent_skills_list.handler({});
assert.equal(result.count, 44, `Expected 44 but got ${result.count}`);
assert.ok(Array.isArray(result.skills));
assert.equal(result.skills.length, 44);
});
test("omniroute_agent_skills_list({category:'api'}) returns exactly 23 entries", async () => {
const result = await agentSkillTools.omniroute_agent_skills_list.handler({ category: "api" });
assert.equal(result.count, 23, `Expected 23 api skills but got ${result.count}`);
assert.ok(result.skills.every((s: { category: string }) => s.category === "api"));
});
test("omniroute_agent_skills_list({category:'cli'}) returns exactly 20 entries", async () => {
const result = await agentSkillTools.omniroute_agent_skills_list.handler({ category: "cli" });
assert.equal(result.count, 20, `Expected 20 cli skills but got ${result.count}`);
assert.ok(result.skills.every((s: { category: string }) => s.category === "cli"));
});
test("omniroute_agent_skills_list result includes coverage shape", async () => {
const result = await agentSkillTools.omniroute_agent_skills_list.handler({});
assert.ok(result.coverage != null, "coverage should be present");
assert.ok(typeof result.coverage.api === "object");
assert.ok(typeof result.coverage.cli === "object");
assert.equal(result.coverage.api.total, 23);
assert.equal(result.coverage.cli.total, 20);
assert.ok(typeof result.coverage.totalSkills === "number");
assert.ok(typeof result.coverage.generatedAt === "string");
});
test("omniroute_agent_skills_list skill entries have required fields", async () => {
const result = await agentSkillTools.omniroute_agent_skills_list.handler({});
const first = result.skills[0];
assert.ok(typeof first.id === "string" && first.id.length > 0);
assert.ok(typeof first.name === "string" && first.name.length > 0);
assert.ok(typeof first.description === "string");
assert.ok(first.category === "api" || first.category === "cli");
assert.ok(typeof first.area === "string");
assert.ok(typeof first.rawUrl === "string");
assert.ok(typeof first.githubUrl === "string");
});
test("AgentSkillsListSchema parses valid category filter", () => {
const parsed = AgentSkillsListSchema.parse({ category: "api" });
assert.equal(parsed.category, "api");
});
test("AgentSkillsListSchema rejects invalid category", () => {
assert.throws(() => AgentSkillsListSchema.parse({ category: "unknown" }));
});
// ─── omniroute_agent_skills_get ─────────────────────────────────────────────
// NOTE: omniroute_agent_skills_get calls fetchSkillMarkdown which tries:
// 1. local filesystem skills/{id}/SKILL.md
// 2. GitHub raw URL
// In unit test environment, SKILL.md files are not yet generated and GitHub
// raw URLs are 404 on feature branches. We test the shape contract by
// verifying: (a) metadata fields are correct, (b) a real GitHub-hosted skill
// (main branch) resolves, or (c) a skill not found throws correctly.
// The deep integration (fetch round-trip) is covered by e2e/ecosystem tests.
test("omniroute_agent_skills_get({id:'omni-providers'}) returns correct skill metadata before markdown fetch", async () => {
const { getSkillById } = await import("../../src/lib/agentSkills/catalog.ts");
const skill = getSkillById("omni-providers");
assert.ok(skill != null, "omni-providers should exist in catalog");
assert.equal(skill!.id, "omni-providers");
assert.equal(skill!.category, "api");
assert.ok(typeof skill!.name === "string" && skill!.name.length > 0);
assert.ok(typeof skill!.rawUrl === "string");
assert.ok(typeof skill!.githubUrl === "string");
});
test("omniroute_agent_skills_get({id:'cli-serve'}) resolves correct cli skill metadata", async () => {
const { getSkillById } = await import("../../src/lib/agentSkills/catalog.ts");
const skill = getSkillById("cli-serve");
assert.ok(skill != null, "cli-serve should exist in catalog");
assert.equal(skill!.id, "cli-serve");
assert.equal(skill!.category, "cli");
assert.ok(typeof skill!.name === "string" && skill!.name.length > 0);
});
test("omniroute_agent_skills_get with invalid id throws Error", async () => {
await assert.rejects(
() => agentSkillTools.omniroute_agent_skills_get.handler({ id: "non-existent-skill-xyz" }),
(err: unknown) => {
assert.ok(err instanceof Error);
assert.ok(err.message.includes("non-existent-skill-xyz"));
return true;
}
);
});
test("AgentSkillsGetSchema requires id field", () => {
assert.throws(() => AgentSkillsGetSchema.parse({}));
});
test("AgentSkillsGetSchema parses valid id", () => {
const parsed = AgentSkillsGetSchema.parse({ id: "omni-providers" });
assert.equal(parsed.id, "omni-providers");
});
// ─── omniroute_agent_skills_coverage ────────────────────────────────────────
test("omniroute_agent_skills_coverage({}) returns coverage shape", async () => {
const result = await agentSkillTools.omniroute_agent_skills_coverage.handler({});
assert.ok(result != null);
assert.ok(typeof result.api === "object");
assert.ok(typeof result.cli === "object");
assert.equal(result.api.total, 23);
assert.equal(result.cli.total, 20);
assert.ok(typeof result.api.have === "number");
assert.ok(typeof result.cli.have === "number");
assert.ok(result.api.have >= 0 && result.api.have <= 23);
assert.ok(result.cli.have >= 0 && result.cli.have <= 20);
assert.ok(typeof result.totalSkills === "number");
assert.equal(result.totalSkills, result.api.have + result.cli.have + (result.config?.have ?? 0));
assert.ok(typeof result.generatedAt === "string");
// Validate ISO datetime format
assert.ok(!isNaN(Date.parse(result.generatedAt)), "generatedAt should be valid ISO datetime");
});
test("AgentSkillsCoverageSchema parses empty input", () => {
const parsed = AgentSkillsCoverageSchema.parse({});
assert.deepEqual(parsed, {});
});
+260
View File
@@ -0,0 +1,260 @@
import test from "node:test";
import assert from "node:assert/strict";
// Dynamic imports to pick up ESM modules with tsx
const {
getCatalog,
getSkillById,
filterCatalog,
computeCoverage,
refreshCatalog,
API_SKILL_IDS,
CLI_SKILL_IDS,
} = await import("../../src/lib/agentSkills/catalog.ts");
const agentSkillsConstants = await import("../../src/shared/constants/agentSkills.ts");
// ─── Counts ───────────────────────────────────────────────────────────────────
test("getCatalog() returns exactly 44 entries", () => {
refreshCatalog();
const catalog = getCatalog();
assert.equal(catalog.length, 44, `Expected 44 but got ${catalog.length}`);
});
test("API_SKILL_IDS has exactly 23 entries", () => {
assert.equal(API_SKILL_IDS.length, 23);
});
test("CLI_SKILL_IDS has exactly 20 entries", () => {
assert.equal(CLI_SKILL_IDS.length, 20);
});
test("getCatalog() contains exactly 22 api skills", () => {
const apiSkills = getCatalog().filter((s) => s.category === "api");
assert.equal(apiSkills.length, 23);
});
test("getCatalog() contains exactly 20 cli skills", () => {
const cliSkills = getCatalog().filter((s) => s.category === "cli");
assert.equal(cliSkills.length, 20);
});
// ─── ID format ────────────────────────────────────────────────────────────────
test("all skill IDs match regex ^[a-z][a-z0-9-]*$", () => {
const ID_REGEX = /^[a-z][a-z0-9-]*$/;
for (const skill of getCatalog()) {
assert.match(skill.id, ID_REGEX, `Skill ID "${skill.id}" does not match expected format`);
}
});
test("all skill IDs are unique (no duplicates)", () => {
const ids = getCatalog().map((s) => s.id);
const uniqueIds = new Set(ids);
assert.equal(
uniqueIds.size,
ids.length,
`Duplicate IDs found: ${ids.filter((id, i) => ids.indexOf(id) !== i).join(", ")}`
);
});
// ─── Required fields ──────────────────────────────────────────────────────────
test("all skills have non-empty name and description", () => {
for (const skill of getCatalog()) {
assert.ok(skill.name.length > 0, `Skill ${skill.id} has empty name`);
assert.ok(skill.description.length > 0, `Skill ${skill.id} has empty description`);
}
});
test("all skills have rawUrl and githubUrl as valid GitHub URLs", () => {
for (const skill of getCatalog()) {
assert.ok(
skill.rawUrl.startsWith("https://raw.githubusercontent.com/"),
`Skill ${skill.id}: rawUrl "${skill.rawUrl}" is not a GitHub raw URL`
);
assert.ok(
skill.githubUrl.startsWith("https://github.com/"),
`Skill ${skill.id}: githubUrl "${skill.githubUrl}" is not a GitHub blob URL`
);
assert.ok(
skill.rawUrl.endsWith("/SKILL.md"),
`Skill ${skill.id}: rawUrl does not end with /SKILL.md`
);
}
});
test("agent skills constants expose URL builders without the unused repository URL", () => {
assert.equal(typeof agentSkillsConstants.getAgentSkillRawUrl, "function");
assert.equal(typeof agentSkillsConstants.getAgentSkillBlobUrl, "function");
assert.equal("AGENT_SKILLS_REPO_URL" in agentSkillsConstants, false);
});
test("api skills have area matching API_SKILL_IDS derived IDs", () => {
const catalog = getCatalog();
for (const id of API_SKILL_IDS) {
const skill = catalog.find((s) => s.id === id);
assert.ok(skill, `API skill ID "${id}" not found in catalog`);
assert.equal(
skill!.category,
"api",
`Skill "${id}" expected category api, got ${skill!.category}`
);
}
});
test("cli skills have area matching CLI_SKILL_IDS derived IDs", () => {
const catalog = getCatalog();
for (const id of CLI_SKILL_IDS) {
const skill = catalog.find((s) => s.id === id);
assert.ok(skill, `CLI skill ID "${id}" not found in catalog`);
assert.equal(
skill!.category,
"cli",
`Skill "${id}" expected category cli, got ${skill!.category}`
);
}
});
// ─── getSkillById ─────────────────────────────────────────────────────────────
test("getSkillById('omni-providers') returns the omni-providers entry", () => {
const skill = getSkillById("omni-providers");
assert.ok(skill, "Expected skill to be found");
assert.equal(skill!.id, "omni-providers");
assert.equal(skill!.category, "api");
assert.equal(skill!.area, "providers");
});
test("getSkillById('cli-serve') returns the cli-serve entry", () => {
const skill = getSkillById("cli-serve");
assert.ok(skill);
assert.equal(skill!.id, "cli-serve");
assert.equal(skill!.category, "cli");
assert.equal(skill!.isEntry, true);
});
test("getSkillById('omni-auth') returns entry with isEntry=true", () => {
const skill = getSkillById("omni-auth");
assert.ok(skill);
assert.equal(skill!.isEntry, true);
});
test("getSkillById('does-not-exist') returns null", () => {
const skill = getSkillById("does-not-exist");
assert.equal(skill, null);
});
test("getSkillById('') returns null", () => {
const skill = getSkillById("");
assert.equal(skill, null);
});
// ─── filterCatalog ────────────────────────────────────────────────────────────
test("filterCatalog({ category: 'api' }) returns 23 api skills", () => {
const skills = filterCatalog({ category: "api" });
assert.equal(skills.length, 23);
for (const s of skills) {
assert.equal(s.category, "api");
}
});
test("filterCatalog({ category: 'cli' }) returns 20 cli skills", () => {
const skills = filterCatalog({ category: "cli" });
assert.equal(skills.length, 20);
for (const s of skills) {
assert.equal(s.category, "cli");
}
});
test("filterCatalog({ area: 'providers' }) returns exactly omni-providers", () => {
const skills = filterCatalog({ area: "providers" });
assert.equal(skills.length, 1);
assert.equal(skills[0].id, "omni-providers");
});
test("filterCatalog({ category: 'api', area: 'mcp' }) returns omni-mcp", () => {
const skills = filterCatalog({ category: "api", area: "mcp" });
assert.equal(skills.length, 1);
assert.equal(skills[0].id, "omni-mcp");
});
test("filterCatalog({ area: 'nonexistent' }) returns empty array", () => {
const skills = filterCatalog({ area: "nonexistent" });
assert.equal(skills.length, 0);
});
test("filterCatalog({}) returns full catalog (44 entries)", () => {
const skills = filterCatalog({});
assert.equal(skills.length, 44);
});
// ─── refreshCatalog ───────────────────────────────────────────────────────────
test("refreshCatalog() causes getCatalog() to re-derive (returns fresh array)", () => {
const first = getCatalog();
refreshCatalog();
const second = getCatalog();
// Different array reference after refresh
assert.notEqual(first, second);
// But same content
assert.equal(first.length, second.length);
assert.equal(first[0].id, second[0].id);
});
// ─── computeCoverage ─────────────────────────────────────────────────────────
test("computeCoverage() returns valid SkillCoverage shape", () => {
const cov = computeCoverage();
assert.ok(typeof cov.api === "object");
assert.equal(cov.api.total, 23);
assert.ok(typeof cov.api.have === "number");
assert.ok(cov.api.have >= 0 && cov.api.have <= 23);
assert.ok(typeof cov.cli === "object");
assert.equal(cov.cli.total, 20);
assert.ok(typeof cov.cli.have === "number");
assert.ok(cov.cli.have >= 0 && cov.cli.have <= 20);
assert.equal(cov.totalSkills, cov.api.have + cov.cli.have + (cov.config?.have ?? 0));
// generatedAt must be a valid ISO datetime string
assert.ok(
!isNaN(Date.parse(cov.generatedAt)),
`generatedAt "${cov.generatedAt}" is not a valid ISO date`
);
});
test("computeCoverage() api.have + cli.have = totalSkills", () => {
const cov = computeCoverage();
assert.equal(cov.totalSkills, cov.api.have + cov.cli.have + (cov.config?.have ?? 0));
});
// ─── Cache behaviour ─────────────────────────────────────────────────────────
test("getCatalog() returns the same array reference on repeated calls (cached)", () => {
refreshCatalog();
const first = getCatalog();
const second = getCatalog();
assert.strictEqual(first, second, "Expected same cached array reference");
});
// ─── Canonical IDs check ─────────────────────────────────────────────────────
test("API_SKILL_IDS first entry is omni-auth", () => {
assert.equal(API_SKILL_IDS[0], "omni-auth");
});
test("API_SKILL_IDS last entry is omni-github-skills", () => {
assert.equal(API_SKILL_IDS[API_SKILL_IDS.length - 1], "omni-github-skills");
});
test("CLI_SKILL_IDS first entry is cli-serve", () => {
assert.equal(CLI_SKILL_IDS[0], "cli-serve");
});
test("CLI_SKILL_IDS last entry is cli-setup", () => {
assert.equal(CLI_SKILL_IDS[CLI_SKILL_IDS.length - 1], "cli-setup");
});
@@ -0,0 +1,304 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import os from "node:os";
// Dynamic import to pick up ESM module
const { parseCliRegistry, getCommandsForFamily } = await import(
"../../src/lib/agentSkills/cliRegistryParser.ts"
);
// ─── Fixture helpers ──────────────────────────────────────────────────────────
/**
* Creates a temporary directory mirroring bin/cli/commands/,
* writes fixture .mjs files, changes CWD, returns cleanup fn.
*/
function withFixtureCli(
files: Record<string, string>,
): { cleanup: () => void } {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "omni-cli-test-"));
const commandsDir = path.join(tmpDir, "bin", "cli", "commands");
fs.mkdirSync(commandsDir, { recursive: true });
for (const [filename, content] of Object.entries(files)) {
fs.writeFileSync(path.join(commandsDir, filename), content, "utf-8");
}
const originalCwd = process.cwd();
process.chdir(tmpDir);
return {
cleanup() {
process.chdir(originalCwd);
fs.rmSync(tmpDir, { recursive: true, force: true });
},
};
}
// ─── Fixture content ──────────────────────────────────────────────────────────
const FIXTURE_PROVIDERS_MJS = `
export function registerProviders(program) {
const providers = program.command('providers').description('Manage provider connections');
providers
.command('list')
.description('List configured provider connections')
.option('--json', 'Print machine-readable JSON')
.action(async (opts) => {});
providers
.command('available')
.description('Show available providers in the catalog')
.option('--search <query>', 'Filter by id or name')
.option('--category <category>', 'Filter by category')
.action(async (opts) => {});
providers
.command('test <idOrName>')
.description('Test a configured provider connection')
.action(async (idOrName, opts) => {});
providers
.command('test-all')
.description('Test all active provider connections')
.action(async (opts) => {});
providers
.command('validate')
.description('Validate local provider configuration')
.action(async (opts) => {});
providers
.command('rotate <idOrName>')
.description('Rotate API key for a provider connection')
.option('--new-key <key>', 'New API key value')
.option('--dry-run', 'Preview without writing')
.action(async (idOrName, opts) => {});
providers
.command('status')
.description('Show provider connection status and expiry')
.option('--json', 'JSON output')
.action(async (opts) => {});
}
`;
const FIXTURE_HEALTH_MJS = `
export function registerHealth(program) {
const health = program
.command('health')
.description('Check server health status')
.option('-v, --verbose', 'Show extended info')
.option('--json', 'Output as JSON')
.action(async (opts) => {});
health
.command('components')
.description('List health components and status')
.action(async (opts) => {});
health
.command('watch')
.description('Live dashboard — refresh every N seconds')
.option('--interval <s>', 'Refresh interval in seconds')
.action(async (opts) => {});
}
`;
const FIXTURE_KEYS_MJS = `
export function registerKeys(program) {
const keys = program.command('keys').description('Manage OmniRoute API keys');
keys
.command('list')
.description('List all API keys')
.option('--json', 'JSON output')
.action(async (opts) => {});
keys
.command('create')
.description('Create a new API key')
.option('--name <name>', 'Key name')
.action(async (opts) => {});
keys
.command('revoke <id>')
.description('Revoke an API key')
.action(async (id, opts) => {});
}
`;
// ─── Tests ────────────────────────────────────────────────────────────────────
test("parseCliRegistry() returns commands Map and families Map", () => {
const { cleanup } = withFixtureCli({
"providers.mjs": FIXTURE_PROVIDERS_MJS,
"health.mjs": FIXTURE_HEALTH_MJS,
});
try {
const result = parseCliRegistry();
assert.ok(result.commands instanceof Map, "commands should be a Map");
assert.ok(result.families instanceof Map, "families should be a Map");
assert.ok(result.commands.size > 0, "commands should not be empty");
assert.ok(result.families.size > 0, "families should not be empty");
} finally {
cleanup();
}
});
test("parseCliRegistry() recognises providers family with ≥5 subcommands", () => {
const { cleanup } = withFixtureCli({
"providers.mjs": FIXTURE_PROVIDERS_MJS,
});
try {
const { families } = parseCliRegistry();
const providerCmds = families.get("cli-providers");
assert.ok(providerCmds, "Expected 'cli-providers' family to exist");
assert.ok(
providerCmds!.length >= 5,
`Expected ≥5 provider commands, got ${providerCmds!.length}: ${providerCmds!.map((c) => c.name).join(", ")}`,
);
} finally {
cleanup();
}
});
test("parseCliRegistry() recognises health family commands", () => {
const { cleanup } = withFixtureCli({
"health.mjs": FIXTURE_HEALTH_MJS,
});
try {
const { families } = parseCliRegistry();
const healthCmds = families.get("cli-health");
assert.ok(healthCmds, "Expected 'cli-health' family to exist");
assert.ok(
healthCmds!.length >= 2,
`Expected ≥2 health commands, got ${healthCmds!.length}`,
);
} finally {
cleanup();
}
});
test("parseCliRegistry() extracts description for each command", () => {
const { cleanup } = withFixtureCli({
"providers.mjs": FIXTURE_PROVIDERS_MJS,
"keys.mjs": FIXTURE_KEYS_MJS,
});
try {
const { commands } = parseCliRegistry();
// Top-level providers command should have description
const providers = [...commands.values()].find((c) => c.name === "providers");
assert.ok(providers, "Expected providers command");
assert.ok(
providers!.description.length > 0,
`Expected non-empty description for providers, got: "${providers!.description}"`,
);
} finally {
cleanup();
}
});
test("parseCliRegistry() marks subcommands with isSubcommand=true (after first)", () => {
const { cleanup } = withFixtureCli({
"providers.mjs": FIXTURE_PROVIDERS_MJS,
});
try {
const { families } = parseCliRegistry();
const providerCmds = families.get("cli-providers")!;
// After the first (top-level) entry, rest should be subcommands
const subCmds = providerCmds.filter((c) => c.isSubcommand);
assert.ok(
subCmds.length >= 4,
`Expected ≥4 subcommands (list, available, test, etc.), got ${subCmds.length}`,
);
} finally {
cleanup();
}
});
test("parseCliRegistry() extracts flags from .option() calls", () => {
const { cleanup } = withFixtureCli({
"providers.mjs": FIXTURE_PROVIDERS_MJS,
});
try {
const { commands } = parseCliRegistry();
// Find a command that has options
const rotate = [...commands.values()].find((c) => c.name.includes("rotate"));
// Flags might be present if parsing found them
if (rotate) {
// If rotate exists and has flags, verify format
for (const flag of rotate.flags) {
assert.ok(
typeof flag === "string" && flag.length > 0,
`Invalid flag: "${flag}"`,
);
}
}
} finally {
cleanup();
}
});
test("parseCliRegistry() skips unrecognised .mjs files", () => {
const { cleanup } = withFixtureCli({
"unknown-custom.mjs": `export function register(p) {}`,
"providers.mjs": FIXTURE_PROVIDERS_MJS,
});
try {
const { families } = parseCliRegistry();
// No family should be mapped from unknown-custom
const hasUnknown = [...families.keys()].some((k) =>
String(k).includes("unknown-custom"),
);
assert.equal(hasUnknown, false, "unknown-custom.mjs should not create a family");
// providers.mjs should still be parsed
assert.ok(families.has("cli-providers"), "Expected cli-providers family from providers.mjs");
} finally {
cleanup();
}
});
test("parseCliRegistry() throws if commands directory is missing", () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "omni-cli-missing-"));
const originalCwd = process.cwd();
process.chdir(tmpDir);
try {
assert.throws(
() => parseCliRegistry(),
/cliRegistryParser: could not read/,
"Expected error when commands dir is missing",
);
} finally {
process.chdir(originalCwd);
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
// ─── Integration test: real providers.mjs (always runs — it's in the repo) ───
test("parseCliRegistry() with real providers.mjs: providers family has ≥5 commands", () => {
// This test uses the actual project files (not a fixture).
// We rely on the CWD being the worktree root during `npm run test:unit`.
const result = parseCliRegistry();
const providerCmds = result.families.get("cli-providers");
assert.ok(providerCmds, "Expected cli-providers family from real providers.mjs");
assert.ok(
providerCmds!.length >= 5,
`Expected ≥5 real provider commands, got ${providerCmds!.length}: ${providerCmds!.map((c) => c.name).join(", ")}`,
);
});
test("getCommandsForFamily('cli-providers') with real files: returns ≥5 strings", () => {
const commands = getCommandsForFamily("cli-providers");
assert.ok(
commands.length >= 5,
`Expected ≥5 cli-providers commands, got ${commands.length}`,
);
for (const cmd of commands) {
assert.ok(typeof cmd === "string" && cmd.length > 0, `Invalid command name: "${cmd}"`);
}
});
+561
View File
@@ -0,0 +1,561 @@
/**
* agentSkills-generator.test.ts
*
* Tests for src/lib/agentSkills/generator.ts
*
* All tests use a temporary directory so they never touch the real `skills/` folder.
* Uses Node.js native test runner.
*/
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import os from "node:os";
// ── Dynamic imports (tsx/esm resolves TS imports) ────────────────────────────
const { generateAgentSkills, buildSkillMarkdown, __testing } = await import(
"../../src/lib/agentSkills/generator.ts"
);
const { getCatalog, refreshCatalog } = await import(
"../../src/lib/agentSkills/catalog.ts"
);
// ── Helpers ──────────────────────────────────────────────────────────────────
/** Creates an isolated tmp directory and returns its path. */
function mkTmpDir(): string {
return fs.mkdtempSync(path.join(os.tmpdir(), "agent-skills-gen-test-"));
}
/** Cleanup a tmp directory. */
function rmTmpDir(dir: string): void {
try {
fs.rmSync(dir, { recursive: true, force: true });
} catch {
// best-effort cleanup
}
}
/**
* Builds a minimal sources object for tests that don't need real parsers.
*/
function emptySources() {
return {
openapi: { paths: new Map(), areas: new Map() },
cliRegistry: { commands: new Map(), families: new Map() },
};
}
// ── Dry-run: no writes ────────────────────────────────────────────────────────
test("dry-run (default) returns report without writing any files", async () => {
const tmpDir = mkTmpDir();
try {
refreshCatalog();
const report = await generateAgentSkills({
dryRun: true,
prune: false,
outputDir: tmpDir,
});
// All 44 skills should appear as generated (would-write) since dir is empty
assert.equal(
report.generated.length + report.unchanged.length,
44,
`Expected 44 total (generated+unchanged), got generated=${report.generated.length} unchanged=${report.unchanged.length}`,
);
assert.equal(report.errors.length, 0, `Unexpected errors: ${JSON.stringify(report.errors)}`);
// No files written
const entries = fs.readdirSync(tmpDir);
assert.equal(
entries.length,
0,
`Dry-run wrote ${entries.length} entries to ${tmpDir}: ${entries.join(", ")}`,
);
} finally {
rmTmpDir(tmpDir);
}
});
test("dry-run generates report with 44 total (generated+unchanged)", async () => {
const tmpDir = mkTmpDir();
try {
refreshCatalog();
const report = await generateAgentSkills({
dryRun: true,
prune: false,
outputDir: tmpDir,
});
const total = report.generated.length + report.unchanged.length;
assert.equal(total, 44);
} finally {
rmTmpDir(tmpDir);
}
});
// ── Apply: writes SKILL.md ─────────────────────────────────────────────────────
test("apply mode writes SKILL.md with valid frontmatter for omni-providers", async () => {
const tmpDir = mkTmpDir();
try {
refreshCatalog();
const report = await generateAgentSkills({
dryRun: false,
prune: false,
outputDir: tmpDir,
onlyIds: ["omni-providers"],
});
assert.equal(report.errors.length, 0, `Errors: ${JSON.stringify(report.errors)}`);
assert.equal(report.generated.length, 1);
assert.equal(report.generated[0], "omni-providers");
const skillFile = path.join(tmpDir, "omni-providers", "SKILL.md");
assert.ok(fs.existsSync(skillFile), `SKILL.md not found at ${skillFile}`);
const content = fs.readFileSync(skillFile, "utf-8");
// Frontmatter present
assert.ok(content.startsWith("---\n"), "Missing frontmatter start");
assert.ok(content.includes("name: omni-providers"), "Missing name in frontmatter");
assert.ok(content.includes("---\n"), "Missing frontmatter end");
// Generated comment present
assert.ok(
content.includes("<!-- generated by src/lib/agentSkills/generator.ts"),
"Missing generated comment",
);
} finally {
rmTmpDir(tmpDir);
}
});
test("apply mode writes all 44 SKILL.md files when no onlyIds filter", async () => {
const tmpDir = mkTmpDir();
try {
refreshCatalog();
const report = await generateAgentSkills({
dryRun: false,
prune: false,
outputDir: tmpDir,
});
assert.equal(report.errors.length, 0, `Errors: ${JSON.stringify(report.errors)}`);
assert.equal(report.generated.length, 44);
// Verify all dirs exist
const catalog = getCatalog();
for (const skill of catalog) {
const skillFile = path.join(tmpDir, skill.id, "SKILL.md");
assert.ok(fs.existsSync(skillFile), `Missing SKILL.md for ${skill.id}`);
}
} finally {
rmTmpDir(tmpDir);
}
});
test("apply mode writes SKILL.md for a CLI skill with correct structure", async () => {
const tmpDir = mkTmpDir();
try {
refreshCatalog();
const report = await generateAgentSkills({
dryRun: false,
prune: false,
outputDir: tmpDir,
onlyIds: ["cli-serve"],
});
assert.equal(report.errors.length, 0);
assert.equal(report.generated.length, 1);
const content = fs.readFileSync(path.join(tmpDir, "cli-serve", "SKILL.md"), "utf-8");
assert.ok(content.includes("name: cli-serve"), "Missing name in frontmatter");
assert.ok(content.includes("## Overview"), "Missing Overview section");
assert.ok(content.includes("## Quick install"), "Missing Quick install section");
assert.ok(content.includes("## Subcommands"), "Missing Subcommands section");
} finally {
rmTmpDir(tmpDir);
}
});
test("apply mode writes SKILL.md for an API skill with correct sections", async () => {
const tmpDir = mkTmpDir();
try {
refreshCatalog();
const report = await generateAgentSkills({
dryRun: false,
prune: false,
outputDir: tmpDir,
onlyIds: ["omni-auth"],
});
assert.equal(report.errors.length, 0);
const content = fs.readFileSync(path.join(tmpDir, "omni-auth", "SKILL.md"), "utf-8");
assert.ok(content.includes("## Overview"), "Missing Overview section");
assert.ok(content.includes("## Authentication"), "Missing Authentication section");
assert.ok(content.includes("## Endpoints"), "Missing endpoints section");
assert.ok(content.includes("## Payloads"), "Missing payloads section");
} finally {
rmTmpDir(tmpDir);
}
});
// ── Idempotency ───────────────────────────────────────────────────────────────
test("idempotency: second apply run reports 0 generated, all unchanged", async () => {
const tmpDir = mkTmpDir();
try {
refreshCatalog();
// First run: apply
const report1 = await generateAgentSkills({
dryRun: false,
prune: false,
outputDir: tmpDir,
onlyIds: ["omni-providers", "cli-serve"],
});
assert.equal(report1.generated.length, 2, "First run should generate 2 skills");
// Second run: same options
const report2 = await generateAgentSkills({
dryRun: false,
prune: false,
outputDir: tmpDir,
onlyIds: ["omni-providers", "cli-serve"],
});
assert.equal(report2.generated.length, 0, "Second run should generate 0 (already up-to-date)");
assert.equal(report2.unchanged.length, 2, "Second run should report 2 unchanged");
} finally {
rmTmpDir(tmpDir);
}
});
// ── Prune: detect orphans ─────────────────────────────────────────────────────
test("prune dry-run detects orphan dirs without deleting them", async () => {
const tmpDir = mkTmpDir();
try {
refreshCatalog();
// Create an orphan directory (not in catalog)
const orphanDir = path.join(tmpDir, "old-orphan-skill");
fs.mkdirSync(orphanDir, { recursive: true });
fs.writeFileSync(path.join(orphanDir, "SKILL.md"), "# Old\n");
const report = await generateAgentSkills({
dryRun: true,
prune: true,
outputDir: tmpDir,
});
assert.ok(report.orphansDetected.includes("old-orphan-skill"), "Orphan not detected");
assert.equal(report.pruned.length, 0, "Nothing should be pruned in dry-run");
// Orphan dir still exists
assert.ok(fs.existsSync(orphanDir), "Orphan dir was deleted in dry-run (should not happen)");
} finally {
rmTmpDir(tmpDir);
}
});
test("prune apply mode deletes orphan dirs", async () => {
const tmpDir = mkTmpDir();
try {
refreshCatalog();
// Create an orphan directory
const orphanDir = path.join(tmpDir, "old-orphan-to-delete");
fs.mkdirSync(orphanDir, { recursive: true });
fs.writeFileSync(path.join(orphanDir, "SKILL.md"), "# Old\n");
const report = await generateAgentSkills({
dryRun: false,
prune: true,
outputDir: tmpDir,
});
assert.ok(report.orphansDetected.includes("old-orphan-to-delete"), "Orphan not detected");
assert.ok(report.pruned.includes("old-orphan-to-delete"), "Orphan not pruned");
// Orphan dir should be gone
assert.ok(
!fs.existsSync(orphanDir),
"Orphan dir was NOT deleted in apply+prune mode",
);
} finally {
rmTmpDir(tmpDir);
}
});
test("prune does not delete catalog skill dirs", async () => {
const tmpDir = mkTmpDir();
try {
refreshCatalog();
// Pre-create a valid skill dir
const validDir = path.join(tmpDir, "omni-providers");
fs.mkdirSync(validDir, { recursive: true });
fs.writeFileSync(path.join(validDir, "SKILL.md"), "# Existing\n");
// Add an orphan
const orphanDir = path.join(tmpDir, "orphan-xyz");
fs.mkdirSync(orphanDir, { recursive: true });
const report = await generateAgentSkills({
dryRun: false,
prune: true,
outputDir: tmpDir,
onlyIds: [],
});
// omni-providers should not be in orphansDetected
assert.ok(
!report.orphansDetected.includes("omni-providers"),
"Valid skill mistakenly flagged as orphan",
);
assert.ok(report.orphansDetected.includes("orphan-xyz"), "Orphan not detected");
} finally {
rmTmpDir(tmpDir);
}
});
// ── Marker preservation ───────────────────────────────────────────────────────
test("marker preservation: custom block survives regeneration", async () => {
const tmpDir = mkTmpDir();
try {
refreshCatalog();
// First apply to create the file
await generateAgentSkills({
dryRun: false,
prune: false,
outputDir: tmpDir,
onlyIds: ["omni-providers"],
});
const skillFile = path.join(tmpDir, "omni-providers", "SKILL.md");
const originalContent = fs.readFileSync(skillFile, "utf-8");
// Inject a custom block
const customBlock = "<!-- skill:custom-start -->\nMy custom content here.\n<!-- skill:custom-end -->";
const contentWithCustom = originalContent + "\n" + customBlock + "\n";
fs.writeFileSync(skillFile, contentWithCustom, "utf-8");
// Re-run generator — file is now different (custom block changed content)
const report2 = await generateAgentSkills({
dryRun: false,
prune: false,
outputDir: tmpDir,
onlyIds: ["omni-providers"],
});
assert.equal(
report2.errors.length,
0,
`Errors: ${JSON.stringify(report2.errors)}`,
);
const newContent = fs.readFileSync(skillFile, "utf-8");
// Custom block should still be present
assert.ok(
newContent.includes("My custom content here."),
"Custom content was lost during regeneration",
);
assert.ok(
newContent.includes("<!-- skill:custom-start -->"),
"Custom start marker missing",
);
assert.ok(
newContent.includes("<!-- skill:custom-end -->"),
"Custom end marker missing",
);
} finally {
rmTmpDir(tmpDir);
}
});
// ── buildSkillMarkdown ─────────────────────────────────────────────────────────
test("buildSkillMarkdown returns valid frontmatter + body for omni-providers", () => {
refreshCatalog();
const sources = emptySources();
const result = buildSkillMarkdown("omni-providers", sources);
assert.ok(typeof result.frontmatter === "object", "frontmatter must be an object");
assert.equal(result.frontmatter.name, "omni-providers");
assert.ok(result.frontmatter.description.length > 0, "description must be non-empty");
assert.ok(typeof result.body === "string" && result.body.length > 0, "body must be a non-empty string");
});
test("buildSkillMarkdown body has no erroneously escaped characters", () => {
refreshCatalog();
const sources = emptySources();
// Test several skills
const testIds = ["omni-providers", "cli-serve", "omni-auth", "cli-health"];
for (const id of testIds) {
const result = buildSkillMarkdown(id, sources);
// Check for common escape errors: \\n in rendered text, &amp;, &lt;, &gt;
assert.ok(
!result.body.includes("\\\\n"),
`Skill ${id}: body contains \\\\n (double-escaped newline)`,
);
assert.ok(
!result.body.includes("&amp;"),
`Skill ${id}: body contains HTML entity &amp;`,
);
assert.ok(
!result.body.includes("&lt;"),
`Skill ${id}: body contains HTML entity &lt;`,
);
assert.ok(
!result.body.includes("&gt;"),
`Skill ${id}: body contains HTML entity &gt;`,
);
}
});
test("buildSkillMarkdown throws for unknown skillId", () => {
refreshCatalog();
const sources = emptySources();
assert.throws(
() => buildSkillMarkdown("non-existent-skill", sources),
/non-existent-skill/,
"Should throw with skill ID in message",
);
});
test("buildSkillMarkdown API skill body contains expected sections", () => {
refreshCatalog();
const sources = emptySources();
const result = buildSkillMarkdown("omni-cache", sources);
assert.ok(result.body.includes("## Overview"), "Missing Overview section");
assert.ok(result.body.includes("## Authentication"), "Missing Authentication section");
assert.ok(result.body.includes("## Endpoints"), "Missing Endpoints section");
assert.ok(result.body.includes("## Payloads"), "Missing Payloads section");
});
test("buildSkillMarkdown CLI skill body contains expected sections", () => {
refreshCatalog();
const sources = emptySources();
const result = buildSkillMarkdown("cli-health", sources);
assert.ok(result.body.includes("## Overview"), "Missing Overview section");
assert.ok(result.body.includes("## Quick install"), "Missing Quick install section");
assert.ok(result.body.includes("## Subcommands"), "Missing Subcommands section");
});
test("buildSkillMarkdown description is at most 2000 chars", () => {
refreshCatalog();
const catalog = getCatalog();
const sources = emptySources();
for (const skill of catalog) {
const result = buildSkillMarkdown(skill.id, sources);
assert.ok(
result.frontmatter.description.length <= 2000,
`Skill ${skill.id}: description too long (${result.frontmatter.description.length} > 2000)`,
);
}
});
// ── onlyIds filter ────────────────────────────────────────────────────────────
test("onlyIds filter limits generation to specified IDs", async () => {
const tmpDir = mkTmpDir();
try {
refreshCatalog();
const report = await generateAgentSkills({
dryRun: false,
prune: false,
outputDir: tmpDir,
onlyIds: ["omni-providers", "cli-serve"],
});
assert.equal(report.generated.length, 2);
assert.ok(report.generated.includes("omni-providers"));
assert.ok(report.generated.includes("cli-serve"));
// Only 2 dirs created
const entries = fs.readdirSync(tmpDir);
assert.equal(entries.length, 2, `Expected 2 dirs, got: ${entries.join(", ")}`);
} finally {
rmTmpDir(tmpDir);
}
});
// ── Generated comment ─────────────────────────────────────────────────────────
test("generated SKILL.md contains the mandatory generated comment", async () => {
const tmpDir = mkTmpDir();
try {
refreshCatalog();
await generateAgentSkills({
dryRun: false,
prune: false,
outputDir: tmpDir,
onlyIds: ["omni-providers"],
});
const content = fs.readFileSync(path.join(tmpDir, "omni-providers", "SKILL.md"), "utf-8");
assert.ok(
content.includes("<!-- generated by src/lib/agentSkills/generator.ts; manual edits will be overwritten -->"),
"Missing mandatory generated comment",
);
} finally {
rmTmpDir(tmpDir);
}
});
// ── Report shape ──────────────────────────────────────────────────────────────
test("report has all required fields with correct types", async () => {
const tmpDir = mkTmpDir();
try {
refreshCatalog();
const report = await generateAgentSkills({
dryRun: true,
prune: false,
outputDir: tmpDir,
});
assert.ok(Array.isArray(report.generated), "generated must be an array");
assert.ok(Array.isArray(report.unchanged), "unchanged must be an array");
assert.ok(Array.isArray(report.pruned), "pruned must be an array");
assert.ok(Array.isArray(report.orphansDetected), "orphansDetected must be an array");
assert.ok(Array.isArray(report.errors), "errors must be an array");
} finally {
rmTmpDir(tmpDir);
}
});
// ── serializeFrontmatter escaping (js/incomplete-sanitization regression) ──────
test("serializeFrontmatter escapes backslashes before quotes → valid round-trippable YAML", async () => {
const { parse } = await import("yaml");
const fm = {
name: 'name with "quote"',
description: 'line1\nline2: path C:\\Users\\x with "q" and trailing \\',
};
const block = __testing.serializeFrontmatter(fm);
// Strip the --- fences and parse the inner YAML; broken escaping (e.g. an
// unescaped trailing backslash that escapes the closing quote) would throw or
// corrupt the value here.
const inner = block.replace(/^---\n/, "").replace(/---\n?$/, "");
const parsed = parse(inner) as { name: string; description: string };
assert.equal(parsed.name, fm.name, "name must round-trip through YAML unchanged");
assert.equal(
parsed.description,
fm.description,
"description with backslashes + quotes + newline must round-trip exactly"
);
});
@@ -0,0 +1,250 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import os from "node:os";
// Dynamic import to pick up ESM module
const { parseOpenapi, getEndpointsForArea } =
await import("../../src/lib/agentSkills/openapiParser.ts");
// ─── Fixture helpers ──────────────────────────────────────────────────────────
/**
* Creates a temporary directory with a minimal openapi.yaml fixture,
* changes CWD to it, and returns a cleanup function.
*/
function withFixtureOpenapi(yamlContent: string): { cleanup: () => void } {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "omni-openapi-test-"));
// openapiParser reads docs/openapi.yaml (consolidated location since #4781,
// previously docs/reference/openapi.yaml) — the fixture must mirror that path.
const docsDir = path.join(tmpDir, "docs");
fs.mkdirSync(docsDir, { recursive: true });
fs.writeFileSync(path.join(docsDir, "openapi.yaml"), yamlContent, "utf-8");
const originalCwd = process.cwd();
process.chdir(tmpDir);
return {
cleanup() {
process.chdir(originalCwd);
fs.rmSync(tmpDir, { recursive: true, force: true });
},
};
}
// ─── Fixture YAML ─────────────────────────────────────────────────────────────
const FIXTURE_YAML = `
openapi: 3.1.0
info:
title: OmniRoute Test
version: 1.0.0
paths:
/api/providers:
get:
tags: [Providers]
summary: List provider connections
description: Returns all configured provider connections.
post:
tags: [Providers]
summary: Add provider connection
/api/providers/{id}:
get:
tags: [Providers]
summary: Get provider by id
patch:
tags: [Providers]
summary: Update provider connection
delete:
tags: [Providers]
summary: Remove provider connection
/api/providers/{id}/test:
post:
tags: [Providers]
summary: Test provider connection
/api/keys:
get:
tags: [APIKeys]
summary: List API keys
post:
tags: [APIKeys]
summary: Create API key
/api/keys/{id}:
delete:
tags: [APIKeys]
summary: Revoke API key
/api/usage/analytics:
get:
tags: [Usage]
summary: Get usage analytics
/api/v1/chat/completions:
post:
tags: [Chat]
summary: Create chat completion
/api/settings:
get:
tags: [Settings]
summary: Get settings
put:
tags: [Settings]
summary: Update settings
`;
// ─── Tests using fixture ──────────────────────────────────────────────────────
test("parseOpenapi() returns paths Map with all operations from fixture", () => {
const { cleanup } = withFixtureOpenapi(FIXTURE_YAML);
try {
const { paths } = parseOpenapi();
assert.ok(paths instanceof Map, "paths should be a Map");
assert.ok(paths.size > 0, "paths should not be empty");
// Spot-check a few keys
assert.ok(paths.has("GET /api/providers"), "Expected GET /api/providers");
assert.ok(paths.has("POST /api/providers"), "Expected POST /api/providers");
assert.ok(paths.has("GET /api/providers/{id}"), "Expected GET /api/providers/{id}");
assert.ok(paths.has("DELETE /api/providers/{id}"), "Expected DELETE /api/providers/{id}");
assert.ok(paths.has("POST /api/v1/chat/completions"), "Expected POST /api/v1/chat/completions");
} finally {
cleanup();
}
});
test("parseOpenapi() groups /api/providers/* under 'providers' area", () => {
const { cleanup } = withFixtureOpenapi(FIXTURE_YAML);
try {
const { areas } = parseOpenapi();
const providerOps = areas.get("providers");
assert.ok(providerOps, "Expected 'providers' area to exist");
assert.ok(
providerOps!.length >= 5,
`Expected at least 5 provider endpoints, got ${providerOps!.length}`
);
const paths = providerOps!.map((op) => op.path);
assert.ok(paths.includes("/api/providers"), "Expected /api/providers");
assert.ok(paths.includes("/api/providers/{id}"), "Expected /api/providers/{id}");
assert.ok(paths.includes("/api/providers/{id}/test"), "Expected /api/providers/{id}/test");
} finally {
cleanup();
}
});
test("parseOpenapi() groups /api/keys/* under 'api-keys' area", () => {
const { cleanup } = withFixtureOpenapi(FIXTURE_YAML);
try {
const { areas } = parseOpenapi();
const keyOps = areas.get("api-keys");
assert.ok(keyOps, "Expected 'api-keys' area to exist");
assert.ok(keyOps!.length >= 2, `Expected at least 2 key endpoints, got ${keyOps!.length}`);
} finally {
cleanup();
}
});
test("parseOpenapi() groups /api/v1/* under 'inference' area", () => {
const { cleanup } = withFixtureOpenapi(FIXTURE_YAML);
try {
const { areas } = parseOpenapi();
const inferenceOps = areas.get("inference");
assert.ok(inferenceOps, "Expected 'inference' area to exist");
assert.ok(
inferenceOps!.length >= 1,
`Expected at least 1 inference endpoint, got ${inferenceOps!.length}`
);
assert.ok(
inferenceOps!.some((op) => op.path === "/api/v1/chat/completions"),
"Expected /api/v1/chat/completions in inference area"
);
} finally {
cleanup();
}
});
test("parseOpenapi() OpenapiPath entries have required fields", () => {
const { cleanup } = withFixtureOpenapi(FIXTURE_YAML);
try {
const { paths } = parseOpenapi();
for (const [key, op] of paths) {
assert.ok(typeof op.method === "string" && op.method.length > 0, `${key}: method missing`);
assert.ok(typeof op.path === "string" && op.path.length > 0, `${key}: path missing`);
assert.ok(typeof op.summary === "string", `${key}: summary not a string`);
assert.ok(Array.isArray(op.tags), `${key}: tags not an array`);
}
} finally {
cleanup();
}
});
test("parseOpenapi() throws if openapi.yaml is missing", () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "omni-openapi-missing-"));
const originalCwd = process.cwd();
process.chdir(tmpDir);
try {
assert.throws(
() => parseOpenapi(),
/openapiParser: could not read/,
"Expected error when openapi.yaml is missing"
);
} finally {
process.chdir(originalCwd);
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
test("parseOpenapi() returns empty areas Map for YAML with no paths", () => {
const emptyPathsYaml = `
openapi: 3.1.0
info:
title: Empty
version: 1.0.0
paths: {}
`;
const { cleanup } = withFixtureOpenapi(emptyPathsYaml);
try {
const { paths, areas } = parseOpenapi();
assert.equal(paths.size, 0);
assert.equal(areas.size, 0);
} finally {
cleanup();
}
});
// ─── Integration test: real openapi.yaml (gated) ─────────────────────────────
const SKIP_REAL = process.env.SKIP_REAL_OPENAPI === "1";
test(
"parseOpenapi() with real openapi.yaml: providers area has ≥5 endpoints",
{ skip: SKIP_REAL ? "SKIP_REAL_OPENAPI=1" : false },
() => {
// This test runs from the project root (the worktree).
// It will fail if openapi.yaml doesn't exist — that's intentional.
const { areas } = parseOpenapi();
const providerOps = areas.get("providers");
assert.ok(providerOps, "Expected 'providers' area in real OpenAPI spec");
assert.ok(
providerOps!.length >= 5,
`Expected ≥5 provider endpoints in real spec, got ${providerOps!.length}`
);
}
);
test(
"getEndpointsForArea('providers') with real openapi.yaml: returns ≥5 strings",
{ skip: SKIP_REAL ? "SKIP_REAL_OPENAPI=1" : false },
() => {
const endpoints = getEndpointsForArea("providers");
assert.ok(
endpoints.length >= 5,
`Expected ≥5 provider endpoint strings, got ${endpoints.length}: ${endpoints.join(", ")}`
);
// Each entry should match "METHOD /path"
for (const ep of endpoints) {
assert.match(ep, /^[A-Z]+ \//, `Endpoint "${ep}" does not match METHOD /path format`);
}
}
);
+432
View File
@@ -0,0 +1,432 @@
/**
* Unit tests for /api/agent-skills/* REST routes.
*
* Uses Node.js native test runner + real catalog (pure, stateless).
* Auth tested via requireManagementAuth with live DB in temp directory.
*
* Coverage goals:
* - GET /api/agent-skills — happy path (43 skills), filters, invalid category
* - GET /api/agent-skills/[id] — found, 404 not found
* - GET /api/agent-skills/[id]/raw — found, 404 not found, 502 on GitHub failure
* - GET /api/agent-skills/coverage — happy path
* - POST /api/agent-skills/generate — 401 no auth, 400 bad body, 503 no generator, 200 with mock
*
* Hard Rule #12: every error response goes through buildErrorBody/errorResponse — verified
* explicitly by asserting no stack trace in messages.
*/
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
// ── DB / auth setup ───────────────────────────────────────────────────────────
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-agentskills-routes-"));
const ORIGINAL_DATA_DIR = process.env.DATA_DIR;
const ORIGINAL_API_KEY_SECRET = process.env.API_KEY_SECRET;
const ORIGINAL_INITIAL_PASSWORD = process.env.INITIAL_PASSWORD;
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.API_KEY_SECRET = process.env.API_KEY_SECRET ?? "agentskills-routes-test-secret";
// Import DB first (order matters — sets DATA_DIR before localDb loads)
const core = await import("../../src/lib/db/core.ts");
const apiKeysDb = await import("../../src/lib/db/apiKeys.ts");
// Import routes AFTER env vars are set
const listRoute = await import("../../src/app/api/agent-skills/route.ts");
const idRoute = await import("../../src/app/api/agent-skills/[id]/route.ts");
const rawRoute = await import("../../src/app/api/agent-skills/[id]/raw/route.ts");
const coverageRoute = await import("../../src/app/api/agent-skills/coverage/route.ts");
const generateRoute = await import("../../src/app/api/agent-skills/generate/route.ts");
// ── Helpers ───────────────────────────────────────────────────────────────────
async function resetStorage() {
core.resetDbInstance();
apiKeysDb.resetApiKeyState();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
delete process.env.INITIAL_PASSWORD;
}
function makeRequest(
method: string,
url: string,
body?: unknown,
headers: Record<string, string> = {},
): Request {
return new Request(url, {
method,
headers: {
...(body !== undefined ? { "content-type": "application/json" } : {}),
...headers,
},
body: body !== undefined ? JSON.stringify(body) : undefined,
});
}
// ── Lifecycle ─────────────────────────────────────────────────────────────────
test.beforeEach(async () => {
await resetStorage();
});
test.after(() => {
core.resetDbInstance();
apiKeysDb.resetApiKeyState();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
if (ORIGINAL_DATA_DIR === undefined) {
delete process.env.DATA_DIR;
} else {
process.env.DATA_DIR = ORIGINAL_DATA_DIR;
}
if (ORIGINAL_API_KEY_SECRET === undefined) {
delete process.env.API_KEY_SECRET;
} else {
process.env.API_KEY_SECRET = ORIGINAL_API_KEY_SECRET;
}
if (ORIGINAL_INITIAL_PASSWORD === undefined) {
delete process.env.INITIAL_PASSWORD;
} else {
process.env.INITIAL_PASSWORD = ORIGINAL_INITIAL_PASSWORD;
}
});
// ═════════════════════════════════════════════════════════════════════════════
// GET /api/agent-skills
// ═════════════════════════════════════════════════════════════════════════════
test("GET /api/agent-skills — returns 44 skills with count and coverage", async () => {
const req = makeRequest("GET", "http://localhost/api/agent-skills");
const res = await listRoute.GET(req);
assert.equal(res.status, 200);
const body = (await res.json()) as { skills: unknown[]; count: number; coverage: unknown };
assert.equal(body.count, 44, `Expected 44 skills but got ${body.count}`);
assert.equal(Array.isArray(body.skills), true);
assert.equal(body.skills.length, 44);
assert.ok(body.coverage !== undefined, "coverage should be present");
});
test("GET /api/agent-skills?category=api — returns 23 api skills", async () => {
const req = makeRequest("GET", "http://localhost/api/agent-skills?category=api");
const res = await listRoute.GET(req);
assert.equal(res.status, 200);
const body = (await res.json()) as { skills: Array<{ category: string }>; count: number };
assert.equal(body.count, 23);
assert.ok(body.skills.every((s) => s.category === "api"), "All skills should be api category");
});
test("GET /api/agent-skills?category=cli — returns 20 cli skills", async () => {
const req = makeRequest("GET", "http://localhost/api/agent-skills?category=cli");
const res = await listRoute.GET(req);
assert.equal(res.status, 200);
const body = (await res.json()) as { skills: Array<{ category: string }>; count: number };
assert.equal(body.count, 20);
assert.ok(body.skills.every((s) => s.category === "cli"), "All skills should be cli category");
});
test("GET /api/agent-skills?area=providers — returns only providers area skills", async () => {
const req = makeRequest("GET", "http://localhost/api/agent-skills?area=providers");
const res = await listRoute.GET(req);
assert.equal(res.status, 200);
const body = (await res.json()) as { skills: Array<{ area: string }>; count: number };
assert.ok(body.count >= 1, "Should find at least one providers skill");
assert.ok(body.skills.every((s) => s.area === "providers"));
});
test("GET /api/agent-skills?category=invalid — returns 400 with sanitized error", async () => {
const req = makeRequest("GET", "http://localhost/api/agent-skills?category=invalid");
const res = await listRoute.GET(req);
assert.equal(res.status, 400);
const body = (await res.json()) as { error: { message: string } };
assert.ok(body.error, "error field should be present");
assert.ok(typeof body.error.message === "string", "error.message should be a string");
// Hard Rule #12: no stack trace exposure
assert.ok(
!body.error.message.match(/\bat \/|\bat file:\/\//),
`Error message must not contain stack trace: "${body.error.message}"`,
);
});
// ═════════════════════════════════════════════════════════════════════════════
// GET /api/agent-skills/[id]
// ═════════════════════════════════════════════════════════════════════════════
test("GET /api/agent-skills/[id] — returns skill for valid id", async () => {
const req = makeRequest("GET", "http://localhost/api/agent-skills/omni-providers");
const res = await idRoute.GET(req, { params: Promise.resolve({ id: "omni-providers" }) });
assert.equal(res.status, 200);
const body = (await res.json()) as { id: string; category: string };
assert.equal(body.id, "omni-providers");
assert.equal(body.category, "api");
});
test("GET /api/agent-skills/[id] — returns skill for cli skill id", async () => {
const req = makeRequest("GET", "http://localhost/api/agent-skills/cli-serve");
const res = await idRoute.GET(req, { params: Promise.resolve({ id: "cli-serve" }) });
assert.equal(res.status, 200);
const body = (await res.json()) as { id: string; category: string };
assert.equal(body.id, "cli-serve");
assert.equal(body.category, "cli");
});
test("GET /api/agent-skills/[id] — returns 404 with sanitized error for unknown id", async () => {
const req = makeRequest("GET", "http://localhost/api/agent-skills/does-not-exist");
const res = await idRoute.GET(req, { params: Promise.resolve({ id: "does-not-exist" }) });
assert.equal(res.status, 404);
const body = (await res.json()) as { error: { message: string } };
assert.ok(body.error);
// Hard Rule #12: no stack trace exposure
assert.ok(
!body.error.message.match(/\bat \/|\bat file:\/\//),
`Error message must not contain stack trace: "${body.error.message}"`,
);
});
// ═════════════════════════════════════════════════════════════════════════════
// GET /api/agent-skills/[id]/raw
// ═════════════════════════════════════════════════════════════════════════════
test("GET /api/agent-skills/[id]/raw — returns 404 with sanitized error for unknown id", async () => {
const req = makeRequest("GET", "http://localhost/api/agent-skills/does-not-exist/raw");
const res = await rawRoute.GET(req, { params: Promise.resolve({ id: "does-not-exist" }) });
assert.equal(res.status, 404);
const contentType = res.headers.get("content-type") ?? "";
assert.ok(contentType.includes("application/json"), "404 should be JSON, not markdown");
const body = (await res.json()) as { error: { message: string } };
assert.ok(body.error);
// Hard Rule #12: no stack trace exposure
assert.ok(
!body.error.message.match(/\bat \/|\bat file:\/\//),
`Error message must not contain stack trace: "${body.error.message}"`,
);
});
test("GET /api/agent-skills/[id]/raw — returns markdown or 502 for valid id (no local file)", async () => {
// With no local skills/ dir, the route will attempt GitHub fetch.
// In test environment with no network or GitHub response, we expect either:
// - 200 with text/markdown if GitHub fetch succeeds (unlikely in CI)
// - 502 if GitHub fetch fails
// We test the 502 branch explicitly here by using a skill where the rawUrl won't work.
const req = makeRequest("GET", "http://localhost/api/agent-skills/omni-providers/raw");
const res = await rawRoute.GET(req, { params: Promise.resolve({ id: "omni-providers" }) });
// Either 200 (network available) or 502 (no network) is acceptable
assert.ok(
res.status === 200 || res.status === 502 || res.status === 500,
`Expected 200, 502, or 500 but got ${res.status}`,
);
if (res.status === 200) {
const contentType = res.headers.get("content-type") ?? "";
assert.ok(
contentType.includes("text/markdown"),
`Expected text/markdown content-type, got: ${contentType}`,
);
const cacheControl = res.headers.get("cache-control") ?? "";
assert.ok(cacheControl.includes("max-age=3600"), "Cache-Control should include max-age=3600");
} else {
// 502 or 500 — error body must not leak stack trace
const body = (await res.json()) as { error: { message: string } };
assert.ok(body.error);
assert.ok(
!body.error.message.match(/\bat \/|\bat file:\/\//),
`Error message must not contain stack trace: "${body.error.message}"`,
);
}
});
// ═════════════════════════════════════════════════════════════════════════════
// GET /api/agent-skills/coverage
// ═════════════════════════════════════════════════════════════════════════════
test("GET /api/agent-skills/coverage — returns valid SkillCoverage shape", async () => {
const res = await coverageRoute.GET();
assert.equal(res.status, 200);
const body = (await res.json()) as {
api: { have: number; total: number };
cli: { have: number; total: number };
totalSkills: number;
generatedAt: string;
};
assert.equal(body.api.total, 23, "api.total must be 23");
assert.equal(body.cli.total, 20, "cli.total must be 20");
assert.ok(typeof body.totalSkills === "number", "totalSkills must be a number");
assert.ok(typeof body.generatedAt === "string", "generatedAt must be a string");
// generatedAt must be a valid ISO datetime
assert.ok(!isNaN(Date.parse(body.generatedAt)), "generatedAt must be a valid ISO datetime");
});
// ═════════════════════════════════════════════════════════════════════════════
// POST /api/agent-skills/generate — auth guard
// ═════════════════════════════════════════════════════════════════════════════
test("POST /api/agent-skills/generate — 401 when auth is required and no token provided", async () => {
// Enable auth by setting INITIAL_PASSWORD (triggers requireManagementAuth)
process.env.INITIAL_PASSWORD = "test-password-requires-login";
const req = makeRequest("POST", "http://localhost/api/agent-skills/generate", {
dryRun: true,
});
const res = await generateRoute.POST(req);
// requireManagementAuth returns 401 or 403 when auth is required and no token
assert.ok(
res.status === 401 || res.status === 403,
`Expected 401 or 403 without auth, got ${res.status}`,
);
const body = (await res.json()) as { error: { message: string } | string };
// Hard Rule #12: no stack trace exposure
const errorMsg =
typeof body.error === "string" ? body.error : (body.error as { message: string }).message;
assert.ok(
!errorMsg.match(/\bat \/|\bat file:\/\//),
`Error message must not contain stack trace: "${errorMsg}"`,
);
});
test("POST /api/agent-skills/generate — 400 when body is invalid (non-boolean dryRun)", async () => {
// Disable auth for this test by not setting INITIAL_PASSWORD
delete process.env.INITIAL_PASSWORD;
const req = makeRequest("POST", "http://localhost/api/agent-skills/generate", {
dryRun: "yes", // invalid — must be boolean
});
const res = await generateRoute.POST(req);
// Should be 400 (body validation fails) or 503 (generator not yet available)
// Since F3 is not merged, generator.ts doesn't exist → dynamic import fails → 503 if auth passes
// But with invalid body, 400 should come first
assert.ok(
res.status === 400 || res.status === 503,
`Expected 400 (bad body) or 503 (generator unavailable), got ${res.status}`,
);
const body = (await res.json()) as { error: { message: string } };
assert.ok(body.error);
// Hard Rule #12: no stack trace exposure
assert.ok(
!body.error.message.match(/\bat \/|\bat file:\/\//),
`Error message must not contain stack trace: "${body.error.message}"`,
);
});
test("POST /api/agent-skills/generate — 503 when generator module unavailable (F3 not merged)", async () => {
// Disable auth for this test
delete process.env.INITIAL_PASSWORD;
const req = makeRequest("POST", "http://localhost/api/agent-skills/generate", {
dryRun: true,
prune: false,
});
const res = await generateRoute.POST(req);
// If generator.ts doesn't exist (F3 not merged), expect 503.
// If it does exist (F3 already merged), it may return 200 with a report.
// Both are valid depending on merge state.
assert.ok(
res.status === 200 || res.status === 503,
`Expected 200 (generator available) or 503 (generator unavailable), got ${res.status}`,
);
const body = (await res.json()) as Record<string, unknown>;
if (res.status === 503) {
const err = body.error as { message: string } | undefined;
assert.ok(err?.message, "503 should include error.message");
// Hard Rule #12: no stack trace exposure
assert.ok(
!err.message.match(/\bat \/|\bat file:\/\//),
`503 error message must not contain stack trace: "${err.message}"`,
);
} else {
// 200: body should look like a GeneratorReport
assert.ok("generated" in body || "report" in body || typeof body === "object");
}
});
test("POST /api/agent-skills/generate — 400 when request body is not JSON", async () => {
// Disable auth for this test
delete process.env.INITIAL_PASSWORD;
const req = new Request("http://localhost/api/agent-skills/generate", {
method: "POST",
headers: { "content-type": "application/json" },
body: "not valid json {{{",
});
const res = await generateRoute.POST(req);
assert.equal(res.status, 400, `Expected 400 for invalid JSON body, got ${res.status}`);
const body = (await res.json()) as { error: { message: string } };
assert.ok(body.error);
// Hard Rule #12: no stack trace exposure
assert.ok(
!body.error.message.match(/\bat \/|\bat file:\/\//),
`Error message must not contain stack trace: "${body.error.message}"`,
);
});
// ═════════════════════════════════════════════════════════════════════════════
// Hard Rule #12 sanity — all error bodies sanitized
// ═════════════════════════════════════════════════════════════════════════════
test("Hard Rule #12: all error responses contain sanitized messages (no 'at /' patterns)", async () => {
delete process.env.INITIAL_PASSWORD;
// Collect error responses from various bad inputs
const errorResponses: Response[] = [
// Invalid category query
await listRoute.GET(
makeRequest("GET", "http://localhost/api/agent-skills?category=bad-val"),
),
// Unknown skill id
await idRoute.GET(makeRequest("GET", "http://localhost/api/agent-skills/unknown-id"), {
params: Promise.resolve({ id: "unknown-id" }),
}),
// Unknown raw skill id
await rawRoute.GET(
makeRequest("GET", "http://localhost/api/agent-skills/unknown-id/raw"),
{ params: Promise.resolve({ id: "unknown-id" }) },
),
// Invalid generate body (non-boolean)
await generateRoute.POST(
makeRequest("POST", "http://localhost/api/agent-skills/generate", {
dryRun: 42,
}),
),
];
for (const res of errorResponses) {
assert.ok(res.status >= 400, `Expected an error status, got ${res.status}`);
const contentType = res.headers.get("content-type") ?? "";
assert.ok(
contentType.includes("application/json") || contentType.includes("json"),
`Error response must be JSON, got content-type: ${contentType}`,
);
const body = (await res.json()) as { error?: { message?: string } };
const message = body?.error?.message ?? "";
assert.ok(
!message.match(/\bat \/|\bat file:\/\//),
`Stack trace detected in error response (status ${res.status}): "${message}"`,
);
}
});
+255
View File
@@ -0,0 +1,255 @@
import test from "node:test";
import assert from "node:assert/strict";
const { AgentSkillSchema, SkillCoverageSchema, ListQuerySchema, GenerateBodySchema } =
await import("../../src/lib/agentSkills/schemas.ts");
test("agent skills schemas module exposes runtime validators", async () => {
const schemas = await import("../../src/lib/agentSkills/schemas.ts");
for (const key of [
"AgentSkillSchema",
"GenerateBodySchema",
"ListQuerySchema",
"SkillCategorySchema",
"SkillCoverageSchema",
]) {
assert.equal(typeof schemas[key].safeParse, "function", `${key} should stay exported`);
}
});
// ─── AgentSkillSchema ─────────────────────────────────────────────────────────
test("AgentSkillSchema — valid api skill parses successfully", () => {
const input = {
id: "omni-providers",
name: "Providers",
description: "Manage LLM providers",
category: "api" as const,
area: "providers",
endpoints: ["GET /api/providers", "POST /api/providers"],
rawUrl: "https://raw.githubusercontent.com/owner/repo/main/skills/omni-providers/SKILL.md",
githubUrl: "https://github.com/owner/repo/blob/main/skills/omni-providers/SKILL.md",
};
const result = AgentSkillSchema.safeParse(input);
assert.equal(result.success, true);
if (result.success) {
assert.equal(result.data.id, "omni-providers");
assert.equal(result.data.category, "api");
}
});
test("AgentSkillSchema — valid cli skill parses successfully", () => {
const input = {
id: "cli-serve",
name: "Serve",
description: "Start the OmniRoute server",
category: "cli" as const,
area: "cli-serve",
cliCommands: ["serve", "serve --port 8080"],
rawUrl: "https://raw.githubusercontent.com/owner/repo/main/skills/cli-serve/SKILL.md",
githubUrl: "https://github.com/owner/repo/blob/main/skills/cli-serve/SKILL.md",
};
const result = AgentSkillSchema.safeParse(input);
assert.equal(result.success, true);
});
test("AgentSkillSchema — invalid id (uppercase) fails", () => {
const input = {
id: "Omni-Providers",
name: "Providers",
description: "Manage LLM providers",
category: "api",
area: "providers",
rawUrl: "https://raw.githubusercontent.com/owner/repo/main/skills/omni-providers/SKILL.md",
githubUrl: "https://github.com/owner/repo/blob/main/skills/omni-providers/SKILL.md",
};
const result = AgentSkillSchema.safeParse(input);
assert.equal(result.success, false);
});
test("AgentSkillSchema — invalid category fails", () => {
const input = {
id: "omni-providers",
name: "Providers",
description: "Manage LLM providers",
category: "unknown",
area: "providers",
rawUrl: "https://raw.githubusercontent.com/owner/repo/main/skills/omni-providers/SKILL.md",
githubUrl: "https://github.com/owner/repo/blob/main/skills/omni-providers/SKILL.md",
};
const result = AgentSkillSchema.safeParse(input);
assert.equal(result.success, false);
});
test("AgentSkillSchema — non-url rawUrl fails", () => {
const input = {
id: "omni-providers",
name: "Providers",
description: "Manage LLM providers",
category: "api",
area: "providers",
rawUrl: "not-a-url",
githubUrl: "https://github.com/owner/repo/blob/main/skills/omni-providers/SKILL.md",
};
const result = AgentSkillSchema.safeParse(input);
assert.equal(result.success, false);
});
test("AgentSkillSchema — optional fields absent parses successfully", () => {
const input = {
id: "omni-providers",
name: "Providers",
description: "Manage LLM providers",
category: "api",
area: "providers",
rawUrl: "https://raw.githubusercontent.com/owner/repo/main/skills/omni-providers/SKILL.md",
githubUrl: "https://github.com/owner/repo/blob/main/skills/omni-providers/SKILL.md",
};
const result = AgentSkillSchema.safeParse(input);
assert.equal(result.success, true);
if (result.success) {
assert.equal(result.data.endpoints, undefined);
assert.equal(result.data.cliCommands, undefined);
assert.equal(result.data.icon, undefined);
assert.equal(result.data.isEntry, undefined);
assert.equal(result.data.isNew, undefined);
}
});
test("AgentSkillSchema — .parse throws on invalid input", () => {
assert.throws(() => {
AgentSkillSchema.parse({
id: "bad id",
name: "",
description: "",
category: "api",
area: "x",
rawUrl: "x",
githubUrl: "x",
});
});
});
// ─── SkillCoverageSchema ──────────────────────────────────────────────────────
test("SkillCoverageSchema — valid coverage parses successfully", () => {
const input = {
api: { have: 23, total: 23 },
cli: { have: 20, total: 20 },
totalSkills: 42,
generatedAt: new Date().toISOString(),
};
const result = SkillCoverageSchema.safeParse(input);
assert.equal(result.success, true);
});
test("SkillCoverageSchema — wrong total literal (api.total=21) fails", () => {
const input = {
api: { have: 21, total: 21 },
cli: { have: 20, total: 20 },
totalSkills: 41,
generatedAt: new Date().toISOString(),
};
const result = SkillCoverageSchema.safeParse(input);
assert.equal(result.success, false);
});
test("SkillCoverageSchema — wrong total literal (cli.total=19) fails", () => {
const input = {
api: { have: 23, total: 23 },
cli: { have: 19, total: 19 },
totalSkills: 41,
generatedAt: new Date().toISOString(),
};
const result = SkillCoverageSchema.safeParse(input);
assert.equal(result.success, false);
});
test("SkillCoverageSchema — invalid datetime fails", () => {
const input = {
api: { have: 23, total: 23 },
cli: { have: 20, total: 20 },
totalSkills: 42,
generatedAt: "not-a-date",
};
const result = SkillCoverageSchema.safeParse(input);
assert.equal(result.success, false);
});
test("SkillCoverageSchema — negative have value fails", () => {
const input = {
api: { have: -1, total: 23 },
cli: { have: 20, total: 20 },
totalSkills: 42,
generatedAt: new Date().toISOString(),
};
const result = SkillCoverageSchema.safeParse(input);
assert.equal(result.success, false);
});
// ─── ListQuerySchema ──────────────────────────────────────────────────────────
test("ListQuerySchema — empty object parses successfully", () => {
const result = ListQuerySchema.safeParse({});
assert.equal(result.success, true);
if (result.success) {
assert.equal(result.data.category, undefined);
assert.equal(result.data.area, undefined);
}
});
test("ListQuerySchema — valid category parses successfully", () => {
const result = ListQuerySchema.safeParse({ category: "api" });
assert.equal(result.success, true);
if (result.success) {
assert.equal(result.data.category, "api");
}
});
test("ListQuerySchema — invalid category fails", () => {
const result = ListQuerySchema.safeParse({ category: "invalid" });
assert.equal(result.success, false);
});
test("ListQuerySchema — area filter parses successfully", () => {
const result = ListQuerySchema.safeParse({ category: "cli", area: "cli-serve" });
assert.equal(result.success, true);
if (result.success) {
assert.equal(result.data.area, "cli-serve");
}
});
// ─── GenerateBodySchema ───────────────────────────────────────────────────────
test("GenerateBodySchema — empty object applies defaults", () => {
const result = GenerateBodySchema.safeParse({});
assert.equal(result.success, true);
if (result.success) {
assert.equal(result.data.dryRun, true);
assert.equal(result.data.prune, false);
assert.equal(result.data.onlyIds, undefined);
}
});
test("GenerateBodySchema — explicit dryRun=false parses", () => {
const result = GenerateBodySchema.safeParse({ dryRun: false, prune: true });
assert.equal(result.success, true);
if (result.success) {
assert.equal(result.data.dryRun, false);
assert.equal(result.data.prune, true);
}
});
test("GenerateBodySchema — onlyIds array parses", () => {
const result = GenerateBodySchema.safeParse({ onlyIds: ["omni-providers", "cli-serve"] });
assert.equal(result.success, true);
if (result.success) {
assert.deepEqual(result.data.onlyIds, ["omni-providers", "cli-serve"]);
}
});
test("GenerateBodySchema — non-boolean dryRun fails", () => {
const result = GenerateBodySchema.safeParse({ dryRun: "yes" });
assert.equal(result.success, false);
});
@@ -0,0 +1,91 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
isClaudeCodeCompatible,
buildProviderUrl,
buildProviderHeaders,
} from "../../open-sse/services/provider.ts";
import { isClaudeCodeCompatibleProvider } from "../../open-sse/services/claudeCodeCompatible.ts";
import {
CC_WIRE_IMAGE_BUILTINS,
usesCcWireImage,
} from "../../open-sse/services/ccWireImageBuiltins.ts";
import { CLAUDE_CODE_COMPATIBLE_USER_AGENT } from "../../open-sse/services/claudeCodeCompatible.ts";
import { CLAUDE_CLI_USER_AGENT } from "../../open-sse/config/anthropicHeaders.ts";
import { applyFingerprint } from "../../open-sse/config/cliFingerprints.ts";
// Regression guard for #6056 — the built-in `agentrouter` provider must route
// through the DYNAMIC Claude-Code wire image (fingerprint headers + `?beta=true`
// chat path) while KEEPING its own registry baseUrl + x-api-key auth.
test("agentrouter is registered in the CC-wire-image built-in allow-set", () => {
assert.ok(CC_WIRE_IMAGE_BUILTINS.has("agentrouter"));
assert.equal(usesCcWireImage("agentrouter"), true);
assert.equal(usesCcWireImage("claude"), false);
assert.equal(usesCcWireImage(null), false);
});
test("(a) both CC predicates return true for agentrouter", () => {
assert.equal(isClaudeCodeCompatible("agentrouter"), true);
assert.equal(isClaudeCodeCompatibleProvider("agentrouter"), true);
});
test("(a) predicates are unaffected for non-allow-set providers", () => {
// Official Claude OAuth provider must NOT be treated as CC-compatible.
assert.equal(isClaudeCodeCompatible("claude"), false);
assert.equal(isClaudeCodeCompatibleProvider("claude"), false);
// Genuine CC-family providers still match via the prefix.
assert.equal(isClaudeCodeCompatible("anthropic-compatible-cc-foo"), true);
assert.equal(isClaudeCodeCompatibleProvider("anthropic-compatible-cc-foo"), true);
});
test("(b) agentrouter outbound headers carry the dynamic CC wire image", () => {
const headers = buildProviderHeaders("agentrouter", { apiKey: "sk-agentrouter" }, true);
// CC wire image markers (not the static getClaudeCliHeaders() shape).
assert.equal(headers["User-Agent"], CLAUDE_CODE_COMPATIBLE_USER_AGENT);
assert.notEqual(headers["User-Agent"], CLAUDE_CLI_USER_AGENT);
assert.equal(headers["x-app"], "cli");
assert.equal(headers["anthropic-dangerous-direct-browser-access"], "true");
assert.ok(headers["anthropic-beta"], "expected the CC anthropic-beta header");
assert.ok(headers["X-Stainless-Package-Version"], "expected CC X-Stainless anchors");
});
test("(b) applyFingerprint selects the claude-code-compatible fingerprint for agentrouter", () => {
const { headers } = applyFingerprint(
"agentrouter",
buildProviderHeaders("agentrouter", { apiKey: "sk-agentrouter" }, true),
{ model: "claude-opus-4-6", messages: [] }
);
// Fingerprint reordering keeps the CC wire image + the preserved x-api-key auth.
assert.equal(headers["x-api-key"], "sk-agentrouter");
assert.equal(headers["User-Agent"], CLAUDE_CODE_COMPATIBLE_USER_AGENT);
});
test("(c) CRUX: agentrouter keeps its OWN x-api-key auth (NOT CC Bearer)", () => {
const headers = buildProviderHeaders("agentrouter", { apiKey: "sk-agentrouter" }, true);
assert.equal(headers["x-api-key"], "sk-agentrouter");
assert.equal(headers["Authorization"], undefined);
});
test("(c) CRUX: agentrouter keeps its OWN registry baseUrl + ?beta=true", () => {
const url = buildProviderUrl("agentrouter", "claude-opus-4-6", true);
assert.equal(url, "https://agentrouter.org/v1/messages?beta=true");
// NOT the CC-family anthropic default baseUrl.
assert.ok(!url.includes("api.anthropic.com"));
});
test("(c) real CC-family provider still uses the CC default baseUrl + Bearer auth", () => {
// The wire-image guard must NOT leak into genuine anthropic-compatible-cc-* providers.
const headers = buildProviderHeaders(
"anthropic-compatible-cc-foo",
{ apiKey: "sk-foo" },
true
);
assert.equal(headers["Authorization"], "Bearer sk-foo");
assert.equal(headers["x-api-key"], undefined);
const url = buildProviderUrl("anthropic-compatible-cc-foo", "claude-sonnet-4-6", true);
assert.ok(url.includes("api.anthropic.com"));
});
+72
View File
@@ -0,0 +1,72 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
parseAndValidateAgyToken,
AgyAuthFileError,
} from "../../src/lib/oauth/utils/agyAuthImport.ts";
// Fixture token values are deliberately generic (not `ya29.`/`1//` shaped) so secret
// scanners don't flag them — the parser only cares that they are non-empty strings.
const ACCESS = "agy-access-token-fixture";
const REFRESH = "agy-refresh-token-fixture";
test("parses the nested agy token-file shape (token.* with ISO expiry, no id_token)", () => {
const parsed = parseAndValidateAgyToken({
token: {
access_token: ACCESS,
token_type: "Bearer",
refresh_token: REFRESH,
expiry: "2026-05-29T06:16:24.338-03:00",
},
auth_method: "consumer",
});
assert.equal(parsed.accessToken, ACCESS);
assert.equal(parsed.refreshToken, REFRESH);
assert.equal(parsed.tokenType, "Bearer");
assert.equal(parsed.authMethod, "consumer");
assert.equal(parsed.expiresAt, new Date("2026-05-29T06:16:24.338-03:00").toISOString());
});
test("accepts a flat fallback shape (access_token/refresh_token at top level)", () => {
const parsed = parseAndValidateAgyToken({
access_token: ACCESS,
refresh_token: REFRESH,
});
assert.equal(parsed.accessToken, ACCESS);
assert.equal(parsed.refreshToken, REFRESH);
assert.equal(parsed.tokenType, "Bearer"); // default
assert.equal(parsed.expiresAt, null);
});
test("supports unix-ms expiry_date as an alternative to ISO expiry", () => {
const ms = 1780038984000;
const parsed = parseAndValidateAgyToken({
token: { access_token: ACCESS, refresh_token: REFRESH, expiry_date: ms },
});
assert.equal(parsed.expiresAt, new Date(ms).toISOString());
});
test("rejects a token file missing access_token", () => {
assert.throws(
() => parseAndValidateAgyToken({ token: { refresh_token: REFRESH } }),
(err) =>
err instanceof AgyAuthFileError &&
err.code === "missing_access_token" &&
err.status === 400
);
});
test("rejects a token file missing refresh_token", () => {
assert.throws(
() => parseAndValidateAgyToken({ token: { access_token: ACCESS } }),
(err) => err instanceof AgyAuthFileError && err.code === "missing_refresh_token"
);
});
test("invalid/garbage expiry becomes null rather than throwing", () => {
const parsed = parseAndValidateAgyToken({
token: { access_token: ACCESS, refresh_token: REFRESH, expiry: "not-a-date" },
});
assert.equal(parsed.expiresAt, null);
});
@@ -0,0 +1,41 @@
/**
* #3696 — antigravity/agy gemini-3.1-pro-high / gemini-3.1-pro-low were being collapsed
* to the bare upstream id `gemini-3.1-pro`, losing the tier distinction.
*
* Wire evidence (captured by maintainer via `agy --model gemini-3.1-pro-high --log-file`):
* - `gemini-3.1-pro-high` sent literally to `/v1internal:streamGenerateContent` → 200 OK
* - `gemini-3.1-pro-low` sent literally → 200 OK
*
* CONCLUSION: the upstream ACCEPTS the suffixed ids directly. The old assumption in #3229
* ("upstream rejects the suffix for gemini-3.x") was refuted by this wire capture.
* The collapse aliases must be removed so the tier-specific ids reach the upstream.
*/
import test from "node:test";
import assert from "node:assert/strict";
import {
ANTIGRAVITY_PUBLIC_MODELS,
resolveAntigravityModelId,
} from "../../open-sse/config/antigravityModelAliases.ts";
test("(#3696) resolveAntigravityModelId passes gemini-3.1-pro-high through unchanged", () => {
assert.equal(resolveAntigravityModelId("gemini-3.1-pro-high"), "gemini-3.1-pro-high");
});
test("(#3696) resolveAntigravityModelId passes gemini-3.1-pro-low through unchanged", () => {
assert.equal(resolveAntigravityModelId("gemini-3.1-pro-low"), "gemini-3.1-pro-low");
});
test("(#3696) no two ANTIGRAVITY_PUBLIC_MODELS entries resolve to the same upstream id", () => {
const seen = new Map<string, string>();
const collisions: string[] = [];
for (const model of ANTIGRAVITY_PUBLIC_MODELS) {
const upstream = resolveAntigravityModelId(model.id);
if (seen.has(upstream)) {
collisions.push(`${model.id} and ${seen.get(upstream)} both resolve to "${upstream}"`);
} else {
seen.set(upstream, model.id);
}
}
assert.deepEqual(collisions, [], `upstream-id collisions: ${collisions.join("; ")}`);
});
+46
View File
@@ -0,0 +1,46 @@
/**
* #3229 — agy `gemini-3.1-pro-high`/`-low` returned [400] but were MASKED as an empty
* `chat.completion` envelope.
*
* Two parts:
* (a) ORIGINAL FIX (#3229): aliased -high/-low to plain `gemini-3.1-pro` (upstream
* was believed to reject the suffix). SUPERSEDED BY #3696: wire capture via
* `agy --model gemini-3.1-pro-high --log-file` confirmed upstream returns 200 OK
* with the suffixed id; the collapse aliases are now removed so the tier-specific
* id passes through verbatim.
* (b) the non-stream branch fed the 4xx response into the SSE collector, producing a
* synthetic `{"object":"chat.completion","content":""}` instead of a real error →
* build a proper sanitized error body for non-ok upstream responses. (Still valid.)
*/
import test from "node:test";
import assert from "node:assert/strict";
import { resolveAntigravityModelId } from "../../open-sse/config/antigravityModelAliases.ts";
import { buildAntigravityUpstreamError } from "../../open-sse/executors/antigravityUpstreamError.ts";
test("(a) agy gemini-3.1-pro -high/-low budget suffixes pass through to upstream unchanged (#3696)", () => {
// #3696: wire capture confirmed upstream accepts the suffixed ids verbatim.
// The old #3229 collapse aliases ("gemini-3.1-pro-high" → "gemini-3.1-pro") were removed.
assert.equal(resolveAntigravityModelId("gemini-3.1-pro-high"), "gemini-3.1-pro-high");
assert.equal(resolveAntigravityModelId("gemini-3.1-pro-low"), "gemini-3.1-pro-low");
// plain id stays plain
assert.equal(resolveAntigravityModelId("gemini-3.1-pro"), "gemini-3.1-pro");
});
test("(b) a non-ok upstream response becomes a real error body, not an empty chat.completion", () => {
const body = buildAntigravityUpstreamError(
400,
"Bad Request",
JSON.stringify({ error: { code: 400, message: "Model not found: gemini-3.1-pro-high" } })
);
assert.notEqual((body as { object?: string }).object, "chat.completion");
assert.ok(body.error, "must carry an error object");
assert.equal(typeof body.error.message, "string");
// sanitized: no raw stack traces leaked (hard rule #12)
assert.ok(!body.error.message.includes("at /"));
// non-JSON upstream body still yields a valid error envelope
const body2 = buildAntigravityUpstreamError(503, "Service Unavailable", "<html>oops</html>");
assert.ok(body2.error);
assert.notEqual((body2 as { object?: string }).object, "chat.completion");
});
@@ -0,0 +1,219 @@
/**
* #3786 — Antigravity (`agy` / `antigravity`) `gemini-3.1-pro-high` returns HTTP 400
* ("Antigravity upstream error (400)") on recent upstream versions; `gemini-3.1-pro-low`
* still works. OmniRoute sends the requested id VERBATIM (per #3696 wire capture). The
* upstream changed the accepted model-id format for the Pro-high tier and the two
* actively-maintained competitor proxies DISAGREE on the live id:
* - AntigravityManager → `gemini-3.1-pro-high`
* - CLIProxyAPI → `gemini-pro-agent` (display: "Gemini 3.1 Pro (High)")
* - older form → `gemini-3-pro-high`
*
* Because the live id cannot be known from static analysis, we mirror AntigravityManager's
* ROBUST approach: a per-request FALLBACK CHAIN that retries alternative upstream ids on a
* 400, until one succeeds (2xx) or the chain is exhausted (then the original 400 surfaces,
* sanitized — hard rule #12).
*
* The fallback chain lives at EXECUTOR REQUEST-TIME (retry on 400). It is NOT a change to
* the static `resolveAntigravityModelId` map, so the #3696 invariant test stays green.
*/
import test from "node:test";
import assert from "node:assert/strict";
import {
ANTIGRAVITY_PRO_FALLBACK_CHAINS,
getAntigravityModelFallbacks,
} from "../../open-sse/config/antigravityModelAliases.ts";
import { AntigravityExecutor } from "../../open-sse/executors/antigravity.ts";
import { seedAntigravityVersionCache } from "../../open-sse/services/antigravityVersion.ts";
type ChatCompletionPayload = {
object?: string;
choices: Array<{ message: { content: string }; finish_reason: string }>;
};
type ErrorPayload = {
error: { code?: string; message: string };
};
// ---------------------------------------------------------------------------
// Pure helper: getAntigravityModelFallbacks
// ---------------------------------------------------------------------------
test("(#3786) getAntigravityModelFallbacks returns the ordered pro-high chain", () => {
assert.deepEqual(getAntigravityModelFallbacks("gemini-3.1-pro-high"), [
"gemini-3.1-pro-high",
"gemini-pro-agent",
"gemini-3-pro-high",
]);
});
test("(#3786) getAntigravityModelFallbacks returns the ordered pro-low chain", () => {
assert.deepEqual(getAntigravityModelFallbacks("gemini-3.1-pro-low"), [
"gemini-3.1-pro-low",
"gemini-3-pro-low",
]);
});
test("(#3786) getAntigravityModelFallbacks returns [] for unrelated models", () => {
assert.deepEqual(getAntigravityModelFallbacks("gemini-2.5-flash"), []);
assert.deepEqual(getAntigravityModelFallbacks("claude-sonnet-4-6"), []);
assert.deepEqual(getAntigravityModelFallbacks("gemini-3-pro-preview"), []);
assert.deepEqual(getAntigravityModelFallbacks(""), []);
});
test("(#3786) every chain starts with its own key (each candidate listed once)", () => {
for (const [key, chain] of Object.entries(ANTIGRAVITY_PRO_FALLBACK_CHAINS)) {
assert.equal(chain[0], key, `chain for ${key} must start with itself`);
assert.equal(new Set(chain).size, chain.length, `chain for ${key} must have no duplicates`);
}
});
// ---------------------------------------------------------------------------
// Behavioral: executor retries the next candidate on a 400
// ---------------------------------------------------------------------------
function makeSuccessSSE(): Response {
return new Response(
'data: {"response":{"candidates":[{"content":{"parts":[{"text":"OK"}]},"finishReason":"STOP"}]}}\n\n',
{ status: 200, headers: { "Content-Type": "text/event-stream" } }
);
}
function make400(modelId: string): Response {
return new Response(
JSON.stringify({ error: { code: 400, message: `Model not found: ${modelId}` } }),
{ status: 400, headers: { "Content-Type": "application/json" } }
);
}
/** Extract the upstream model id from the serialized request envelope. */
function envelopeModel(init: RequestInit | undefined): string {
try {
return JSON.parse(String(init?.body)).model as string;
} catch {
return "";
}
}
test("(#3786) execute retries pro-high with the next candidate when the first id 400s", async () => {
const executor = new AntigravityExecutor();
const originalFetch = globalThis.fetch;
seedAntigravityVersionCache("2026.04.17-test");
const modelsTried: string[] = [];
globalThis.fetch = (async (_url: string, init?: RequestInit) => {
const m = envelopeModel(init);
modelsTried.push(m);
// First candidate (gemini-3.1-pro-high) → 400, second (gemini-pro-agent) → 200
if (m === "gemini-3.1-pro-high") return make400(m);
return makeSuccessSSE();
}) as typeof fetch;
try {
const result = await executor.execute({
model: "antigravity/gemini-3.1-pro-high",
body: { request: { contents: [] } },
stream: false,
credentials: { accessToken: "token", projectId: "project-1" },
log: { debug() {}, warn() {}, info() {} },
});
const payload = (await result.response.json()) as ChatCompletionPayload;
assert.equal(result.response.status, 200, "second candidate should succeed");
assert.equal(payload.choices[0].message.content, "OK");
// Exactly two upstream calls: the 400 then the 200 on the next id.
assert.deepEqual(modelsTried, ["gemini-3.1-pro-high", "gemini-pro-agent"]);
} finally {
globalThis.fetch = originalFetch;
}
});
test("(#3786) execute exhausts the chain on all-400 and surfaces a sanitized 400 (each candidate tried once)", async () => {
const executor = new AntigravityExecutor();
const originalFetch = globalThis.fetch;
seedAntigravityVersionCache("2026.04.17-test");
const modelsTried: string[] = [];
globalThis.fetch = (async (_url: string, init?: RequestInit) => {
const m = envelopeModel(init);
modelsTried.push(m);
return make400(m); // every candidate fails with 400
}) as typeof fetch;
try {
const result = await executor.execute({
model: "antigravity/gemini-3.1-pro-high",
body: { request: { contents: [] } },
stream: false,
credentials: { accessToken: "token", projectId: "project-1" },
log: { debug() {}, warn() {}, info() {} },
});
const payload = (await result.response.json()) as ErrorPayload;
// Surfaces a real, sanitized 400 — not a masked empty chat.completion.
assert.equal(result.response.status, 400);
assert.ok(payload.error, "must carry an error object");
assert.equal(typeof payload.error.message, "string");
assert.ok(!payload.error.message.includes("at /"), "no raw stack trace (hard rule #12)");
// Each candidate tried EXACTLY once (bounded — no infinite loop).
assert.deepEqual(modelsTried, ["gemini-3.1-pro-high", "gemini-pro-agent", "gemini-3-pro-high"]);
} finally {
globalThis.fetch = originalFetch;
}
});
test("(#3786) happy path: first id 200 makes exactly ONE upstream call (zero extra)", async () => {
const executor = new AntigravityExecutor();
const originalFetch = globalThis.fetch;
seedAntigravityVersionCache("2026.04.17-test");
const modelsTried: string[] = [];
globalThis.fetch = (async (_url: string, init?: RequestInit) => {
modelsTried.push(envelopeModel(init));
return makeSuccessSSE();
}) as typeof fetch;
try {
const result = await executor.execute({
model: "antigravity/gemini-3.1-pro-high",
body: { request: { contents: [] } },
stream: false,
credentials: { accessToken: "token", projectId: "project-1" },
log: { debug() {}, warn() {}, info() {} },
});
assert.equal(result.response.status, 200);
assert.deepEqual(modelsTried, ["gemini-3.1-pro-high"], "exactly one call on the happy path");
} finally {
globalThis.fetch = originalFetch;
}
});
test("(#3786) a non-pro model that 400s does NOT trigger the fallback chain", async () => {
const executor = new AntigravityExecutor();
const originalFetch = globalThis.fetch;
seedAntigravityVersionCache("2026.04.17-test");
const modelsTried: string[] = [];
globalThis.fetch = (async (_url: string, init?: RequestInit) => {
modelsTried.push(envelopeModel(init));
return make400(envelopeModel(init));
}) as typeof fetch;
try {
const result = await executor.execute({
model: "antigravity/gemini-2.5-flash",
body: { request: { contents: [] } },
stream: false,
credentials: { accessToken: "token", projectId: "project-1" },
log: { debug() {}, warn() {}, info() {} },
});
// flash 400 surfaces directly — only the requested id is tried, no chain.
assert.equal(result.response.status, 400);
assert.deepEqual(modelsTried, ["gemini-2.5-flash"]);
} finally {
globalThis.fetch = originalFetch;
}
});
+44
View File
@@ -0,0 +1,44 @@
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"
);
});
});
+80
View File
@@ -0,0 +1,80 @@
import test from "node:test";
import assert from "node:assert/strict";
import { AI_PROVIDERS, USAGE_SUPPORTED_PROVIDERS } from "../../src/shared/constants/providers.ts";
import { REGISTRY } from "../../open-sse/config/providerRegistry.ts";
import { PROVIDERS as LEGACY_PROVIDERS } from "../../open-sse/config/constants.ts";
import {
PROVIDERS as OAUTH_PROVIDER_IDS,
AGY_CONFIG,
} from "../../src/lib/oauth/constants/oauth.ts";
import { supportsTokenRefresh, REFRESH_LEAD_MS } from "../../open-sse/services/tokenRefresh.ts";
import {
AGY_PUBLIC_MODELS,
isUserCallableAgyModelId,
getClientVisibleAgyModelName,
} from "../../open-sse/config/agyModels.ts";
test("agy is registered as an OAuth provider in the UI catalog", () => {
const agy = AI_PROVIDERS.agy;
assert.ok(agy, "AI_PROVIDERS.agy must exist");
assert.equal(agy.id, "agy");
assert.equal(agy.name, "Antigravity CLI");
assert.equal(agy.riskNoticeVariant, "oauth");
assert.equal(agy.subscriptionRisk, true);
});
test("agy supports the usage/quota API", () => {
assert.ok(USAGE_SUPPORTED_PROVIDERS.includes("agy"));
});
test("agy registry entry reuses the antigravity backend (no duplicate executor/format)", () => {
const agy = REGISTRY.agy;
assert.ok(agy, "REGISTRY.agy must exist");
assert.equal(agy.format, "antigravity");
assert.equal(agy.executor, "antigravity");
assert.equal(agy.authType, "oauth");
assert.equal(agy.authHeader, "bearer");
assert.equal(agy.passthroughModels, true);
});
test("agy reuses the identical antigravity Google OAuth credentials (no new embedded secret)", () => {
// The agy client_id was verified byte-for-byte identical to antigravity's.
assert.equal(LEGACY_PROVIDERS.agy.clientId, LEGACY_PROVIDERS.antigravity.clientId);
assert.equal(LEGACY_PROVIDERS.agy.clientSecret, LEGACY_PROVIDERS.antigravity.clientSecret);
assert.equal(AGY_CONFIG.clientId, LEGACY_PROVIDERS.antigravity.clientId);
assert.equal(OAUTH_PROVIDER_IDS.AGY, "agy");
});
test("agy ships its own catalog including the Claude models antigravity omits", () => {
const ids = REGISTRY.agy.models.map((m) => m.id);
assert.ok(ids.includes("claude-opus-4-6-thinking"), "must expose Claude Opus 4.6 Thinking");
assert.ok(ids.includes("claude-sonnet-4-6"), "must expose Claude Sonnet 4.6");
assert.ok(ids.includes("gemini-3.5-flash-low"), "must expose clean Flash Low tier");
assert.ok(ids.includes("gemini-3.5-flash-medium"), "must expose clean Flash Medium tier");
assert.ok(ids.includes("gemini-3.5-flash-high"), "must expose clean Flash High tier");
assert.ok(!ids.includes("gemini-3-flash-agent"));
assert.ok(!ids.includes("gemini-3-flash"));
assert.ok(!ids.includes("gemini-3.5-flash-extra-low"));
// Tab-completion models are not chat-callable and must be excluded.
assert.ok(!ids.includes("tab_flash_lite_preview"));
assert.ok(!ids.includes("tab_jump_flash_lite_preview"));
assert.equal(ids.length, AGY_PUBLIC_MODELS.length);
});
test("agy model helpers resolve catalog ids and display names", () => {
assert.equal(isUserCallableAgyModelId("claude-opus-4-6-thinking"), true);
assert.equal(isUserCallableAgyModelId("tab_flash_lite_preview"), false);
assert.equal(isUserCallableAgyModelId(""), false);
assert.equal(
getClientVisibleAgyModelName("claude-opus-4-6-thinking"),
"Claude Opus 4.6 (Thinking)"
);
assert.equal(getClientVisibleAgyModelName("unknown-model", "Fallback"), "Fallback");
});
test("agy token refresh is wired on the Google (non-rotating) refresh path", () => {
assert.equal(supportsTokenRefresh("agy"), true);
// Same 15-minute proactive lead as antigravity (Google refresh tokens are permanent).
assert.equal(REFRESH_LEAD_MS.agy, REFRESH_LEAD_MS.antigravity);
});
+89
View File
@@ -0,0 +1,89 @@
import test from "node:test";
import assert from "node:assert/strict";
const usageModule = await import("../../open-sse/services/usage.ts");
const providerLimitUtils =
await import("../../src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.tsx");
test("agy is registered for the background usage fetcher", () => {
assert.ok(
usageModule.USAGE_FETCHER_PROVIDERS.includes("agy"),
"agy should be fetched by the generic quota refresher"
);
});
test("getUsageForProvider routes agy through the Antigravity usage implementation", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = async () =>
new Response(
JSON.stringify({
models: {
"gemini-3-flash-agent": {
quotaInfo: {
remainingFraction: 0.75,
resetTime: "2026-06-06T00:00:00Z",
},
},
},
}),
{ status: 200, headers: { "Content-Type": "application/json" } }
);
try {
const result = await usageModule.getUsageForProvider(
{
id: "agy-test-conn",
provider: "agy",
accessToken: "fake-token",
providerSpecificData: {},
},
{ forceRefresh: true }
);
assert.ok(result && typeof result === "object");
assert.notEqual(
(result as { message?: string }).message,
"Usage API not implemented for agy",
"agy must not fall through to the unsupported-provider branch"
);
assert.ok("quotas" in result, "agy should return quota data when upstream responds");
const quota = (result as { quotas: Record<string, any> }).quotas["gemini-3.5-flash-high"];
assert.ok(quota, "should expose the clean agy per-model quota");
assert.equal(quota.remainingPercentage, 75);
assert.equal(
(result as { quotas: Record<string, any> }).quotas["gemini-3-flash-agent"],
undefined,
"agy quota should not expose retired upstream IDs"
);
} finally {
globalThis.fetch = originalFetch;
}
});
test("parseQuotaData treats agy quota payloads like Antigravity", () => {
const parsed = providerLimitUtils.parseQuotaData("agy", {
quotas: {
credits: { remaining: 42 },
"gemini-3.5-flash-high": {
used: 250,
total: 1000,
remainingPercentage: 75,
},
models: { used: 0, total: 0 },
},
});
assert.equal(
parsed.length,
2,
"credits and model quota should be rendered, models summary skipped"
);
const credits = parsed.find((quota: any) => quota.name === "credits");
assert.ok(credits, "credits quota should be rendered");
assert.equal(credits.isCredits, true);
const modelQuota = parsed.find((quota: any) => quota.name === "gemini-3.5-flash-high");
assert.ok(modelQuota, "model quota should be rendered");
assert.equal(modelQuota.remainingPercentage, 75);
});
@@ -0,0 +1,71 @@
/**
* 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)}`
);
});
@@ -0,0 +1,74 @@
/**
* Tests for Anthropic billing header fingerprint stability (#1638).
*
* Validates that the billing header fingerprint is stable across different
* messages within the same day, preventing prompt-cache prefix invalidation.
*/
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
// Replicate the stabilized fingerprint logic from base.ts
function computeStableFingerprint(ccVersion: string): string {
const dayStamp = new Date().toISOString().slice(0, 10); // YYYY-MM-DD
return createHash("sha256").update(`${dayStamp}${ccVersion}`).digest("hex").slice(0, 3);
}
// The old implementation for comparison
function computeOldFingerprint(firstUserMessageText: string, version: string): string {
const FINGERPRINT_SALT = "59cf53e54c78";
const indices = [4, 7, 20];
const chars = indices.map((i) => firstUserMessageText[i] || "0").join("");
const input = `${FINGERPRINT_SALT}${chars}${version}`;
return createHash("sha256").update(input).digest("hex").slice(0, 3);
}
describe("Anthropic billing header fingerprint (#1638)", () => {
const ccVersion = "2.1.137";
it("should produce the same fingerprint for different messages (stable)", () => {
const fp1 = computeStableFingerprint(ccVersion);
const fp2 = computeStableFingerprint(ccVersion);
assert.equal(fp1, fp2, "Same-day fingerprints should be identical");
});
it("should produce a 3-character hex fingerprint", () => {
const fp = computeStableFingerprint(ccVersion);
assert.equal(fp.length, 3, "Fingerprint should be 3 chars");
assert.ok(/^[a-f0-9]{3}$/.test(fp), `Fingerprint '${fp}' should be lowercase hex`);
});
it("old implementation produces DIFFERENT fingerprints for different messages", () => {
const msg1 = "Hello, how can I help you with your code?";
const msg2 = "Please fix the bug in my application";
const fp1 = computeOldFingerprint(msg1, ccVersion);
const fp2 = computeOldFingerprint(msg2, ccVersion);
assert.notEqual(fp1, fp2, "Old method should differ per message — this was the bug");
});
it("new implementation produces SAME fingerprint regardless of message content", () => {
// The new implementation doesn't use message content at all
const fp1 = computeStableFingerprint(ccVersion);
const fp2 = computeStableFingerprint(ccVersion);
const fp3 = computeStableFingerprint(ccVersion);
assert.equal(fp1, fp2);
assert.equal(fp2, fp3);
});
it("billing header line should be deterministic within the same day", () => {
const fp = computeStableFingerprint(ccVersion);
const billingLine1 = `x-anthropic-billing-header: cc_version=${ccVersion}.${fp}; cc_entrypoint=cli; cch=00000;`;
const billingLine2 = `x-anthropic-billing-header: cc_version=${ccVersion}.${fp}; cc_entrypoint=cli; cch=00000;`;
assert.equal(billingLine1, billingLine2, "Billing lines should be byte-identical");
});
it("should produce different fingerprints for different days", () => {
// Simulate different days by computing manually
const day1 = "2026-04-27";
const day2 = "2026-04-28";
const fp1 = createHash("sha256").update(`${day1}${ccVersion}`).digest("hex").slice(0, 3);
const fp2 = createHash("sha256").update(`${day2}${ccVersion}`).digest("hex").slice(0, 3);
// They should be different (extremely high probability with SHA-256)
assert.notEqual(fp1, fp2, "Different days should produce different fingerprints");
});
});
@@ -0,0 +1,80 @@
// Regression test for the anthropic-compatible connection validator.
//
// Upstream fix: GET /models is not part of the Anthropic API spec; many
// compatible proxies either 404, 401, or 403 on /models even with a valid
// API key. The connection test must therefore not reject the credentials
// solely on a 401/403 from /models — it must fall back to POST /v1/messages
// (the canonical Anthropic auth probe) and treat any non-401/403 messages
// response as proof that the key was accepted.
//
// Ported from decolua/9router 584cf66a (Co-author: Rehan Choirul).
import test from "node:test";
import assert from "node:assert/strict";
const { validateProviderApiKey } = await import("../../src/lib/providers/validation.ts");
const originalFetch = globalThis.fetch;
test.afterEach(() => {
globalThis.fetch = originalFetch;
});
test(
"anthropic-compatible validation falls back to /messages when /models returns 403",
async () => {
const calls: { url: string; method: string }[] = [];
globalThis.fetch = async (url: any, init: any = {}) => {
const u = String(url);
const method = String(init?.method || "GET").toUpperCase();
calls.push({ url: u, method });
if (u.endsWith("/models")) {
return new Response(JSON.stringify({ error: "forbidden on models" }), { status: 403 });
}
// /messages: upstream accepts the key but rejects the toy payload with 400.
return new Response(JSON.stringify({ error: "bad request" }), { status: 400 });
};
const result = await validateProviderApiKey({
provider: "anthropic-compatible-403-on-models",
apiKey: "sk-test",
providerSpecificData: { baseUrl: "https://proxy.example.com/v1/messages" },
});
// BEFORE the fix this returned { valid: false, error: "Invalid API key" }
// because validateAnthropicCompatibleProvider short-circuited on the 403
// from GET /models without ever probing POST /v1/messages.
assert.equal(result.valid, true, "403 on /models alone must NOT mark the key invalid");
assert.equal(result.error, null);
// The validator must actually exercise the messages endpoint.
const messagesCall = calls.find(
(call) => call.url.endsWith("/messages") && call.method === "POST"
);
assert.ok(messagesCall, "expected a POST /messages probe after /models 403");
}
);
test(
"anthropic-compatible validation still rejects when /messages itself returns 401",
async () => {
// Symmetry guard: the fix must NOT make every 403/401 pass. Only the
// messages probe is authoritative — if it also rejects auth, the key is bad.
globalThis.fetch = async (url: any) => {
const u = String(url);
if (u.endsWith("/models")) {
return new Response(JSON.stringify({ error: "no models endpoint" }), { status: 403 });
}
return new Response(JSON.stringify({ error: "invalid_api_key" }), { status: 401 });
};
const result = await validateProviderApiKey({
provider: "anthropic-compatible-truly-bad-key",
apiKey: "sk-bad",
providerSpecificData: { baseUrl: "https://proxy.example.com/v1/messages" },
});
assert.equal(result.valid, false);
assert.equal(result.error, "Invalid API key");
}
);
@@ -0,0 +1,67 @@
// Regression test for #1475 — duplicate case-variant Anthropic headers.
//
// Node/undici's fetch lowercases and MERGES same-name headers, so an outbound
// set carrying both "anthropic-version" and "Anthropic-Version" collapses into
// a single "anthropic-version: 2023-06-01, 2023-06-01" value, which the
// Anthropic API rejects. normalizeAnthropicHeaderVariants() reconciles the two
// case variants down to one canonical lowercase header with a single value
// (and a deduped, joined comma-list for anthropic-beta) before the request is
// dispatched in BaseExecutor.buildHeaders / DefaultExecutor.buildHeaders.
import test from "node:test";
import assert from "node:assert/strict";
import { normalizeAnthropicHeaderVariants } from "../../open-sse/config/anthropicHeaders.ts";
import { DefaultExecutor } from "../../open-sse/executors/default.ts";
test("normalizeAnthropicHeaderVariants collapses case-variant anthropic-version into one value", () => {
const headers: Record<string, string> = {
"anthropic-version": "2023-06-01",
"Anthropic-Version": "2023-06-01",
};
normalizeAnthropicHeaderVariants(headers);
// The mixed-case variant must be gone, and the lowercase one must carry a
// single value (no "v, v" duplication that Anthropic rejects).
assert.equal(headers["Anthropic-Version"], undefined);
assert.equal(headers["anthropic-version"], "2023-06-01");
assert.ok(!headers["anthropic-version"].includes(","));
});
test("normalizeAnthropicHeaderVariants dedupes and joins anthropic-beta variants", () => {
const headers: Record<string, string> = {
"anthropic-beta": "oauth-2025-04-20, prompt-caching",
"Anthropic-Beta": "prompt-caching, fine-grained",
};
normalizeAnthropicHeaderVariants(headers);
assert.equal(headers["Anthropic-Beta"], undefined);
assert.equal(headers["anthropic-beta"], "oauth-2025-04-20,prompt-caching,fine-grained");
});
test("normalizeAnthropicHeaderVariants leaves a single lowercase header untouched", () => {
const headers: Record<string, string> = { "anthropic-version": "2023-06-01" };
normalizeAnthropicHeaderVariants(headers);
assert.equal(headers["anthropic-version"], "2023-06-01");
assert.equal(headers["Anthropic-Version"], undefined);
});
test("normalizeAnthropicHeaderVariants is a no-op when no anthropic headers are present", () => {
const headers: Record<string, string> = { "Content-Type": "application/json" };
normalizeAnthropicHeaderVariants(headers);
assert.deepEqual(headers, { "Content-Type": "application/json" });
});
test("DefaultExecutor.buildHeaders does not emit duplicate anthropic-version variants for an anthropic-compatible provider", () => {
const executor = new DefaultExecutor("anthropic-compatible-test");
const headers = executor.buildHeaders({ apiKey: "sk-test" }, true) as Record<string, string>;
// Whatever case the upstream registry/auth path used, only the canonical
// lowercase header survives, with exactly one value.
assert.equal(headers["Anthropic-Version"], undefined);
if (headers["anthropic-version"] !== undefined) {
assert.ok(!headers["anthropic-version"].includes(","));
}
});
@@ -0,0 +1,51 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { isOfficialAnthropicBaseUrl } from "../../open-sse/utils/anthropicHost.ts";
// CodeQL #674 (js/incomplete-url-substring-sanitization): the official-Anthropic check
// must use exact hostname equality, not a substring `.includes("api.anthropic.com")`, so a
// look-alike upstream cannot impersonate the official endpoint and suppress the Bearer
// fallback intended for third-party gateways.
test("official endpoints are recognized (empty / scheme / scheme-less / path)", () => {
assert.equal(isOfficialAnthropicBaseUrl(""), true, "empty baseUrl = default official");
assert.equal(isOfficialAnthropicBaseUrl("https://api.anthropic.com"), true);
assert.equal(isOfficialAnthropicBaseUrl("https://api.anthropic.com/v1"), true);
assert.equal(isOfficialAnthropicBaseUrl("https://api.anthropic.com/"), true);
// scheme-less host (operator may omit the protocol) is parsed with an assumed https://
assert.equal(isOfficialAnthropicBaseUrl("api.anthropic.com"), true);
assert.equal(isOfficialAnthropicBaseUrl("api.anthropic.com/v1"), true);
});
test("look-alike / third-party hosts are NOT treated as official", () => {
// The exact strings the old substring check would have wrongly accepted:
assert.equal(isOfficialAnthropicBaseUrl("https://api.anthropic.com.evil.test"), false);
assert.equal(isOfficialAnthropicBaseUrl("https://api.anthropic.com.evil.test/v1"), false);
assert.equal(isOfficialAnthropicBaseUrl("https://evil.test/?x=api.anthropic.com"), false);
assert.equal(isOfficialAnthropicBaseUrl("https://evil.test/api.anthropic.com"), false);
assert.equal(isOfficialAnthropicBaseUrl("https://my-gateway.test/anthropic"), false);
// a genuinely different host
assert.equal(isOfficialAnthropicBaseUrl("https://openrouter.ai/api"), false);
});
test("unparseable baseUrl falls back to third-party (Bearer emitted)", () => {
assert.equal(isOfficialAnthropicBaseUrl("http://"), false);
assert.equal(isOfficialAnthropicBaseUrl(":::"), false);
});
test("source no longer uses substring .includes for the official-host check", () => {
const here = path.dirname(fileURLToPath(import.meta.url));
const src = fs.readFileSync(
path.join(here, "../../open-sse/executors/default.ts"),
"utf8"
);
assert.equal(
src.includes('.includes("api.anthropic.com")'),
false,
"default.ts must not substring-match the official Anthropic host (CodeQL #674)"
);
});
@@ -0,0 +1,130 @@
// Regression test for the Anthropic-compatible stream "thinking block never closes" bug.
//
// When clients consume the OpenAI-compatible stream that OmniRoute synthesises from
// Claude-native SSE, they need an explicit signal that the thinking/reasoning section
// has ended; otherwise the UI stays stuck on the "thinking" indicator even after the
// upstream stream has cleanly completed.
//
// Inspired by upstream decolua/9router PR #454.
//
// Before the fix, the `content_block_stop` event for a thinking block emitted NO
// terminating chunk at all (a previous drift had emitted `reasoning_content: ""`,
// which is semantically a no-op and does not signal "thinking complete" to clients
// such as Claude Code).
//
// PR #4633 added an immediate `content: "</think>"` chunk on close.
//
// PR #5123 refined this to DEFERRED emission: instead of emitting at content_block_stop
// (which caused the marker to leak before tool_calls in tool-use streams), the marker is
// now queued and flushed either:
// • at the first text_delta that follows (preserving #4633 for Claude Code / Cursor), or
// • at message_delta finish when there are no tool_calls (pure thinking-only responses).
// This test covers the message_delta flush path (thinking block with no subsequent text).
import test from "node:test";
import assert from "node:assert/strict";
const { claudeToOpenAIResponse } = await import(
"../../open-sse/translator/response/claude-to-openai.ts"
);
function newState() {
return {
toolCalls: new Map(),
toolNameMap: new Map(),
messageId: "msg_test",
model: "claude-3-7-sonnet",
toolCallIndex: 0,
};
}
test("claudeToOpenAIResponse emits </think> close marker on message finish (deferred from content_block_stop)", () => {
const state = newState();
// Open thinking block.
claudeToOpenAIResponse(
{
type: "content_block_start",
index: 0,
content_block: { type: "thinking", thinking: "" },
},
state
);
// Stream reasoning delta.
claudeToOpenAIResponse(
{
type: "content_block_delta",
index: 0,
delta: { type: "thinking_delta", thinking: "Plan first." },
},
state
);
// Close the thinking block — marker is now DEFERRED (not emitted here).
const closeChunks = claudeToOpenAIResponse(
{ type: "content_block_stop", index: 0 },
state
);
// content_block_stop for a thinking block no longer emits </think> immediately
// (the marker is deferred to prevent leaking before tool_calls — see #5123).
const immediateClose = Array.isArray(closeChunks) ? closeChunks : [];
const hasImmediateMarker = immediateClose.some(
(chunk) => chunk?.choices?.[0]?.delta?.content === "</think>"
);
assert.equal(
hasImmediateMarker,
false,
"content_block_stop must NOT emit </think> immediately (deferred — see #5123)"
);
// After close, the thinking-block flag is cleared and the pending marker is queued.
assert.equal(state.inThinkingBlock, false);
assert.equal(state.pendingThinkClose, true, "pendingThinkClose must be set after thinking block stop");
// message_delta with stop_reason=end_turn and no tool_calls → marker must be flushed here.
const finishChunks = claudeToOpenAIResponse(
{ type: "message_delta", delta: { stop_reason: "end_turn" }, usage: { output_tokens: 5 } },
state
);
const arr = Array.isArray(finishChunks) ? finishChunks : [];
const hasCloseMarker = arr.some(
(chunk) => chunk?.choices?.[0]?.delta?.content === "</think>"
);
assert.ok(
hasCloseMarker,
`expected a chunk with delta.content === "</think>" in message_delta result; got ${JSON.stringify(arr)}`
);
// pendingThinkClose must be cleared after flush.
assert.equal(state.pendingThinkClose, false, "pendingThinkClose must be cleared after flush");
});
test("claudeToOpenAIResponse does not emit </think> on stop of non-thinking blocks", () => {
const state = newState();
// Open + immediately close a text block — must NOT inject </think>.
claudeToOpenAIResponse(
{
type: "content_block_start",
index: 0,
content_block: { type: "text", text: "" },
},
state
);
const closeChunks = claudeToOpenAIResponse(
{ type: "content_block_stop", index: 0 },
state
);
const arr = Array.isArray(closeChunks) ? closeChunks : [];
const hasCloseMarker = arr.some(
(chunk) => chunk?.choices?.[0]?.delta?.content === "</think>"
);
assert.equal(
hasCloseMarker,
false,
"text-block close must not emit </think> sentinel"
);
});
@@ -0,0 +1,113 @@
/**
* TDD regression tests for #3707:
* 1. `decide429("quota_exhausted")` → `full_quota_exhausted` verdict (engine contract)
* 2. `markConnectionQuotaExhausted` persists the 24h cooldown in the DB so that
* cross-request and post-restart routing skips exhausted connections.
*
* Bug: before the fix the executor never called `setConnectionRateLimitUntil`,
* so `isConnectionRateLimited` always returned false for AG connections that
* had their daily quota exhausted — learned state was lost on restart.
*/
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-ag-quota-"));
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");
import {
classify429,
decide429,
FULL_QUOTA_COOLDOWN_MS,
} from "../../open-sse/services/antigravity429Engine.ts";
import { markConnectionQuotaExhausted } from "../../open-sse/executors/antigravity.ts";
test.after(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
// ── Engine contract (regression guard) ───────────────────────────────────────
test("decide429: quota_exhausted category → full_quota_exhausted kind with 24h cooldown", () => {
const decision = decide429("quota_exhausted", null);
assert.equal(decision.kind, "full_quota_exhausted");
assert.equal(decision.retryAfterMs, FULL_QUOTA_COOLDOWN_MS);
assert.equal(FULL_QUOTA_COOLDOWN_MS, 24 * 60 * 60 * 1000, "cooldown must be 24h");
});
test("decide429: quota_exhausted with explicit retryAfterMs preserves the provided value", () => {
const twoDaysMs = 2 * 24 * 60 * 60 * 1000;
const decision = decide429("quota_exhausted", twoDaysMs);
assert.equal(decision.kind, "full_quota_exhausted");
assert.equal(decision.retryAfterMs, twoDaysMs);
});
test("classify429: AG 'Individual quota reached' message → quota_exhausted", () => {
const msg =
"Individual quota reached. Contact your administrator to enable overages. Resets in 14h22m.";
assert.equal(classify429(msg), "quota_exhausted");
});
test("classify429: AG G1 Credits Exhausted message → quota_exhausted", () => {
assert.equal(classify429("insufficient_g1_credits_balance"), "quota_exhausted");
});
test("classify429: standard Gemini rate limit 'resource has been exhausted' -> rate_limited or unknown, not quota_exhausted", () => {
const msg =
"RESOURCE_EXHAUSTED: Resource has been exhausted (e.g. queries per minute limit was reached).";
const result = classify429(msg);
assert.notEqual(
result,
"quota_exhausted",
"RESOURCE_EXHAUSTED rate limit should not be classified as quota_exhausted"
);
});
// ── DB persistence (the missing wire — Bug #2) ───────────────────────────────
test("markConnectionQuotaExhausted persists 24h cooldown; isConnectionRateLimited returns true", async () => {
const conn = await providersDb.createProviderConnection({
provider: "antigravity",
authType: "oauth",
name: "AG Test Quota",
});
const connId = (conn as any).id;
assert.equal(
providersDb.isConnectionRateLimited(connId),
false,
"should start as not rate-limited"
);
markConnectionQuotaExhausted(connId, FULL_QUOTA_COOLDOWN_MS);
assert.equal(
providersDb.isConnectionRateLimited(connId),
true,
"should be rate-limited after marking quota exhausted"
);
});
test("markConnectionQuotaExhausted: expired cooldown does not block the connection", async () => {
const conn = await providersDb.createProviderConnection({
provider: "antigravity",
authType: "oauth",
name: "AG Test Expired",
});
const connId = (conn as any).id;
// Set cooldown in the past — simulates expired cooldown
providersDb.setConnectionRateLimitUntil(connId, Date.now() - 1);
assert.equal(
providersDb.isConnectionRateLimited(connId),
false,
"expired cooldown should not block"
);
});
@@ -0,0 +1,88 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { classify429 as classify429AG } from "../../open-sse/services/antigravity429Engine.ts";
import { classify429 as classify429Shared } from "../../src/shared/utils/classify429.ts";
import {
parseRetryFromErrorText,
checkFallbackError,
} from "../../open-sse/services/accountFallback.ts";
test("TDD S1: classify429 (Antigravity engine) detects INSUFFICIENT_G1_CREDITS_BALANCE", () => {
const msg = JSON.stringify({
error: {
code: 429,
message: "Resource has been exhausted (e.g. check quota).",
status: "RESOURCE_EXHAUSTED",
details: [
{
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
reason: "INSUFFICIENT_G1_CREDITS_BALANCE",
},
],
},
});
const category = classify429AG(msg);
assert.equal(category, "quota_exhausted");
});
test("TDD S1: classify429 (Shared utility) detects INSUFFICIENT_G1_CREDITS_BALANCE", () => {
const body = {
error: {
code: 429,
message: "Resource has been exhausted (e.g. check quota).",
status: "RESOURCE_EXHAUSTED",
details: [
{
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
reason: "INSUFFICIENT_G1_CREDITS_BALANCE",
},
],
},
};
const kind = classify429Shared({ status: 429, body });
assert.equal(kind, "quota_exhausted");
});
test("TDD S2: Regression: standard Gemini rate limit 'queries per minute limit was reached' -> rate_limit (shared) and rate_limited (AG)", () => {
const msg =
"RESOURCE_EXHAUSTED: Resource has been exhausted (e.g. queries per minute limit was reached).";
assert.notEqual(classify429AG(msg), "quota_exhausted");
assert.equal(classify429Shared({ status: 429, body: msg }), "rate_limit");
});
test("TDD S3: parseRetryFromErrorText parses resets in 5h and resets in 164h27m24s", () => {
// Antigravity returns: "Individual quota reached. Contact your administrator to enable overages. Resets in 5h."
const msg5h =
"Individual quota reached. Contact your administrator to enable overages. Resets in 5h.";
const msgWeekly =
"Individual quota reached. Contact your administrator to enable overages. Resets in 164h27m24s.";
const val5h = parseRetryFromErrorText(msg5h);
assert.equal(val5h, 5 * 3600 * 1000);
const valWeekly = parseRetryFromErrorText(msgWeekly);
assert.equal(valWeekly, 164 * 3600 * 1000 + 27 * 60 * 1000 + 24 * 1000);
});
test("TDD S3: checkFallbackError extracts retry hint for oauth providers even if useUpstreamRetryHints is false", () => {
const errorText =
"Individual quota reached. Contact your administrator to enable overages. Resets in 5h.";
const res = checkFallbackError(
429,
errorText,
0,
"gemini-3.5-flash",
"antigravity", // which uses oauth provider profile (useUpstreamRetryHints: false)
null
);
assert.equal(res.shouldFallback, true);
assert.equal(res.usedUpstreamRetryHint, false);
// Connection cooldown should be the default/scaled backoff cooldown (e.g. ~5000 ms) because useUpstreamRetryHints is false
assert.notEqual(res.cooldownMs, 5 * 3600 * 1000);
// But quotaResetHintMs MUST be the precise parsed reset time (5h = 18,000,000 ms)
assert.equal(res.quotaResetHintMs, 5 * 3600 * 1000);
});
@@ -0,0 +1,101 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import {
AntigravityExecutor,
__test_stripTrailingAntigravityAssistantTurn,
} from "../../open-sse/executors/antigravity.ts";
/**
* Ports decolua/9router#2321 (anki1kr): Vertex AI (used by Antigravity for
* Claude-branded models) rejects a conversation ending on an assistant turn —
* "This model does not support assistant message prefill" — so the request must
* always end on a user turn.
*
* Upstream's diff patched `openaiToClaudeRequestForAntigravity` in
* `open-sse/translator/request/openai-to-claude.ts`, which has ZERO callers in
* OmniRoute (dead code). The live Antigravity Claude dispatch path converts to
* Gemini `contents` (`role: "user"/"model"`) in `AntigravityExecutor.transformRequest`
* via `sanitizeAntigravityGeminiRequest` — this test drives THAT function end-to-end.
*/
async function transform(model: string, contents: Array<Record<string, unknown>>) {
const executor = new AntigravityExecutor();
const body = {
request: {
contents,
generationConfig: {},
},
};
const result = await executor.transformRequest(model, body, true, {
projectId: "project-1",
});
if (result instanceof Response) throw new Error("Unexpected Response from transformRequest");
return (result as Record<string, unknown>).request as Record<string, unknown>;
}
test("(a) strips a single trailing assistant (model) turn for Claude models", async () => {
const request = await transform("antigravity/claude-opus-4-8", [
{ role: "user", parts: [{ text: "Hello" }] },
{ role: "model", parts: [{ text: "Hi there" }] }, // prefill to strip
]);
const contents = request.contents as Array<{ role: string }>;
assert.equal(contents.length, 1);
assert.equal(contents.at(-1)?.role, "user");
});
test("(b) does NOT strip a trailing model turn for non-Claude (native Gemini) models", async () => {
const request = await transform("antigravity/gemini-3.1-pro", [
{ role: "user", parts: [{ text: "Hello" }] },
{ role: "model", parts: [{ text: "Hi there" }] },
]);
const contents = request.contents as Array<{ role: string }>;
assert.equal(contents.length, 2);
assert.equal(contents.at(-1)?.role, "model", "native Gemini requests via Antigravity are untouched");
});
test("(c) a Claude conversation already ending on user is unchanged", async () => {
const request = await transform("antigravity/claude-opus-4-8", [
{ role: "user", parts: [{ text: "Hello" }] },
{ role: "model", parts: [{ text: "Hi" }] },
{ role: "user", parts: [{ text: "What is 2+2?" }] },
]);
const contents = request.contents as Array<{ role: string }>;
assert.equal(contents.length, 3);
assert.equal(contents.at(-1)?.role, "user");
});
test("(d) multiple trailing model turns are all stripped", () => {
// Under normal executor flow, adjacent same-role turns are merged before this
// helper runs — this directly exercises the helper's robustness for an input
// that (defensively) still carries multiple consecutive trailing "model" turns.
const request = __test_stripTrailingAntigravityAssistantTurn({
contents: [
{ role: "user", parts: [{ text: "Hello" }] },
{ role: "model", parts: [{ text: "A" }] },
{ role: "model", parts: [{ text: "B" }] },
],
});
const contents = request.contents as Array<{ role: string }>;
assert.equal(contents.length, 1);
assert.equal(contents.at(-1)?.role, "user");
});
test("(e) never strips contents down to empty", () => {
const request = __test_stripTrailingAntigravityAssistantTurn({
contents: [{ role: "model", parts: [{ text: "solo prefill" }] }],
});
const contents = request.contents as Array<{ role: string }>;
// A lone trailing "model" turn is preserved rather than emptying `contents`
// (an empty contents array is itself an invalid upstream request).
assert.equal(contents.length, 1);
assert.equal(contents[0].role, "model");
});
test("empty/missing contents does not throw", () => {
const request = __test_stripTrailingAntigravityAssistantTurn({ contents: [] });
assert.deepEqual(request.contents, []);
const request2 = __test_stripTrailingAntigravityAssistantTurn({});
assert.equal(request2.contents, undefined);
});
@@ -0,0 +1,143 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { createRequire } from "node:module";
import { normalizeAntigravityClientProfile } from "../../src/shared/constants/antigravityClientProfile.ts";
import {
applyAntigravityClientProfileHeaders,
getAntigravityBootstrapHeaders,
antigravityHarnessUserAgent,
getAntigravityClientProfile,
} from "../../open-sse/services/antigravityClientProfile.ts";
import { antigravityUserAgent } from "../../open-sse/services/antigravityHeaders.ts";
import { deriveAntigravityMachineId } from "../../open-sse/services/antigravityIdentity.ts";
import {
seedAntigravityVersionCache,
clearAntigravityVersionCache,
} from "../../open-sse/services/antigravityVersion.ts";
const require = createRequire(import.meta.url);
const { machineIdSync } = require("node-machine-id") as {
machineIdSync: (original?: boolean) => string;
};
test.afterEach(() => {
clearAntigravityVersionCache();
});
test("normalizeAntigravityClientProfile maps cli/sdk aliases to harness", () => {
assert.equal(normalizeAntigravityClientProfile("cli"), "harness");
assert.equal(normalizeAntigravityClientProfile("SDK"), "harness");
assert.equal(normalizeAntigravityClientProfile("ide"), "ide");
assert.equal(normalizeAntigravityClientProfile(undefined), "ide");
});
test("getAntigravityClientProfile reads providerSpecificData.clientProfile per connection", () => {
assert.equal(
getAntigravityClientProfile({
providerSpecificData: { clientProfile: "harness" },
}),
"harness"
);
assert.equal(getAntigravityClientProfile({ providerSpecificData: {} }), "ide");
});
test("applyAntigravityClientProfileHeaders uses IDE fingerprint by default", () => {
seedAntigravityVersionCache("4.2.0");
const headers: Record<string, string> = {
Authorization: "Bearer token",
"Content-Type": "application/json",
};
applyAntigravityClientProfileHeaders(
headers,
{ connectionId: "conn-1", providerSpecificData: {} },
{ project: "project-1" }
);
assert.match(headers["User-Agent"], /^Antigravity\/4\.2\.0 /);
assert.equal(headers["x-client-name"], "antigravity");
assert.equal(headers["x-client-version"], "4.2.0");
assert.equal(typeof headers["x-machine-id"], "string");
assert.equal(typeof headers["x-vscode-sessionid"], "string");
assert.equal(headers["x-goog-user-project"], "project-1");
assert.equal(headers["X-Goog-Api-Client"], undefined);
});
test("antigravityUserAgent matches Antigravity Manager platform fingerprints", () => {
assert.equal(
antigravityUserAgent("4.2.0", "darwin"),
"Antigravity/4.2.0 (Macintosh; Intel Mac OS X 10_15_7) Chrome/142.0.7444.175 Electron/39.2.3"
);
assert.equal(
antigravityUserAgent("4.2.0", "win32"),
"Antigravity/4.2.0 (Windows NT 10.0; Win64; x64) Chrome/142.0.7444.175 Electron/39.2.3"
);
assert.equal(
antigravityUserAgent("4.2.0", "linux"),
"Antigravity/4.2.0 (X11; Linux x86_64) Chrome/142.0.7444.175 Electron/39.2.3"
);
});
test("deriveAntigravityMachineId uses the raw system machine id like Antigravity Manager", () => {
let systemMachineId: string | null = null;
try {
systemMachineId = machineIdSync(true);
} catch {
systemMachineId = null;
}
if (!systemMachineId) return;
assert.equal(deriveAntigravityMachineId(), systemMachineId);
});
test("antigravityHarnessUserAgent uses Go harness platform and arch names", () => {
assert.equal(
antigravityHarnessUserAgent("2.1.1", "darwin", "arm64"),
"antigravity/2.1.1 darwin/arm64"
);
assert.equal(
antigravityHarnessUserAgent("2.1.1", "win32", "x64"),
"antigravity/2.1.1 windows/amd64"
);
assert.equal(
antigravityHarnessUserAgent("2.1.1", "linux", "x64"),
"antigravity/2.1.1 linux/amd64"
);
});
test("getAntigravityBootstrapHeaders uses SDK loadCodeAssist harness fingerprint", () => {
seedAntigravityVersionCache("2.1.1");
const headers = getAntigravityBootstrapHeaders("harness", "token");
assert.equal(headers.Authorization, "Bearer token");
assert.equal(
headers["User-Agent"],
`${antigravityHarnessUserAgent("2.1.1")} google-api-nodejs-client/10.3.0`
);
assert.equal(headers["X-Goog-Api-Client"], "gl-node/22.21.1");
assert.equal(headers["Client-Metadata"], undefined);
});
test("applyAntigravityClientProfileHeaders uses harness fingerprint when configured", () => {
seedAntigravityVersionCache("4.2.0");
const headers: Record<string, string> = {
Authorization: "Bearer token",
"Content-Type": "application/json",
"x-client-name": "antigravity",
"x-vscode-sessionid": "old-session",
};
applyAntigravityClientProfileHeaders(
headers,
{ connectionId: "conn-2", providerSpecificData: { clientProfile: "harness" } },
{ project: "project-2" }
);
assert.equal(headers["User-Agent"], antigravityHarnessUserAgent("4.2.0"));
assert.equal(headers["X-Goog-Api-Client"], undefined);
assert.equal(headers["x-client-name"], undefined);
assert.equal(headers["x-vscode-sessionid"], undefined);
assert.equal(headers["x-goog-user-project"], "project-2");
});
+174
View File
@@ -0,0 +1,174 @@
/**
* Tests for antigravity.ts — AI Credits overages fallback.
*
* Verifies:
* 1. Credit balance cache read/write (getAntigravityRemainingCredits / updateAntigravityRemainingCredits)
* 2. SSE remainingCredits extraction logic from collectStreamToResponse
* 3. accountId consistency: executor and fetcher must use the same key (email || sub)
* 4. Balance updates are correctly reflected in subsequent reads
*/
import { describe, it } from "node:test";
import assert from "node:assert/strict";
// ── Import credit cache helpers ───────────────────────────────────────────────
import {
getAntigravityRemainingCredits,
updateAntigravityRemainingCredits,
} from "../../open-sse/executors/antigravity.ts";
// ── Credit balance cache tests ────────────────────────────────────────────────
describe("getAntigravityRemainingCredits / updateAntigravityRemainingCredits", () => {
it("returns null for an account with no cached balance", () => {
const accountId = `test-unknown-${Date.now()}`;
assert.equal(
getAntigravityRemainingCredits(accountId),
null,
"should return null before any update"
);
});
it("returns the balance after updateAntigravityRemainingCredits", () => {
const accountId = `test-write-${Date.now()}`;
updateAntigravityRemainingCredits(accountId, 42);
assert.equal(getAntigravityRemainingCredits(accountId), 42, "stored balance should be 42");
});
it("overwrites a previous balance with a new value", () => {
const accountId = `test-overwrite-${Date.now()}`;
updateAntigravityRemainingCredits(accountId, 100);
updateAntigravityRemainingCredits(accountId, 55);
assert.equal(getAntigravityRemainingCredits(accountId), 55, "should reflect the latest update");
});
it("stores balance=0 correctly (not treated as null/falsy)", () => {
const accountId = `test-zero-${Date.now()}`;
updateAntigravityRemainingCredits(accountId, 0);
assert.equal(
getAntigravityRemainingCredits(accountId),
0,
"balance 0 must be stored and returned as 0"
);
});
it("different accountIds do not interfere with each other", () => {
const idA = `test-a-${Date.now()}`;
const idB = `test-b-${Date.now()}`;
updateAntigravityRemainingCredits(idA, 200);
updateAntigravityRemainingCredits(idB, 999);
assert.equal(getAntigravityRemainingCredits(idA), 200);
assert.equal(getAntigravityRemainingCredits(idB), 999);
});
});
// ── accountId key consistency ─────────────────────────────────────────────────
describe("accountId key consistency: executor vs fetcher derivation", () => {
it("both executor and fetcher use email → sub → 'unknown' order", () => {
// Enforces the contract between executor (write) and fetcher (read) for creditBalanceCache.
const credentials = { email: "user@example.com", sub: "abc123" };
const providerSpecificData = { email: "user@example.com", sub: "abc123" };
// executor derivation
const executorAccountId = credentials.email || credentials.sub || "unknown";
// fetcher derivation
const fetcherAccountId = providerSpecificData.email || providerSpecificData.sub || "unknown";
assert.equal(
executorAccountId,
fetcherAccountId,
"accountId must match between executor and fetcher"
);
});
it("falls back to sub when email is absent — both paths agree", () => {
const credentials = { sub: "sub-only-123" };
const providerSpecificData = { sub: "sub-only-123" };
const executorAccountId = credentials.email || credentials.sub || "unknown";
const fetcherAccountId = providerSpecificData.email || providerSpecificData.sub || "unknown";
assert.equal(executorAccountId, "sub-only-123");
assert.equal(fetcherAccountId, "sub-only-123");
assert.equal(executorAccountId, fetcherAccountId);
});
it("both paths return 'unknown' when email and sub are absent", () => {
const credentials = {};
const providerSpecificData = {};
const executorAccountId = credentials.email || credentials.sub || "unknown";
const fetcherAccountId = providerSpecificData.email || providerSpecificData.sub || "unknown";
assert.equal(executorAccountId, "unknown");
assert.equal(fetcherAccountId, "unknown");
});
});
// ── SSE remainingCredits extraction logic ─────────────────────────────────────
describe("SSE remainingCredits extraction logic", () => {
it("parses GOOGLE_ONE_AI credit amount from remainingCredits array", () => {
const remainingCredits = [
{ creditType: "GOOGLE_ONE_AI", creditAmount: "123" },
{ creditType: "SOME_OTHER", creditAmount: "999" },
];
const googleCredit = remainingCredits.find((c) => c.creditType === "GOOGLE_ONE_AI");
assert.ok(googleCredit, "GOOGLE_ONE_AI entry must be found");
const balance = parseInt(googleCredit.creditAmount, 10);
assert.equal(balance, 123, "credit balance must be parsed correctly");
});
it("handles missing GOOGLE_ONE_AI gracefully — no crash", () => {
const remainingCredits = [{ creditType: "SOME_OTHER", creditAmount: "999" }];
const googleCredit = remainingCredits.find((c) => c.creditType === "GOOGLE_ONE_AI");
assert.equal(googleCredit, undefined, "should not find GOOGLE_ONE_AI if not present");
});
it("handles malformed creditAmount gracefully — NaN is not stored", () => {
const remainingCredits = [{ creditType: "GOOGLE_ONE_AI", creditAmount: "not-a-number" }];
const googleCredit = remainingCredits.find((c) => c.creditType === "GOOGLE_ONE_AI");
const balance = parseInt(googleCredit.creditAmount, 10);
assert.ok(isNaN(balance), "NaN guard prevents invalid balance storage");
});
it("balance update is correctly reflected in the cache after a successful parse", () => {
const accountId = `test-sse-${Date.now()}`;
const remainingCredits = [{ creditType: "GOOGLE_ONE_AI", creditAmount: "77" }];
const googleCredit = remainingCredits.find((c) => c.creditType === "GOOGLE_ONE_AI");
if (googleCredit) {
const balance = parseInt(googleCredit.creditAmount, 10);
if (!isNaN(balance)) {
updateAntigravityRemainingCredits(accountId, balance);
}
}
assert.equal(getAntigravityRemainingCredits(accountId), 77);
});
it("skips cache update when creditAmount is NaN — balance remains null", () => {
const accountId = `test-nan-guard-${Date.now()}`;
const remainingCredits = [{ creditType: "GOOGLE_ONE_AI", creditAmount: "bad" }];
const googleCredit = remainingCredits.find((c) => c.creditType === "GOOGLE_ONE_AI");
if (googleCredit) {
const balance = parseInt(googleCredit.creditAmount, 10);
if (!isNaN(balance)) {
updateAntigravityRemainingCredits(accountId, balance);
}
// NaN — no update should happen
}
assert.equal(
getAntigravityRemainingCredits(accountId),
null,
"balance should remain null when parsing yields NaN"
);
});
});
@@ -0,0 +1,235 @@
/**
* Tests: antigravity loadCodeAssist bootstrap before :models discovery.
*
* The Google Cloud Code Assist /v1internal:models endpoint requires a prior
* /v1internal:loadCodeAssist call to assign a project context to the OAuth
* token. Without this bootstrap, :models returns 404 for all three base URLs.
*
* These tests verify:
* 1. ensureAntigravityProjectAssigned calls loadCodeAssist before returning.
* 2. The call is memoized — repeated calls for the same token do not re-hit
* the network.
* 3. Non-fatal: if loadCodeAssist fails, the function resolves without throwing.
* 4. The loadCodeAssist request uses the correct headers (Authorization, User-Agent).
* 5. Ordering guarantee — in a full discovery flow, loadCodeAssist is called
* BEFORE any :models request.
*/
import { test, describe, beforeEach } from "node:test";
import assert from "node:assert/strict";
import {
ensureAntigravityProjectAssigned,
clearAntigravityProjectCache,
getAntigravityProjectFromCache,
getAntigravityLoadCodeAssistUrls,
} from "../../open-sse/services/antigravityProjectBootstrap.ts";
// Reset the module-level memoization cache between tests.
beforeEach(() => {
clearAntigravityProjectCache();
});
describe("ensureAntigravityProjectAssigned", () => {
test("calls loadCodeAssist and caches the returned project id", async () => {
const calls: string[] = [];
const mockFetch = async (url: string, _init?: RequestInit): Promise<Response> => {
calls.push(url);
if (url.endsWith(":loadCodeAssist")) {
return new Response(JSON.stringify({ cloudaicompanionProject: "proj-from-bootstrap" }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
return new Response("Not Found", { status: 404 });
};
const projectId = await ensureAntigravityProjectAssigned("fake-token-1", mockFetch);
const loadCalls = calls.filter((u) => u.endsWith(":loadCodeAssist"));
assert.ok(loadCalls.length >= 1, ":loadCodeAssist must be called at least once");
assert.equal(projectId, "proj-from-bootstrap", "project id must be returned");
assert.equal(
getAntigravityProjectFromCache("fake-token-1"),
"proj-from-bootstrap",
"project id must be memoized after first call"
);
});
test("subsequent calls for the same token skip the network", async () => {
let networkCalls = 0;
const mockFetch = async (url: string, _init?: RequestInit): Promise<Response> => {
networkCalls += 1;
return new Response(JSON.stringify({ cloudaicompanionProject: "proj-cached" }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
};
await ensureAntigravityProjectAssigned("fake-token-2", mockFetch);
await ensureAntigravityProjectAssigned("fake-token-2", mockFetch);
await ensureAntigravityProjectAssigned("fake-token-2", mockFetch);
assert.equal(networkCalls, 1, "network must be called exactly once for the same token");
});
test("different tokens each trigger their own loadCodeAssist call", async () => {
const calledFor: string[] = [];
const mockFetch = async (url: string, init?: RequestInit): Promise<Response> => {
const auth = new Headers(init?.headers).get("Authorization") ?? "";
calledFor.push(auth);
return new Response(JSON.stringify({ cloudaicompanionProject: "proj-x" }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
};
await ensureAntigravityProjectAssigned("token-A", mockFetch);
await ensureAntigravityProjectAssigned("token-B", mockFetch);
assert.equal(calledFor.length, 2, "each unique token should trigger one network call");
});
test("does not throw when loadCodeAssist returns non-200", async () => {
const mockFetch = async (_url: string, _init?: RequestInit): Promise<Response> => {
return new Response("Service Unavailable", { status: 503 });
};
// Must resolve without throwing even if all endpoints fail.
await assert.doesNotReject(ensureAntigravityProjectAssigned("fail-token", mockFetch));
});
test("does not throw when fetch rejects (network error)", async () => {
const mockFetch = async (_url: string, _init?: RequestInit): Promise<Response> => {
throw new Error("ECONNREFUSED");
};
await assert.doesNotReject(ensureAntigravityProjectAssigned("throw-token", mockFetch));
});
test("sets Authorization header with Bearer token", async () => {
let capturedAuth: string | null = null;
const mockFetch = async (_url: string, init?: RequestInit): Promise<Response> => {
capturedAuth = new Headers(init?.headers).get("Authorization") ?? null;
return new Response(JSON.stringify({ cloudaicompanionProject: "proj-auth-check" }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
};
await ensureAntigravityProjectAssigned("my-secret-token", mockFetch);
assert.equal(capturedAuth, "Bearer my-secret-token", "Authorization header must be set");
});
test("uses CLI/SDK harness headers when requested", async () => {
let capturedHeaders: Headers | null = null;
const mockFetch = async (_url: string, init?: RequestInit): Promise<Response> => {
capturedHeaders = new Headers(init?.headers);
return new Response(JSON.stringify({ cloudaicompanionProject: "proj-harness" }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
};
await ensureAntigravityProjectAssigned("harness-token", mockFetch, "harness");
assert.match(
capturedHeaders?.get("User-Agent") || "",
/^antigravity\/4\.2\.0 [^ ]+\/[^ ]+ google-api-nodejs-client\/10\.3\.0$/
);
assert.equal(capturedHeaders?.get("X-Goog-Api-Client"), "gl-node/22.21.1");
assert.equal(capturedHeaders?.get("Client-Metadata"), null);
});
test("falls through to next URL when first loadCodeAssist returns 404", async () => {
const hitUrls: string[] = [];
const mockFetch = async (url: string, _init?: RequestInit): Promise<Response> => {
hitUrls.push(url);
// Exact hostname match (not substring .includes) so the check can't be fooled by a
// look-alike host like daily-cloudcode-pa.googleapis.com.evil.com (CodeQL
// js/incomplete-url-substring-sanitization).
if (new URL(url).hostname === "daily-cloudcode-pa.googleapis.com") {
// First URL fails
return new Response("not found", { status: 404 });
}
// Second URL succeeds
return new Response(JSON.stringify({ cloudaicompanionProject: "proj-fallback" }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
};
const projectId = await ensureAntigravityProjectAssigned("fallback-token", mockFetch);
assert.ok(hitUrls.length >= 2, "should try at least two URLs on the first failure");
assert.equal(projectId, "proj-fallback", "should return the project from the successful URL");
assert.equal(
getAntigravityProjectFromCache("fallback-token"),
"proj-fallback",
"should cache the project from the successful URL"
);
});
test("getAntigravityLoadCodeAssistUrls returns URLs matching ANTIGRAVITY_BASE_URLS", () => {
const urls = getAntigravityLoadCodeAssistUrls();
assert.ok(urls.length >= 1, "must return at least one URL");
for (const url of urls) {
assert.ok(url.endsWith(":loadCodeAssist"), `URL must end with :loadCodeAssist, got: ${url}`);
assert.ok(url.startsWith("https://"), `URL must be HTTPS, got: ${url}`);
}
});
});
// ── Ordering guarantee: loadCodeAssist BEFORE :models ─────────────────────────
//
// This test simulates the full discovery flow: a test-controlled fetch
// that records call order, and verifies that :loadCodeAssist precedes
// any :models request. The integration is verified by calling
// ensureAntigravityProjectAssigned then simulating a :models request.
describe("ordering guarantee: loadCodeAssist before :models", () => {
test("loadCodeAssist is called before :models in a simulated discovery flow", async () => {
const callOrder: string[] = [];
const mockFetch = async (url: string, _init?: RequestInit): Promise<Response> => {
if (url.endsWith(":loadCodeAssist")) {
callOrder.push("loadCodeAssist");
return new Response(JSON.stringify({ cloudaicompanionProject: "proj-order-test" }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
if (url.endsWith(":models")) {
callOrder.push("models");
return new Response(
JSON.stringify({
models: [{ id: "gemini-3-pro-antigravity", displayName: "Gemini 3 Pro" }],
}),
{ status: 200, headers: { "Content-Type": "application/json" } }
);
}
return new Response("not found", { status: 404 });
};
// Step 1: bootstrap project (what route.ts now does before the models loop).
await ensureAntigravityProjectAssigned("order-token", mockFetch);
// Step 2: simulate a :models discovery request (what the loop does).
const modelsUrl = "https://cloudcode-pa.googleapis.com/v1internal:models";
await mockFetch(modelsUrl);
const loadIdx = callOrder.indexOf("loadCodeAssist");
const modelsIdx = callOrder.indexOf("models");
assert.ok(loadIdx >= 0, ":loadCodeAssist must be called");
assert.ok(modelsIdx >= 0, ":models must be called");
assert.ok(loadIdx < modelsIdx, ":loadCodeAssist must be called BEFORE :models");
});
});
@@ -0,0 +1,48 @@
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));
});
@@ -0,0 +1,34 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { AntigravityExecutor } from "../../open-sse/executors/antigravity.ts";
type ChatCompletionPayload = {
choices: Array<{
message: { content: string };
finish_reason: string;
}>;
};
test("AntigravityExecutor.collectStreamToResponse normalizes prohibited content finish reasons", async () => {
const executor = new AntigravityExecutor();
const response = new Response(
'data: {"response":{"candidates":[{"content":{"parts":[{"text":"partial text"}]},"finishReason":"PROHIBITED_CONTENT"}]}}\n\n',
{
status: 200,
headers: { "Content-Type": "text/event-stream" },
}
);
const result = await executor.collectStreamToResponse(
response,
"gemini-2.5-flash",
"https://example.com",
{ Authorization: "Bearer ag-token" },
{ request: {} }
);
const payload = (await result.response.json()) as ChatCompletionPayload;
assert.equal(payload.choices[0].message.content, "partial text");
assert.equal(payload.choices[0].finish_reason, "content_filter");
});
@@ -0,0 +1,24 @@
import test from "node:test";
import assert from "node:assert/strict";
import { detectFormatFromEndpoint } from "@omniroute/open-sse/services/provider.ts";
const ENVELOPE = {
model: "gemini-2.5-pro",
project: "projects/test",
request: { contents: [{ role: "user", parts: [{ text: "oi" }] }] },
};
test("detectFormatFromEndpoint classifies the /v1/antigravity path as antigravity", () => {
assert.equal(detectFormatFromEndpoint(ENVELOPE, "/v1/antigravity"), "antigravity");
assert.equal(detectFormatFromEndpoint(ENVELOPE, "/api/v1/antigravity"), "antigravity");
assert.equal(
detectFormatFromEndpoint(ENVELOPE, "/v1/antigravity:streamGenerateContent"),
"antigravity"
);
});
test("the antigravity carve-out does not disturb the other endpoint formats", () => {
assert.equal(detectFormatFromEndpoint({ messages: [] }, "/v1/messages"), "claude");
assert.equal(detectFormatFromEndpoint({ messages: [] }, "/v1/chat/completions"), "openai");
assert.equal(detectFormatFromEndpoint({ input: "x" }, "/v1/responses"), "openai-responses");
});
+9
View File
@@ -0,0 +1,9 @@
import test from "node:test";
import assert from "node:assert/strict";
import { getAntigravityLoadCodeAssistMetadata } from "../../open-sse/services/antigravityHeaders.ts";
test("loadCodeAssist metadata matches Antigravity Manager (ideType only)", () => {
assert.deepEqual(getAntigravityLoadCodeAssistMetadata(), { ideType: "ANTIGRAVITY" });
assert.equal("platform" in getAntigravityLoadCodeAssistMetadata(), false);
assert.equal("pluginType" in getAntigravityLoadCodeAssistMetadata(), false);
});
@@ -0,0 +1,119 @@
/**
* LEDGER-3 (#3821-review) — the Antigravity local-usage fallback (#3604) replaces a
* stale full `fetchAvailableModels` bucket (used=0) with real consumption summed from
* `usage_history`, flipping quotaSource to "localUsageHistory". Every prior #3604 test
* mocks only the HTTP layer, so the model-id match against usage_history.model was never
* exercised end-to-end. This seeds a real usage_history row keyed by the CLIENT tier id
* the fallback queries and asserts the flip — the regression guard for the id contract.
*
* Contract note: the fallback queries `usage_history WHERE model = <client tier id>`
* (e.g. gemini-3.5-flash-high), so the executor MUST log usage under that same client id
* for the fallback to fire. This test pins exactly that join.
*/
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-ag-local-usage-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.API_KEY_SECRET = "test-ag-local-usage-secret";
const core = await import("../../src/lib/db/core.ts");
// Load usage.ts up-front (its index.ts proxyFetch patch runs at module eval) before mocks.
const usageModule = await import("../../open-sse/services/usage.ts");
const { getUsageForProvider } = usageModule;
const originalFetch = globalThis.fetch;
test.after(() => {
globalThis.fetch = originalFetch;
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
test("Antigravity fetchAvailableModels(used=0) → localUsageHistory when usage_history has rows", async () => {
core.resetDbInstance();
// resetTime an hour out → the 5h local-usage window is [now-4h, now+1h).
const resetTime = new Date(Date.now() + 60 * 60 * 1000).toISOString();
const seededTimestamp = new Date(Date.now() - 30 * 60 * 1000).toISOString(); // within window
// Seed a usage_history row keyed by the CLIENT tier id the fallback queries.
const db = core.getDbInstance() as unknown as { prepare: (sql: string) => { run: (...a: unknown[]) => unknown } };
db.prepare(
`INSERT INTO usage_history (provider, model, connection_id, tokens_input, tokens_output, tokens_reasoning, success, timestamp)
VALUES (?, ?, ?, ?, ?, ?, 1, ?)`
).run("antigravity", "gemini-3.5-flash-high", "conn-local-1", 1000, 1500, 500, seededTimestamp);
// Total seeded tokens = 3000 → ceil(3000/1000) = 3 units used.
globalThis.fetch = (async (input: any) => {
const url = typeof input === "string" ? input : input?.url || "";
// retrieveUserQuota (the live signal) is unavailable → falls back to fetchAvailableModels.
if (url.includes("retrieveUserQuota")) {
return { ok: false, status: 404, json: async () => ({}) } as Response;
}
// fetchAvailableModels returns a FULL (stale) bucket: remainingFraction 1.0 + resetTime.
return {
ok: true,
json: async () => ({
models: {
"gemini-3.5-flash-high": {
quotaInfo: { remainingFraction: 1.0, resetTime },
},
},
}),
} as Response;
}) as typeof fetch;
const connection = {
id: "conn-local-1",
provider: "antigravity",
accessToken: "fake-token-local-usage-unique",
providerSpecificData: {},
projectId: undefined,
};
const result = await getUsageForProvider(connection, { forceRefresh: true });
assert.ok(result && "quotas" in result, "should return quotas");
const quota = (result as any).quotas["gemini-3.5-flash-high"];
assert.ok(quota, "should have the gemini-3.5-flash-high quota");
assert.equal(quota.quotaSource, "localUsageHistory", "stale full bucket replaced by local usage");
assert.equal(quota.used, 3, "3000 seeded tokens → 3 units used");
});
test("Antigravity stays fetchAvailableModels when usage_history has no matching rows", async () => {
core.resetDbInstance();
const resetTime = new Date(Date.now() + 60 * 60 * 1000).toISOString();
globalThis.fetch = (async (input: any) => {
const url = typeof input === "string" ? input : input?.url || "";
if (url.includes("retrieveUserQuota")) {
return { ok: false, status: 404, json: async () => ({}) } as Response;
}
return {
ok: true,
json: async () => ({
models: {
"gemini-3.5-flash-high": { quotaInfo: { remainingFraction: 1.0, resetTime } },
},
}),
} as Response;
}) as typeof fetch;
const connection = {
id: "conn-local-2",
provider: "antigravity",
accessToken: "fake-token-local-usage-empty",
providerSpecificData: {},
projectId: undefined,
};
const result = await getUsageForProvider(connection, { forceRefresh: true });
const quota = (result as any).quotas["gemini-3.5-flash-high"];
assert.ok(quota, "should have the quota");
assert.equal(quota.quotaSource, "fetchAvailableModels", "no local rows → keep the catalog view");
assert.equal(quota.used, 0, "full bucket stays at 0 used");
});
@@ -0,0 +1,63 @@
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-antigravity-mitm-"));
process.env.DATA_DIR = TEST_DATA_DIR;
const core = await import("../../src/lib/db/core.ts");
const modelsDb = await import("../../src/lib/db/models.ts");
const { AntigravityExecutor } = await import("../../open-sse/executors/antigravity.ts");
test.beforeEach(() => {
core.resetDbInstance();
});
test.after(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
// #3144: the executor resolves the upstream model through the dynamic MITM alias
// table (populated by model sync) before falling back to the static alias map.
test("transformRequest resolves the upstream model via the dynamic MITM alias table (#3144)", async () => {
await modelsDb.setMitmAliasAll("antigravity", {
"gemini-dynamic": "antigravity/gemini-3.1-pro-low",
});
const executor = new AntigravityExecutor();
const result = await executor.transformRequest(
"antigravity/gemini-dynamic",
{ request: { contents: [] } },
true,
{ projectId: "project-mitm" }
);
if (result instanceof Response) throw new Error("Unexpected Response from transformRequest");
// The MITM alias wins and the "antigravity/" prefix is stripped.
assert.equal(result.model, "gemini-3.1-pro-low");
});
// #3144 regression: a corrupted (non-string) MITM alias value must NOT short-circuit
// cleanModelName into returning undefined — it has to fall through to the static
// resolution and still produce a valid string model (the pre-fix code returned
// undefined here, which then threw on `upstreamModel.toLowerCase()`).
test("transformRequest tolerates a corrupted non-string MITM alias value (#3144)", async () => {
await modelsDb.setMitmAliasAll("antigravity", {
"gemini-3.1-pro": { unexpected: "object" },
});
const executor = new AntigravityExecutor();
const result = await executor.transformRequest(
"antigravity/gemini-3.1-pro",
{ request: { contents: [] } },
true,
{ projectId: "project-mitm" }
);
if (result instanceof Response) throw new Error("Unexpected Response from transformRequest");
assert.equal(typeof result.model, "string");
assert.equal(result.model, "gemini-3.1-pro");
});
@@ -0,0 +1,249 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
ANTIGRAVITY_PUBLIC_MODELS,
getClientVisibleAntigravityModelName,
isUserCallableAntigravityModelId,
resolveAntigravityModelId,
toClientAntigravityModelId,
toClientAntigravityQuotaModelId,
} from "../../open-sse/config/antigravityModelAliases.ts";
import { AntigravityExecutor } from "../../open-sse/executors/antigravity.ts";
import { openaiToAntigravityRequest } from "../../open-sse/translator/request/openai-to-gemini.ts";
function getPublicModel(id: string) {
return ANTIGRAVITY_PUBLIC_MODELS.find((model) => model.id === id) as any;
}
// #3821-review LEDGER-5 — the upstream quota-bucket → client-tier remap is now the single
// source of truth here (was duplicated as an inline if-ladder in usage.ts). It operates on
// the UPSTREAM quota namespace, where `gemini-3.5-flash-low` is the Medium tier's bucket.
test("toClientAntigravityQuotaModelId maps upstream quota buckets to client tiers", () => {
assert.equal(
toClientAntigravityQuotaModelId("gemini-3.5-flash-extra-low"),
"gemini-3.5-flash-low"
);
// Dual-meaning id: in the quota namespace this bucket is the Medium tier.
assert.equal(toClientAntigravityQuotaModelId("gemini-3.5-flash-low"), "gemini-3.5-flash-medium");
assert.equal(toClientAntigravityQuotaModelId("gemini-3-flash-agent"), "gemini-3.5-flash-high");
// Non-tier ids fall back to the standard reverse alias map.
assert.equal(toClientAntigravityQuotaModelId("gemini-3.1-pro"), "gemini-3-pro-preview");
// Always-allowed bucket passes through unchanged.
assert.equal(toClientAntigravityQuotaModelId("credits"), "credits");
// Retired preview buckets are dropped (hidden from clients).
assert.equal(toClientAntigravityQuotaModelId("gemini-3.5-flash-preview"), null);
assert.equal(toClientAntigravityQuotaModelId("gemini-3-flash-preview"), null);
assert.equal(toClientAntigravityQuotaModelId(""), null);
});
test("resolveAntigravityModelId maps the documented Antigravity aliases to upstream IDs", () => {
assert.equal(resolveAntigravityModelId("gemini-3-pro-preview"), "gemini-3.1-pro");
assert.equal(resolveAntigravityModelId("gemini-3-pro-image-preview"), "gemini-3-pro-image");
assert.equal(
resolveAntigravityModelId("gemini-2.5-computer-use-preview-10-2025"),
"rev19-uic3-1p"
);
assert.equal(resolveAntigravityModelId("gemini-3.5-flash-low"), "gemini-3.5-flash-extra-low");
assert.equal(resolveAntigravityModelId("gemini-3.5-flash-medium"), "gemini-3.5-flash-low");
assert.equal(resolveAntigravityModelId("gemini-3.5-flash-high"), "gemini-3-flash-agent");
// Backward-compat: retired flagship public id routes to the High tier upstream.
assert.equal(resolveAntigravityModelId("gemini-3.5-flash-preview"), "gemini-3-flash-agent");
assert.equal(resolveAntigravityModelId("gemini-claude-sonnet-4-5"), "claude-sonnet-4-6");
assert.equal(resolveAntigravityModelId("gemini-claude-sonnet-4-5-thinking"), "claude-sonnet-4-6");
assert.equal(
resolveAntigravityModelId("gemini-claude-opus-4-5-thinking"),
"claude-opus-4-6-thinking"
);
assert.equal(resolveAntigravityModelId("unknown-model"), "unknown-model");
});
test("toClientAntigravityModelId exposes client-visible aliases for known upstream IDs", () => {
assert.equal(toClientAntigravityModelId("gemini-3.1-pro"), "gemini-3-pro-preview");
assert.equal(toClientAntigravityModelId("gemini-3.5-flash-extra-low"), "gemini-3.5-flash-low");
assert.equal(toClientAntigravityModelId("gemini-3-flash-agent"), "gemini-3.5-flash-high");
assert.equal(toClientAntigravityModelId("gpt-oss-120b-medium"), "gpt-oss-120b-medium");
assert.equal(toClientAntigravityModelId("claude-sonnet-4-6"), "claude-sonnet-4-6");
assert.equal(toClientAntigravityModelId("claude-opus-4-6-thinking"), "claude-opus-4-6-thinking");
});
test("isUserCallableAntigravityModelId only allows public chat-capable model IDs", () => {
assert.equal(isUserCallableAntigravityModelId("gemini-3-pro-preview"), true);
assert.equal(isUserCallableAntigravityModelId("gemini-3.1-pro"), true);
// Retired flagship id stays callable as a hidden backward-compat alias (routes to High),
// even though it is no longer exposed in the public catalog.
assert.equal(isUserCallableAntigravityModelId("gemini-3.5-flash-preview"), true);
assert.equal(isUserCallableAntigravityModelId("gemini-3-flash-agent"), true);
assert.equal(isUserCallableAntigravityModelId("gemini-3.1-flash-lite"), true);
assert.equal(isUserCallableAntigravityModelId("gemini-2.5-pro"), true);
assert.equal(isUserCallableAntigravityModelId("gemini-2.5-flash"), true);
assert.equal(isUserCallableAntigravityModelId("gemini-2.5-flash-lite"), true);
assert.equal(isUserCallableAntigravityModelId("gemini-2.5-flash-thinking"), true);
assert.equal(isUserCallableAntigravityModelId("gemini-pro-agent"), true);
// #3184: Claude IS user-callable through the Antigravity OAuth provider (same backend as
// `agy`, verified empirically). An earlier assumption that it was removed in Antigravity
// 2.0 was wrong.
assert.equal(isUserCallableAntigravityModelId("claude-opus-4-6-thinking"), true);
assert.equal(isUserCallableAntigravityModelId("claude-sonnet-4-6"), true);
assert.equal(isUserCallableAntigravityModelId("claude-sonnet-5"), true);
// Antigravity 2.0.4 exposes Gemini 3.5 Flash as separate UI tiers.
assert.equal(isUserCallableAntigravityModelId("gemini-3.1-pro-high"), true);
assert.equal(isUserCallableAntigravityModelId("gemini-3.1-pro-low"), true);
assert.equal(isUserCallableAntigravityModelId("gemini-3.5-flash-low"), true);
assert.equal(isUserCallableAntigravityModelId("gemini-3.5-flash-medium"), true);
assert.equal(isUserCallableAntigravityModelId("gemini-3.5-flash-high"), true);
assert.equal(isUserCallableAntigravityModelId("gemini-3.5-flash-extra-low"), true);
assert.equal(isUserCallableAntigravityModelId("tab_flash_lite_preview"), false);
assert.equal(isUserCallableAntigravityModelId("unknown-model"), false);
});
test("ANTIGRAVITY_PUBLIC_MODELS exposes captured Antigravity 2.0.1 names and capabilities", () => {
// #3184: Claude is exposed in the antigravity catalog (same backend as `agy`, verified).
assert.deepEqual(getPublicModel("claude-opus-4-6-thinking"), {
id: "claude-opus-4-6-thinking",
name: "Claude Opus 4.6 (Thinking)",
contextLength: 200000,
maxOutputTokens: 65536,
supportsReasoning: true,
supportsVision: true,
toolCalling: true,
});
assert.equal(getPublicModel("claude-sonnet-4-6").name, "Claude Sonnet 4.6 (Thinking)");
// claude-sonnet-5 was added to the Antigravity catalog alongside the existing Claude entries.
assert.deepEqual(getPublicModel("claude-sonnet-5"), {
id: "claude-sonnet-5",
name: "Claude Sonnet 5 (Thinking)",
contextLength: 200000,
maxOutputTokens: 65536,
supportsReasoning: true,
supportsVision: true,
toolCalling: true,
});
assert.deepEqual(getPublicModel("gemini-3.5-flash-high"), {
id: "gemini-3.5-flash-high",
name: "Gemini 3.5 Flash (High)",
contextLength: 1048576,
maxOutputTokens: 65536,
supportsReasoning: true,
supportsVision: true,
toolCalling: true,
});
assert.equal(
getClientVisibleAntigravityModelName("gemini-3.5-flash-medium"),
"Gemini 3.5 Flash (Medium)"
);
assert.equal(getClientVisibleAntigravityModelName("gemini-2.5-flash"), "Gemini 2.5 Flash");
assert.equal(
getClientVisibleAntigravityModelName("gemini-2.5-flash-lite"),
"Gemini 2.5 Flash Lite"
);
assert.equal(
getClientVisibleAntigravityModelName("gemini-2.5-flash-thinking"),
"Gemini 2.5 Flash Thinking"
);
assert.deepEqual(getPublicModel("gpt-oss-120b-medium"), {
id: "gpt-oss-120b-medium",
name: "GPT-OSS 120B (Medium)",
contextLength: 131072,
maxOutputTokens: 32768,
supportsReasoning: true,
toolCalling: true,
});
assert.equal(getPublicModel("gemini-3-pro-image-preview").contextLength, undefined);
assert.equal(
getPublicModel("gemini-2.5-computer-use-preview-10-2025").maxOutputTokens,
undefined
);
});
test("ANTIGRAVITY_PUBLIC_MODELS has no duplicate model IDs", () => {
const ids = ANTIGRAVITY_PUBLIC_MODELS.map((model) => model.id);
const seen = new Set<string>();
const duplicates = ids.filter((id) => {
if (seen.has(id)) return true;
seen.add(id);
return false;
});
assert.deepEqual(duplicates, [], `duplicate model IDs found: ${duplicates.join(", ")}`);
});
test("AntigravityExecutor.transformRequest resolves alias models before dispatching upstream", async () => {
const executor = new AntigravityExecutor();
const result = await executor.transformRequest(
"antigravity/gemini-3-pro-preview",
{
request: {
contents: [{ role: "user", parts: [{ text: "Hello" }] }],
},
},
true,
{ projectId: "project-1" }
);
if (result instanceof Response) throw new Error("Unexpected Response from transformRequest");
assert.equal(result.model, "gemini-3.1-pro");
});
test("AntigravityExecutor.transformRequest maps Gemini 3.5 Flash tiers to live upstream IDs", async () => {
const executor = new AntigravityExecutor();
const result = await executor.transformRequest(
"antigravity/gemini-3.5-flash-high",
{
request: {
contents: [{ role: "user", parts: [{ text: "Hello" }] }],
},
},
true,
{ projectId: "project-1" }
);
if (result instanceof Response) throw new Error("Unexpected Response from transformRequest");
// The "High" tier resolves to the live upstream id; the request body is forwarded
// under that id. (Dropped four assertions on modelConfigId/model_config_id — the
// executor never sets those fields, so they were vacuously true and gave false
// confidence. #3821-review LEDGER-10.)
assert.equal(result.model, "gemini-3-flash-agent");
assert.deepEqual(result.request.contents, [{ role: "user", parts: [{ text: "Hello" }] }]);
});
test("AntigravityExecutor.transformRequest sends Claude through Gemini-compatible Cloud Code schema", async () => {
const executor = new AntigravityExecutor();
const bridged = openaiToAntigravityRequest(
"claude-opus-4-6-thinking",
{
messages: [{ role: "user", content: "Hello" }],
max_completion_tokens: 32_000,
temperature: 0.5,
reasoning_effort: "high",
},
true,
{ projectId: "project-1" } as any
);
const result = await executor.transformRequest(
"antigravity/claude-opus-4-6-thinking",
bridged,
true,
{
projectId: "project-1",
}
);
if (result instanceof Response) throw new Error("Unexpected Response from transformRequest");
const request = result.request as any;
assert.deepEqual(request.contents, [{ role: "user", parts: [{ text: "Hello" }] }]);
// Capped to MAX_ANTIGRAVITY_OUTPUT_TOKENS (16384) by the executor (#4636) to avoid
// the Antigravity Cloud Code 400 on maxOutputTokens > 16384, overriding the
// thinkingBudget+1 bump (which would otherwise be 32769).
assert.equal(request.generationConfig.maxOutputTokens, 16384);
assert.equal(request.generationConfig.temperature, 0.5);
assert.equal(request.generationConfig.topK, 40);
assert.equal(request.generationConfig.topP, 1);
assert.equal(request.messages, undefined);
assert.equal(request.system, undefined);
assert.equal(request.max_tokens, undefined);
assert.equal(request.stream, undefined);
assert.equal(request.temperature, undefined);
assert.equal(request.thinking, undefined);
assert.equal(request.generationConfig.thinkingConfig, undefined);
});
@@ -0,0 +1,83 @@
// Regression guard for the Antigravity OAuth login hang on Google's consent page.
//
// The embedded Antigravity client is a Google "Desktop/native" OAuth client.
// Sending a PKCE code_challenge AND the `openid` scope pushed Google into the
// `signin/oauth/firstparty/nativeapp` consent flow, which hung and never redirected
// back (operator report 2026-06-27). The working 9router flow uses a plain
// authorization_code grant (client_secret, no code_challenge) and does NOT request
// `openid`. This test pins our antigravity (and the `agy` alias) to that shape.
//
// Flip-proof: set flowType back to "authorization_code_pkce" → generateAuthData emits
// code_challenge → first assertion fails. Re-add "openid" → scope assertion fails.
import test from "node:test";
import assert from "node:assert/strict";
import { generateAuthData } from "../../src/lib/oauth/providers.ts";
import PROVIDERS from "../../src/lib/oauth/providers/index.ts";
const REDIRECT = "http://127.0.0.1:20128/callback";
for (const providerId of ["antigravity", "agy"]) {
test(`${providerId}: no PKCE + no openid in the auth URL (matches working 9router flow)`, () => {
assert.equal(
PROVIDERS[providerId].flowType,
"authorization_code",
`${providerId} must use a plain authorization_code grant (no PKCE) for the Google native client`
);
const authData = generateAuthData(providerId, REDIRECT);
assert.ok(authData.authUrl, `${providerId} must produce an auth URL`);
const url = new URL(authData.authUrl);
assert.equal(url.origin, "https://accounts.google.com");
// No PKCE challenge — its presence triggers the hanging nativeapp consent.
assert.equal(
url.searchParams.get("code_challenge"),
null,
`${providerId} auth URL must NOT carry a PKCE code_challenge`
);
assert.equal(url.searchParams.get("code_challenge_method"), null);
// No openid scope — only the Cloud Code / userinfo scopes 9router requests.
const scopes = (url.searchParams.get("scope") || "").split(" ");
assert.ok(!scopes.includes("openid"), `${providerId} must not request the openid scope`);
assert.ok(
scopes.includes("https://www.googleapis.com/auth/cloud-platform"),
`${providerId} must still request the cloud-platform scope`
);
});
}
test("antigravity.exchangeToken never forwards code_verifier (no PKCE → no invalid_grant 500)", async () => {
const origFetch = globalThis.fetch;
let sentBody = "";
globalThis.fetch = (async (_url: unknown, init: { body?: unknown } = {}) => {
sentBody = String(init.body ?? "");
return new Response(
JSON.stringify({ access_token: "t", refresh_token: "r", expires_in: 3600 }),
{ status: 200, headers: { "Content-Type": "application/json" } }
);
}) as typeof fetch;
try {
// Pass a codeVerifier (as the modal does — generateAuthData always mints one).
// It MUST be ignored: the authorize URL had no code_challenge, so forwarding a
// code_verifier makes Google reject the exchange (invalid_grant → 500).
await PROVIDERS.antigravity.exchangeToken(
{
clientId: "cid",
clientSecret: "sec",
tokenUrl: "https://oauth2.googleapis.com/token",
},
"the-code",
"http://127.0.0.1:20128/callback",
"should-be-ignored-verifier"
);
} finally {
globalThis.fetch = origFetch;
}
const params = new URLSearchParams(sentBody);
assert.equal(params.get("code_verifier"), null, "must NOT forward code_verifier (no PKCE)");
assert.equal(params.get("client_secret"), "sec", "must authenticate via client_secret");
assert.equal(params.get("grant_type"), "authorization_code");
});
@@ -0,0 +1,104 @@
// Regression guard for the Antigravity OAuth login hang.
//
// The dashboard login "just spun forever" because postExchange `await`ed the
// onboardUser retry loop (up to 10×5s, each fetch un-timed) inline, so a slow/
// unreachable Antigravity upstream blocked the /exchange response indefinitely.
//
// Fix: onboarding is fire-and-forget (matches the 9router web flow) and every
// blocking call is AbortSignal.timeout-bounded. This test proves postExchange
// returns promptly regardless of onboarding, and never hangs when an upstream
// stalls.
//
// Flip-proof: revert onboarding to an inline `await` loop and test 1 hangs on the
// onboard gate → times out → fails. Drop the AbortSignal.timeout and test 2
// hangs → fails.
import test from "node:test";
import assert from "node:assert/strict";
import { antigravity } from "../../src/lib/oauth/providers/antigravity.ts";
const originalFetch = globalThis.fetch;
function jsonRes(body: unknown, status = 200): Response {
return new Response(JSON.stringify(body), {
status,
headers: { "Content-Type": "application/json" },
});
}
// A fetch that rejects when its AbortSignal fires, and otherwise never resolves.
// Mirrors real fetch: an already-aborted signal rejects immediately (so a shared
// deadline reused across fallback endpoints fails fast after the first abort).
function stalledFetch(init?: { signal?: AbortSignal }): Promise<Response> {
return new Promise((_resolve, reject) => {
const abortErr = () => new DOMException("The operation was aborted.", "AbortError");
const signal = init?.signal;
if (signal?.aborted) {
reject(abortErr());
return;
}
signal?.addEventListener("abort", () => reject(abortErr()));
});
}
test.afterEach(() => {
globalThis.fetch = originalFetch;
});
test("postExchange returns before onboarding finishes (fire-and-forget — never blocks login)", async () => {
// The onboard call is gated: it does not resolve until we release it AFTER
// postExchange has already returned. With the old inline `await` loop,
// postExchange would block on this gate forever → the test times out. With the
// fire-and-forget fix it returns immediately.
let releaseOnboard: () => void = () => {};
const onboardGate = new Promise<void>((r) => {
releaseOnboard = r;
});
let onboardStarted = false;
globalThis.fetch = (async (url: unknown) => {
const u = String(url);
if (u.includes("userinfo")) return jsonRes({ email: "user@example.com" });
if (u.includes("loadCodeAssist")) {
return jsonRes({
cloudaicompanionProject: "proj-123",
allowedTiers: [{ id: "legacy-tier", isDefault: true }],
});
}
if (u.includes("onboardUser")) {
onboardStarted = true;
await onboardGate;
return jsonRes({ done: true });
}
return jsonRes({});
}) as typeof fetch;
const start = Date.now();
const result = await antigravity.postExchange({ access_token: "tok" } as never);
const elapsed = Date.now() - start;
assert.ok(elapsed < 3000, `postExchange must not block on onboarding; took ${elapsed}ms`);
assert.equal(result.projectId, "proj-123", "projectId still resolved from loadCodeAssist");
// Let the backgrounded onboarding complete cleanly (no lingering work).
releaseOnboard();
await new Promise((r) => setTimeout(r, 50));
assert.ok(onboardStarted, "onboarding still runs — in the background, after the response");
});
test("postExchange stays timeout-bounded when loadCodeAssist/userinfo stall (no infinite hang)", async () => {
globalThis.fetch = (async (url: unknown, init?: { signal?: AbortSignal }) => {
const u = String(url);
if (u.includes("userinfo") || u.includes("loadCodeAssist")) return stalledFetch(init);
return jsonRes({});
}) as typeof fetch;
const start = Date.now();
const result = await antigravity.postExchange({ access_token: "tok" } as never);
const elapsed = Date.now() - start;
// userInfo + loadCodeAssist are AbortSignal.timeout(8s)-bounded (one shared
// deadline each), so the worst case is ~16s — never an infinite hang.
assert.ok(elapsed < 22000, `postExchange must be timeout-bounded; took ${elapsed}ms`);
assert.equal(result.projectId, "", "no project when loadCodeAssist times out");
});
@@ -0,0 +1,127 @@
/**
* 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");
});
+26
View File
@@ -0,0 +1,26 @@
import { describe, it } from "node:test";
import assert from "node:assert";
import { updateProviderConnectionSchema } from "../../src/shared/validation/schemas.js";
describe("Antigravity Project ID Schema Validation", () => {
it("should accept projectId and providerSpecificData.projectId", () => {
const result = updateProviderConnectionSchema.safeParse({
projectId: "anti-project",
providerSpecificData: { projectId: "anti-project" },
});
assert.strictEqual(result.success, true);
});
it("should accept null projectId and preserve nested null through JSON serialization", () => {
const payload = {
projectId: null,
providerSpecificData: { projectId: null },
};
const serialized = JSON.stringify(payload);
assert.match(serialized, /"projectId":null/);
const result = updateProviderConnectionSchema.safeParse(payload);
assert.strictEqual(result.success, true);
});
});
@@ -0,0 +1,75 @@
/**
* Issue #3850 — Antigravity refresh nulls the stored refresh_token.
*
* Google's OAuth token endpoint normally OMITS `refresh_token` on a refresh
* (its refresh tokens are non-rotating), and occasionally returns it as an
* EMPTY STRING. The canonical `refreshGoogleToken` preserves the existing token
* via `tokens.refresh_token || refreshToken` (treats "" as absent), but the
* Antigravity executor's `refreshCredentials` used
* `typeof tokens.refresh_token === "string" ? tokens.refresh_token : credentials.refreshToken`
* — and `typeof "" === "string"` is true, so an empty-string response
* OVERWROTE the good token with "", effectively nulling it on first refresh.
*
* This regression guard asserts the executor preserves the existing refresh
* token when the upstream returns it empty or omits it.
*/
import { test } from "node:test";
import assert from "node:assert/strict";
import { AntigravityExecutor } from "../../open-sse/executors/antigravity.ts";
const OLD_REFRESH = "1//old-non-rotating-refresh-token";
async function withStubbedFetch<T>(
jsonBody: Record<string, unknown>,
fn: () => Promise<T>
): Promise<T> {
const original = globalThis.fetch;
globalThis.fetch = (async () =>
new Response(JSON.stringify(jsonBody), {
status: 200,
headers: { "Content-Type": "application/json" },
})) as typeof fetch;
try {
return await fn();
} finally {
globalThis.fetch = original;
}
}
test("#3850 empty-string refresh_token from Google preserves the existing token", async () => {
const executor = new AntigravityExecutor();
const refreshed = await withStubbedFetch(
{ access_token: "new-access", refresh_token: "", expires_in: 3600 },
() => executor.refreshCredentials({ refreshToken: OLD_REFRESH, accessToken: "stale" })
);
assert.ok(refreshed, "refreshCredentials should return credentials, not null");
assert.equal(refreshed.accessToken, "new-access");
assert.equal(
refreshed.refreshToken,
OLD_REFRESH,
"empty-string refresh_token must NOT overwrite the stored token"
);
});
test("#3850 omitted refresh_token from Google preserves the existing token", async () => {
const executor = new AntigravityExecutor();
const refreshed = await withStubbedFetch({ access_token: "new-access", expires_in: 3600 }, () =>
executor.refreshCredentials({ refreshToken: OLD_REFRESH, accessToken: "stale" })
);
assert.ok(refreshed);
assert.equal(refreshed.refreshToken, OLD_REFRESH);
});
test("#3850 a real rotated refresh_token still replaces the stored token", async () => {
const executor = new AntigravityExecutor();
const refreshed = await withStubbedFetch(
{ access_token: "new-access", refresh_token: "1//brand-new-token", expires_in: 3600 },
() => executor.refreshCredentials({ refreshToken: OLD_REFRESH, accessToken: "stale" })
);
assert.ok(refreshed);
assert.equal(refreshed.refreshToken, "1//brand-new-token");
});
+141
View File
@@ -0,0 +1,141 @@
/**
* TDD tests for port of upstream PR #2054 (decolua/9router):
* fix(antigravity): retry transient upstream failures
*
* Covers:
* 1. isTransientAntigravityError classifies 5xx + body patterns correctly
* 2. extractErrorMessage extracts message from various JSON shapes
* 3. Tool-name deduplication in buildGeminiTools (via geminiToolsSanitizer)
*/
import { test } from "node:test";
import assert from "node:assert/strict";
import { AntigravityExecutor } from "../../open-sse/executors/antigravity.ts";
import { buildGeminiTools } from "../../open-sse/translator/helpers/geminiToolsSanitizer.ts";
// ── isTransientAntigravityError ────────────────────────────────────────────────
test("isTransientAntigravityError: 503 → true (transient status)", () => {
const ex = new AntigravityExecutor();
assert.equal(ex.isTransientAntigravityError(503, ""), true);
});
test("isTransientAntigravityError: 500 + 'Agent execution terminated due to error' body → true", () => {
const ex = new AntigravityExecutor();
assert.equal(
ex.isTransientAntigravityError(500, "Agent execution terminated due to error"),
true
);
});
test("isTransientAntigravityError: 500 + 'high traffic' body → true", () => {
const ex = new AntigravityExecutor();
assert.equal(ex.isTransientAntigravityError(500, "high traffic"), true);
});
test("isTransientAntigravityError: 500 + 'capacity' body → true", () => {
const ex = new AntigravityExecutor();
assert.equal(ex.isTransientAntigravityError(500, "service is at capacity"), true);
});
test("isTransientAntigravityError: 400 + 'Invalid request' body → false (veto)", () => {
const ex = new AntigravityExecutor();
assert.equal(ex.isTransientAntigravityError(400, "Invalid request"), false);
});
test("isTransientAntigravityError: 404 → false", () => {
const ex = new AntigravityExecutor();
assert.equal(ex.isTransientAntigravityError(404, "not found"), false);
});
test("isTransientAntigravityError: 429 → true (rate limited is transient)", () => {
const ex = new AntigravityExecutor();
assert.equal(ex.isTransientAntigravityError(429, ""), true);
});
test("isTransientAntigravityError: 502 → true (bad gateway is transient)", () => {
const ex = new AntigravityExecutor();
assert.equal(ex.isTransientAntigravityError(502, ""), true);
});
test("isTransientAntigravityError: 504 → true (gateway timeout is transient)", () => {
const ex = new AntigravityExecutor();
assert.equal(ex.isTransientAntigravityError(504, ""), true);
});
// ── extractErrorMessage ────────────────────────────────────────────────────────
test("extractErrorMessage: extracts error.message from standard JSON shape", () => {
const ex = new AntigravityExecutor();
const json = { error: { message: "quota exceeded" } };
const msg = ex.extractErrorMessage(json, "");
assert.ok(msg.includes("quota exceeded"), `expected 'quota exceeded' in: ${msg}`);
});
test("extractErrorMessage: falls back to top-level message field", () => {
const ex = new AntigravityExecutor();
const json = { message: "service unavailable" };
const msg = ex.extractErrorMessage(json, "");
assert.ok(msg.includes("service unavailable"), `expected 'service unavailable' in: ${msg}`);
});
test("extractErrorMessage: falls back to bodyText when json has no message", () => {
const ex = new AntigravityExecutor();
const msg = ex.extractErrorMessage(null, "raw error body text");
assert.ok(msg.includes("raw error body text"), `expected body text in: ${msg}`);
});
// ── Tool-name deduplication ────────────────────────────────────────────────────
test("buildGeminiTools: deduplicates tool names that sanitize to the same value", () => {
// "read/file" and "read/file" appear twice → only one after dedup
const tools = [
{
functionDeclarations: [
{
name: "read/file",
description: "Read a file",
parameters: { type: "object", properties: {} },
},
{
name: "read/file",
description: "Read a file (dup)",
parameters: { type: "object", properties: {} },
},
],
},
];
const result = buildGeminiTools(tools);
assert.ok(Array.isArray(result), "should return array");
const decls = result![0]?.functionDeclarations ?? [];
// After sanitizing "read/file" → "read_file", both resolve to same name; only 1 should remain
const names = decls.map((d) => d.name);
assert.equal(new Set(names).size, names.length, `Duplicate names found: ${names}`);
assert.equal(names.length, 1, `Expected 1 declaration, got ${names.length}: ${names}`);
});
test("buildGeminiTools: tools with different sanitized names are both kept", () => {
const tools = [
{
functionDeclarations: [
{
name: "read_file",
description: "Read a file",
parameters: { type: "object", properties: {} },
},
{
name: "write_file",
description: "Write a file",
parameters: { type: "object", properties: {} },
},
],
},
];
const result = buildGeminiTools(tools);
assert.ok(Array.isArray(result), "should return array");
const decls = result![0]?.functionDeclarations ?? [];
assert.equal(decls.length, 2, `Expected 2 declarations, got ${decls.length}`);
});
@@ -0,0 +1,60 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { AntigravityExecutor } from "../../open-sse/executors/antigravity.ts";
import { DEFAULT_SAFETY_SETTINGS } from "../../open-sse/translator/helpers/geminiHelper.ts";
// Regression for #5003: the Antigravity (Google Cloud Code) request builder explicitly set
// `safetySettings: undefined`, which `JSON.stringify` drops entirely. With no safetySettings
// reaching Cloud Code, Google applies its server-side safety defaults that false-flag benign
// technical prompts as `prohibited_content` (HTTP 200 with a blocked body that combo failover
// treats as terminal). The native Gemini paths all default to all-OFF
// (DEFAULT_SAFETY_SETTINGS); Antigravity must match for parity.
test("transformRequest defaults safetySettings to all-OFF when none supplied (#5003)", async () => {
const executor = new AntigravityExecutor();
const body = {
request: {
contents: [{ role: "user", parts: [{ text: "hi" }] }],
generationConfig: {},
},
};
const result = await executor.transformRequest("antigravity/claude-sonnet-4-6", body, true, {
projectId: "project-1",
});
if (result instanceof Response) throw new Error("Unexpected Response from transformRequest");
const innerRequest = result.request as Record<string, unknown>;
assert.deepEqual(
innerRequest.safetySettings,
DEFAULT_SAFETY_SETTINGS,
"safetySettings must default to all-OFF for parity with native Gemini paths"
);
});
test("transformRequest honors a caller-supplied safetySettings (#5003)", async () => {
const executor = new AntigravityExecutor();
const callerSafety = [
{ category: "HARM_CATEGORY_HATE_SPEECH", threshold: "BLOCK_MEDIUM_AND_ABOVE" },
];
const body = {
request: {
contents: [{ role: "user", parts: [{ text: "hi" }] }],
generationConfig: {},
safetySettings: callerSafety,
},
};
const result = await executor.transformRequest("antigravity/claude-sonnet-4-6", body, true, {
projectId: "project-1",
});
if (result instanceof Response) throw new Error("Unexpected Response from transformRequest");
const innerRequest = result.request as Record<string, unknown>;
assert.deepEqual(
innerRequest.safetySettings,
callerSafety,
"a caller-supplied safetySettings must not be clobbered"
);
});
@@ -0,0 +1,63 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { AntigravityExecutor } from "../../open-sse/executors/antigravity.ts";
// Regression guard for #4309 (thanks @Ardem2025 / Dmitry Kuznetsov): when the upstream
// SSE collection aborts or errors, AntigravityExecutor.collectStreamToResponse must
// release the stream reader AND cancel the response body — otherwise the underlying
// socket stays checked out of the Undici agent pool (a slow socket/memory leak under
// abort-heavy load). Previously the catch block just fell through ("return whatever was
// collected"), leaving the body uncancelled and the reader locked.
//
// We drive the error path deterministically with a pre-aborted signal (the first loop
// iteration throws "Request aborted during SSE collection" before any read), then assert
// the reader was released and the body cancelled exactly as the fix does.
test("collectStreamToResponse releases the reader and cancels the body on abort (Undici socket leak #4309)", async () => {
const executor = new AntigravityExecutor();
let releaseLockCalls = 0;
let cancelCalls = 0;
const fakeReader = {
// Never resolves — with a pre-aborted signal the loop throws before ever reading,
// so read() must not be needed to reach the cleanup path.
read: () => new Promise<never>(() => {}),
releaseLock: () => {
releaseLockCalls += 1;
},
};
const fakeBody = {
getReader: () => fakeReader,
cancel: () => {
cancelCalls += 1;
return Promise.resolve();
},
};
const response = { body: fakeBody } as unknown as Response;
const controller = new AbortController();
controller.abort(); // pre-aborted → the collect loop throws on its first guard check
await executor.collectStreamToResponse(
response,
"antigravity-model",
"https://example.invalid/sse",
{},
{},
null,
controller.signal
);
assert.ok(
releaseLockCalls >= 1,
"reader.releaseLock() must be called on the abort/error path (was never released before the fix)"
);
assert.equal(
cancelCalls,
1,
"response.body.cancel() must be called exactly once to return the socket to the Undici pool"
);
});
@@ -0,0 +1,102 @@
import test from "node:test";
import assert from "node:assert/strict";
import { openaiToAntigravityRequest } from "../../open-sse/translator/request/openai-to-gemini.ts";
// Regression guard for the Antigravity v1internal 400 that fires when a Gemini
// built-in tool (google_search / web_search / googleSearch ...) is sent in the
// same request as functionDeclarations. The Cloud Code envelope must strip the
// built-in tool names out of functionDeclarations before dispatch.
// Inspired-by decolua/9router#1095 (author @vanszs).
type EnvelopeTool = { functionDeclarations?: Array<{ name: string }> };
function declaredToolNames(result: { request: { tools?: EnvelopeTool[] } }): string[] {
return (result.request.tools ?? []).flatMap((tool) =>
(tool.functionDeclarations ?? []).map((fn) => fn.name)
);
}
const CREDS = { projectId: "proj-1" } as never;
test("Antigravity envelope strips built-in tool names mixed with custom functionDeclarations (#1095)", () => {
const result = openaiToAntigravityRequest(
"gemini-2.5-pro",
{
messages: [{ role: "user", content: "Search and read" }],
tools: [
{
type: "function",
function: { name: "google_search", parameters: { type: "object", properties: {} } },
},
{
type: "function",
function: { name: "read_file", parameters: { type: "object", properties: {} } },
},
],
},
false,
CREDS
);
const names = declaredToolNames(result);
// Built-in tool name must be gone from functionDeclarations...
assert.ok(
!names.includes("google_search") && !names.includes("googleSearch"),
`expected built-in tool name stripped, got ${JSON.stringify(names)}`
);
// ...while the custom declaration survives.
assert.ok(names.includes("read_file"), `expected custom tool kept, got ${JSON.stringify(names)}`);
// toolConfig stays set because custom declarations remain.
assert.deepEqual(result.request.toolConfig, {
functionCallingConfig: { mode: "VALIDATED" },
});
});
test("Antigravity envelope keeps a normal custom-only tool request intact (#1095 regression)", () => {
const result = openaiToAntigravityRequest(
"gemini-2.5-pro",
{
messages: [{ role: "user", content: "Hello" }],
tools: [
{
type: "function",
function: { name: "weather", parameters: { type: "object", properties: {} } },
},
],
},
false,
CREDS
);
const names = declaredToolNames(result);
assert.deepEqual(names, ["weather"]);
assert.deepEqual(result.request.toolConfig, {
functionCallingConfig: { mode: "VALIDATED" },
});
});
test("Antigravity envelope drops tools + toolConfig when only built-in tools are present (#1095)", () => {
const result = openaiToAntigravityRequest(
"gemini-2.5-pro",
{
messages: [{ role: "user", content: "Search the web" }],
tools: [
{
type: "function",
function: { name: "google_search", parameters: { type: "object", properties: {} } },
},
],
},
false,
CREDS
);
const names = declaredToolNames(result);
assert.ok(
!names.includes("google_search") && !names.includes("googleSearch"),
`expected no built-in functionDeclarations, got ${JSON.stringify(names)}`
);
// No custom declarations remain -> no VALIDATED toolConfig is forced.
assert.equal(result.request.toolConfig, undefined);
});
@@ -0,0 +1,45 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { AntigravityExecutor } from "../../open-sse/executors/antigravity.ts";
// Regression for #1944: Claude models served via Antigravity (Google Cloud Code) started
// returning `400 Invalid JSON payload received. Unknown name "output_config"` (path "/").
// `output_config` is an Anthropic/Claude-Code-only field; Google's Cloud Code envelope
// rejects unknown top-level fields. The Antigravity envelope spreads `...passthroughFields`
// (every top-level body field except a known few), so a top-level `output_config` (or the
// legacy `output_format`) leaked straight into the envelope Google validates.
test("transformRequest drops a top-level output_config from the Antigravity envelope (#1944)", async () => {
const executor = new AntigravityExecutor();
const body = {
output_config: { effort: "high", format: { type: "json_schema" } },
output_format: "json",
request: {
contents: [{ role: "user", parts: [{ text: "hi" }] }],
generationConfig: {},
},
};
const result = await executor.transformRequest("antigravity/claude-sonnet-4-6", body, true, {
projectId: "project-1",
});
if (result instanceof Response) throw new Error("Unexpected Response from transformRequest");
assert.equal(
(result as Record<string, unknown>).output_config,
undefined,
"output_config must not reach the Google Cloud Code envelope"
);
assert.equal(
(result as Record<string, unknown>).output_format,
undefined,
"legacy output_format must not reach the envelope either"
);
// The sanitized inner request (Claude path) must also be free of output_config.
assert.equal(
(result.request as Record<string, unknown>).output_config,
undefined,
"output_config must not survive in the inner Gemini request"
);
});
@@ -0,0 +1,54 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { AntigravityExecutor } from "../../open-sse/executors/antigravity.ts";
// Regression for #1926: Claude/OpenAI "thinking" fields leaked into the Antigravity
// (Google Cloud Code) request envelope, which Google rejects at the top level with
// `400 Bad input: Error: oneOf at '/' not met` (and, for named fields, `Unknown name
// "thinking"`). This broke every reasoning/thinking model served via Antigravity
// (e.g. `claude-opus-4-x-thinking`). The envelope spreads `...passthroughFields`
// (every top-level body field except a known few), so a top-level thinking field set
// by the unified thinking adapter leaked straight into the envelope Google validates.
//
// The #1944 fix only dropped `output_config`/`output_format`; the thinking family
// (`thinking`, `reasoning_effort`, `reasoning`, `enable_thinking`, `thinking_budget`)
// still passed through. This test guards that the whole family is stripped.
const THINKING_FIELDS = [
"thinking",
"reasoning_effort",
"reasoning",
"enable_thinking",
"thinking_budget",
] as const;
test("transformRequest drops Claude/OpenAI thinking fields from the Antigravity envelope (#1926)", async () => {
const executor = new AntigravityExecutor();
const body = {
thinking: { type: "enabled", budget_tokens: 4096 },
reasoning_effort: "high",
reasoning: { effort: "high" },
enable_thinking: true,
thinking_budget: 4096,
request: {
contents: [{ role: "user", parts: [{ text: "hi" }] }],
generationConfig: {},
},
};
const result = await executor.transformRequest("antigravity/claude-opus-4-8-thinking", body, true, {
projectId: "project-1",
});
if (result instanceof Response) throw new Error("Unexpected Response from transformRequest");
const envelope = result as Record<string, unknown>;
for (const field of THINKING_FIELDS) {
assert.equal(
envelope[field],
undefined,
`top-level "${field}" must not reach the Google Cloud Code envelope`
);
}
});
@@ -0,0 +1,193 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
AG_DECOY_TOOLS,
AG_TOOL_SUFFIX,
cloakAntigravityToolPayload,
stripEnumDescriptions,
} from "../../open-sse/config/toolCloaking.ts";
test("cloakAntigravityToolPayload cloaks custom tools, preserves native tools and injects decoys", () => {
const payload = {
request: {
tools: [
{
functionDeclarations: [
{
name: "workspace_read",
description: "Read a file",
parameters: { type: "OBJECT", properties: {} },
},
{
name: "run_command",
description: "Native tool should stay visible",
parameters: { type: "OBJECT", properties: {} },
},
],
},
],
contents: [
{
role: "model",
parts: [{ functionCall: { name: "workspace_read", args: { path: "/tmp/a" } } }],
},
{
role: "user",
parts: [{ functionResponse: { name: "workspace_read", response: { ok: true } } }],
},
],
},
};
const result = cloakAntigravityToolPayload(payload);
const declarations = (result.body.request.tools?.[0] as any)?.functionDeclarations || [];
const names = declarations.map((tool: { name: string }) => tool.name);
assert.ok(names.includes(`workspace_read${AG_TOOL_SUFFIX}`));
assert.ok(names.includes("run_command"));
assert.ok(names.includes("browser_subagent"));
assert.ok(names.includes("mcp_sequential_thinking_sequentialthinking"));
for (const name of names) {
assert.match(name, /^[a-zA-Z0-9_]+$/);
}
assert.equal(
result.body.request.contents[0].parts[0].functionCall.name,
`workspace_read${AG_TOOL_SUFFIX}`
);
assert.equal(
result.body.request.contents[1].parts[0].functionResponse.name,
`workspace_read${AG_TOOL_SUFFIX}`
);
assert.equal(result.toolNameMap?.get(`workspace_read${AG_TOOL_SUFFIX}`), "workspace_read");
assert.equal(
declarations.filter((tool: { name: string }) => tool.name === "browser_subagent").length,
1
);
assert.ok(AG_DECOY_TOOLS.length > 20);
});
test("cloakAntigravityToolPayload composes namespace sanitization maps with Antigravity cloaking", () => {
const payload = {
_toolNameMap: new Map([["workspace_read", "mcp__filesystem__workspace_read"]]),
request: {
tools: [
{
functionDeclarations: [
{
name: "workspace_read",
description: "Read a file",
parameters: { type: "OBJECT", properties: {} },
},
],
},
],
contents: [],
},
};
const result = cloakAntigravityToolPayload(payload);
assert.equal(
result.toolNameMap?.get(`workspace_read${AG_TOOL_SUFFIX}`),
"mcp__filesystem__workspace_read"
);
});
test("stripEnumDescriptions removes enumDescriptions at every nesting level", () => {
const schema = {
type: "OBJECT",
enumDescriptions: ["should be removed at root"],
properties: {
mode: {
type: "STRING",
enum: ["a", "b"],
enumDescriptions: ["desc a", "desc b"],
},
nested: {
type: "OBJECT",
properties: {
choice: {
type: "STRING",
enumDescriptions: ["deep desc"],
},
},
},
list: {
type: "ARRAY",
items: {
type: "STRING",
enumDescriptions: ["item desc"],
},
},
},
};
const stripped = stripEnumDescriptions(schema) as any;
assert.equal("enumDescriptions" in stripped, false);
assert.equal("enumDescriptions" in stripped.properties.mode, false);
assert.equal("enumDescriptions" in stripped.properties.nested.properties.choice, false);
assert.equal("enumDescriptions" in stripped.properties.list.items, false);
// Non-target fields are preserved.
assert.deepEqual(stripped.properties.mode.enum, ["a", "b"]);
// Input is not mutated.
assert.ok(Array.isArray((schema.properties.mode as any).enumDescriptions));
});
test("cloakAntigravityToolPayload strips enumDescriptions from declaration parameters", () => {
const payload = {
request: {
tools: [
{
functionDeclarations: [
{
name: "workspace_read",
description: "Read a file",
parameters: {
type: "OBJECT",
enumDescriptions: ["root-level"],
properties: {
mode: {
type: "STRING",
enum: ["read", "write"],
enumDescriptions: ["read mode", "write mode"],
},
},
},
},
{
name: "run_command",
description: "Native tool keeps visible name but loses enumDescriptions",
parameters: {
type: "OBJECT",
properties: {
shell: {
type: "STRING",
enumDescriptions: ["bash", "zsh"],
},
},
},
},
],
},
],
contents: [],
},
};
const result = cloakAntigravityToolPayload(payload);
const declarations = (result.body.request.tools?.[0] as any)?.functionDeclarations || [];
const cloaked = declarations.find(
(d: { name: string }) => d.name === `workspace_read${AG_TOOL_SUFFIX}`
);
assert.ok(cloaked, "cloaked client tool present");
assert.equal("enumDescriptions" in cloaked.parameters, false);
assert.equal("enumDescriptions" in cloaked.parameters.properties.mode, false);
assert.deepEqual(cloaked.parameters.properties.mode.enum, ["read", "write"]);
const native = declarations.find((d: { name: string }) => d.name === "run_command");
assert.ok(native, "native tool preserved");
assert.equal("enumDescriptions" in native.parameters.properties.shell, false);
});
@@ -0,0 +1,285 @@
/**
* Tests for src/lib/usage/fetcher.ts — Antigravity quota parsing.
*
* Verifies that remainingFraction is correctly parsed:
* - undefined → 0% remaining (exhausted quota)
* - 0 → 0% remaining (exhausted quota, explicit)
* - 1.0 → 100% remaining (full quota)
* - 0.5 → 50% remaining (partial quota)
*/
import { describe, it, mock } from "node:test";
import assert from "node:assert/strict";
describe("getUsageForProvider (antigravity in fetcher.ts)", () => {
const connectionBase = {
provider: "antigravity",
accessToken: "fake-token",
providerSpecificData: {},
projectId: undefined,
id: "test-conn",
};
it("defaults to 0% remaining when remainingFraction is undefined", async () => {
const fetcherModule = await import("../../src/lib/usage/fetcher.ts");
const { getUsageForProvider } = fetcherModule;
const mockFetch = mock.method(global, "fetch", async () => ({
ok: true,
json: async () => ({
models: {
"gemini-2.5-pro": {
quotaInfo: {
remainingFraction: undefined,
resetTime: "2026-05-26T00:00:00Z",
},
},
},
}),
}));
try {
const result = await getUsageForProvider(connectionBase);
assert.ok(result, "should return a result");
if ("modelQuotas" in result) {
const quota = result.modelQuotas["gemini-2.5-pro"];
assert.ok(quota, "should have quota for gemini-2.5-pro");
assert.equal(quota.remaining, 0, "remaining should be 0%");
assert.equal(quota.limited, true, "should be marked as limited");
}
} finally {
mockFetch.mock.restore();
}
});
it("defaults to 0% remaining when remainingFraction key is absent (exhausted quota)", async () => {
const fetcherModule = await import("../../src/lib/usage/fetcher.ts");
const { getUsageForProvider } = fetcherModule;
const mockFetch = mock.method(global, "fetch", async () => ({
ok: true,
json: async () => ({
models: {
"gemini-2.5-pro": {
quotaInfo: {
// remainingFraction key is omitted (exhausted quota)
resetTime: "2026-05-26T00:00:00Z",
},
},
},
}),
}));
try {
const result = await getUsageForProvider(connectionBase);
assert.ok(result, "should return a result");
if ("modelQuotas" in result) {
const quota = result.modelQuotas["gemini-2.5-pro"];
assert.ok(quota, "should have quota for gemini-2.5-pro");
assert.equal(quota.remaining, 0, "remaining should be 0% when key is absent");
assert.equal(quota.limited, true, "should be marked as limited");
assert.equal(quota.resetAt, "2026-05-26T00:00:00Z", "should preserve resetTime");
}
} finally {
mockFetch.mock.restore();
}
});
it("parses remainingFraction=0 as exhausted quota", async () => {
const fetcherModule = await import("../../src/lib/usage/fetcher.ts");
const { getUsageForProvider } = fetcherModule;
const mockFetch = mock.method(global, "fetch", async () => ({
ok: true,
json: async () => ({
models: {
"gemini-2.5-pro": {
quotaInfo: {
remainingFraction: 0,
resetTime: "2026-05-26T00:00:00Z",
},
},
},
}),
}));
try {
const result = await getUsageForProvider(connectionBase);
assert.ok(result, "should return a result");
if ("modelQuotas" in result) {
const quota = result.modelQuotas["gemini-2.5-pro"];
assert.ok(quota, "should have quota for gemini-2.5-pro");
assert.equal(quota.remaining, 0, "remaining should be 0%");
assert.equal(quota.limited, true, "should be marked as limited");
}
} finally {
mockFetch.mock.restore();
}
});
it("parses remainingFraction=1.0 with resetTime as full quota", async () => {
const fetcherModule = await import("../../src/lib/usage/fetcher.ts");
const { getUsageForProvider } = fetcherModule;
const mockFetch = mock.method(global, "fetch", async () => ({
ok: true,
json: async () => ({
models: {
"gemini-2.5-pro": {
quotaInfo: {
remainingFraction: 1.0,
resetTime: "2026-05-26T00:00:00Z",
},
},
},
}),
}));
try {
const result = await getUsageForProvider(connectionBase);
assert.ok(result, "should return a result");
if ("modelQuotas" in result) {
const quota = result.modelQuotas["gemini-2.5-pro"];
assert.ok(quota, "should have quota for gemini-2.5-pro");
assert.equal(quota.remaining, 100, "remaining should be 100%");
assert.equal(quota.limited, false, "should not be marked as limited");
}
} finally {
mockFetch.mock.restore();
}
});
it("parses remainingFraction=0.5 as partial quota", async () => {
const fetcherModule = await import("../../src/lib/usage/fetcher.ts");
const { getUsageForProvider } = fetcherModule;
const mockFetch = mock.method(global, "fetch", async () => ({
ok: true,
json: async () => ({
models: {
"gemini-2.5-pro": {
quotaInfo: {
remainingFraction: 0.5,
resetTime: "2026-05-26T00:00:00Z",
},
},
},
}),
}));
try {
const result = await getUsageForProvider(connectionBase);
assert.ok(result, "should return a result");
if ("modelQuotas" in result) {
const quota = result.modelQuotas["gemini-2.5-pro"];
assert.ok(quota, "should have quota for gemini-2.5-pro");
assert.equal(quota.remaining, 50, "remaining should be 50%");
assert.equal(quota.limited, false, "should not be marked as limited");
}
} finally {
mockFetch.mock.restore();
}
});
it("skips credit-based models without remainingFraction", async () => {
const fetcherModule = await import("../../src/lib/usage/fetcher.ts");
const { getUsageForProvider } = fetcherModule;
const mockFetch = mock.method(global, "fetch", async () => ({
ok: true,
json: async () => ({
models: {
"gemini-2.5-pro": {
quotaInfo: {},
},
},
}),
}));
try {
const result = await getUsageForProvider(connectionBase);
assert.ok(result, "should return a result");
if ("modelQuotas" in result) {
assert.equal(
Object.keys(result.modelQuotas).length,
0,
"should not include credit-based models"
);
}
} finally {
mockFetch.mock.restore();
}
});
it("clamps remainingFraction > 1 to 100%", async () => {
const fetcherModule = await import("../../src/lib/usage/fetcher.ts");
const { getUsageForProvider } = fetcherModule;
const mockFetch = mock.method(global, "fetch", async () => ({
ok: true,
json: async () => ({
models: {
"gemini-2.5-pro": {
quotaInfo: {
remainingFraction: 1.5,
resetTime: "2026-05-26T00:00:00Z",
},
},
},
}),
}));
try {
const result = await getUsageForProvider(connectionBase);
assert.ok(result, "should return a result");
if ("modelQuotas" in result) {
const quota = result.modelQuotas["gemini-2.5-pro"];
assert.ok(quota, "should have quota for gemini-2.5-pro");
assert.equal(quota.remaining, 100, "remaining should be clamped to 100%");
assert.equal(quota.limited, false, "should not be marked as limited");
}
} finally {
mockFetch.mock.restore();
}
});
it("clamps negative remainingFraction to 0%", async () => {
const fetcherModule = await import("../../src/lib/usage/fetcher.ts");
const { getUsageForProvider } = fetcherModule;
const mockFetch = mock.method(global, "fetch", async () => ({
ok: true,
json: async () => ({
models: {
"gemini-2.5-pro": {
quotaInfo: {
remainingFraction: -0.5,
resetTime: "2026-05-26T00:00:00Z",
},
},
},
}),
}));
try {
const result = await getUsageForProvider(connectionBase);
assert.ok(result, "should return a result");
if ("modelQuotas" in result) {
const quota = result.modelQuotas["gemini-2.5-pro"];
assert.ok(quota, "should have quota for gemini-2.5-pro");
assert.equal(quota.remaining, 0, "remaining should be clamped to 0%");
assert.equal(quota.limited, true, "should be marked as limited");
}
} finally {
mockFetch.mock.restore();
}
});
});
@@ -0,0 +1,279 @@
/**
* Tests for open-sse/services/usage.ts — Antigravity quota parsing.
*
* Verifies that remainingFraction is correctly parsed:
* - undefined → 0% remaining (exhausted quota)
* - 0 → 0% remaining (exhausted quota, explicit)
* - 1.0 → 100% remaining (full quota)
* - 1.0 without resetTime → unlimited (e.g. tab-completion)
* - 0.5 → 50% remaining (partial quota)
*/
import { describe, it } from "node:test";
import assert from "node:assert/strict";
// IMPORTANT: load usage.ts up-front so the proxyFetch patch in
// `open-sse/index.ts` (which runs at module evaluation) finishes BEFORE we
// install fetch mocks. Otherwise the first test races the patch and ends up
// hitting the real network instead of the mock.
const usageModule = await import("../../open-sse/services/usage.ts");
const { getUsageForProvider } = usageModule;
describe("getUsageForProvider (antigravity in usage.ts)", () => {
const connectionBase = {
id: "test-conn",
provider: "antigravity",
accessToken: "fake-token",
providerSpecificData: {},
projectId: undefined,
};
it("defaults to 0% remaining when remainingFraction is undefined", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = async () =>
({
ok: true,
json: async () => ({
models: {
"gemini-3.5-flash-high": {
quotaInfo: {
remainingFraction: undefined,
resetTime: "2026-05-26T00:00:00Z",
},
},
},
}),
}) as Response;
try {
const result = await getUsageForProvider(connectionBase, { forceRefresh: true });
assert.ok(result, "should return a result");
assert.ok("quotas" in result, "should have quotas");
if ("quotas" in result) {
const quota = result.quotas["gemini-3.5-flash-high"];
assert.ok(quota, "should have quota for gemini-3.5-flash-high");
assert.equal(quota.remainingPercentage, 0, "remaining should be 0%");
assert.equal(quota.unlimited, false, "should not be unlimited");
assert.equal(quota.used > 0, true, "used should be > 0 when quota is exhausted");
}
} finally {
globalThis.fetch = originalFetch;
}
});
it("parses remainingFraction=0 as exhausted quota", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = async () =>
({
ok: true,
json: async () => ({
models: {
"gemini-3.5-flash-high": {
quotaInfo: {
remainingFraction: 0,
resetTime: "2026-05-26T00:00:00Z",
},
},
},
}),
}) as Response;
try {
const usageModule = await import("../../open-sse/services/usage.ts");
const { getUsageForProvider } = usageModule;
const result = await getUsageForProvider(connectionBase, { forceRefresh: true });
assert.ok(result, "should return a result");
assert.ok("quotas" in result, "should have quotas");
if ("quotas" in result) {
const quota = result.quotas["gemini-3.5-flash-high"];
assert.ok(quota, "should have quota for gemini-3.5-flash-high");
assert.equal(quota.remainingPercentage, 0, "remaining should be 0%");
assert.equal(quota.unlimited, false, "should not be unlimited");
}
} finally {
globalThis.fetch = originalFetch;
}
});
it("parses remainingFraction=1.0 with resetTime as full quota", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = async () =>
({
ok: true,
json: async () => ({
models: {
"gemini-3.5-flash-high": {
quotaInfo: {
remainingFraction: 1.0,
resetTime: "2026-05-26T00:00:00Z",
},
},
},
}),
}) as Response;
try {
const usageModule = await import("../../open-sse/services/usage.ts");
const { getUsageForProvider } = usageModule;
const result = await getUsageForProvider(connectionBase, { forceRefresh: true });
assert.ok(result, "should return a result");
assert.ok("quotas" in result, "should have quotas");
if ("quotas" in result) {
const quota = result.quotas["gemini-3.5-flash-high"];
assert.ok(quota, "should have quota for gemini-3.5-flash-high");
assert.equal(quota.remainingPercentage, 100, "remaining should be 100%");
assert.equal(quota.unlimited, false, "should not be unlimited (has resetTime)");
}
} finally {
globalThis.fetch = originalFetch;
}
});
it("parses remainingFraction=1.0 without resetTime as unlimited (e.g. tab-completion)", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = async () =>
({
ok: true,
json: async () => ({
models: {
"gemini-3.1-flash-lite": {
quotaInfo: {
remainingFraction: 1.0,
},
},
},
}),
}) as Response;
try {
const usageModule = await import("../../open-sse/services/usage.ts");
const { getUsageForProvider } = usageModule;
const result = await getUsageForProvider(connectionBase, { forceRefresh: true });
assert.ok(result, "should return a result");
assert.ok("quotas" in result, "should have quotas");
if ("quotas" in result) {
const quota = result.quotas["gemini-3.1-flash-lite"];
assert.ok(quota, "should have quota for gemini-3.1-flash-lite");
assert.equal(quota.remainingPercentage, 100, "remaining should be 100%");
assert.equal(quota.unlimited, true, "should be unlimited (no resetTime)");
}
} finally {
globalThis.fetch = originalFetch;
}
});
it("parses remainingFraction=0.5 as partial quota", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = async () =>
({
ok: true,
json: async () => ({
models: {
"gemini-3.5-flash-high": {
quotaInfo: {
remainingFraction: 0.5,
resetTime: "2026-05-26T00:00:00Z",
},
},
},
}),
}) as Response;
try {
const usageModule = await import("../../open-sse/services/usage.ts");
const { getUsageForProvider } = usageModule;
const result = await getUsageForProvider(connectionBase, { forceRefresh: true });
assert.ok(result, "should return a result");
assert.ok("quotas" in result, "should have quotas");
if ("quotas" in result) {
const quota = result.quotas["gemini-3.5-flash-high"];
assert.ok(quota, "should have quota for gemini-3.5-flash-high");
assert.equal(quota.remainingPercentage, 50, "remaining should be 50%");
assert.equal(quota.unlimited, false, "should not be unlimited");
}
} finally {
globalThis.fetch = originalFetch;
}
});
it("clamps remainingFraction > 1 to 100%", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = async () =>
({
ok: true,
json: async () => ({
models: {
"gemini-3.5-flash-high": {
quotaInfo: {
remainingFraction: 1.5,
resetTime: "2026-05-26T00:00:00Z",
},
},
},
}),
}) as Response;
try {
const usageModule = await import("../../open-sse/services/usage.ts");
const { getUsageForProvider } = usageModule;
const result = await getUsageForProvider(connectionBase, { forceRefresh: true });
assert.ok(result, "should return a result");
assert.ok("quotas" in result, "should have quotas");
if ("quotas" in result) {
const quota = result.quotas["gemini-3.5-flash-high"];
assert.ok(quota, "should have quota for gemini-3.5-flash-high");
assert.equal(quota.remainingPercentage, 100, "remaining should be clamped to 100%");
assert.equal(quota.unlimited, false, "should not be unlimited (has resetTime)");
}
} finally {
globalThis.fetch = originalFetch;
}
});
it("clamps negative remainingFraction to 0%", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = async () =>
({
ok: true,
json: async () => ({
models: {
"gemini-3.5-flash-high": {
quotaInfo: {
remainingFraction: -0.5,
resetTime: "2026-05-26T00:00:00Z",
},
},
},
}),
}) as Response;
try {
const usageModule = await import("../../open-sse/services/usage.ts");
const { getUsageForProvider } = usageModule;
const result = await getUsageForProvider(connectionBase, { forceRefresh: true });
assert.ok(result, "should return a result");
assert.ok("quotas" in result, "should have quotas");
if ("quotas" in result) {
const quota = result.quotas["gemini-3.5-flash-high"];
assert.ok(quota, "should have quota for gemini-3.5-flash-high");
assert.equal(quota.remainingPercentage, 0, "remaining should be clamped to 0%");
assert.equal(quota.unlimited, false, "should not be unlimited");
}
} finally {
globalThis.fetch = originalFetch;
}
});
});
+94
View File
@@ -0,0 +1,94 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
ANTIGRAVITY_FALLBACK_VERSION,
ANTIGRAVITY_VERSION_CACHE_TTL_MS,
clearAntigravityVersionCache,
getCachedAntigravityVersion,
resolveAntigravityVersion,
seedAntigravityVersionCache,
} from "../../open-sse/services/antigravityVersion.ts";
const originalDateNow = Date.now;
test.afterEach(() => {
Date.now = originalDateNow;
clearAntigravityVersionCache();
});
test("resolveAntigravityVersion uses the official release feed and caches the result for 6 hours", async () => {
let calls = 0;
const fetchMock = async () => {
calls += 1;
return new Response(JSON.stringify([{ version: "4.2.0", execution_id: "4781536860569600" }]), {
status: 200,
headers: { "Content-Type": "application/json" },
});
};
const first = await resolveAntigravityVersion(fetchMock as typeof fetch);
const second = await resolveAntigravityVersion(fetchMock as typeof fetch);
assert.equal(first, "4.2.0");
assert.equal(second, "4.2.0");
assert.equal(calls, 1);
assert.equal(getCachedAntigravityVersion(), "4.2.0");
});
test("resolveAntigravityVersion refreshes the cache after the TTL elapses", async () => {
let now = 1_000;
Date.now = () => now;
const firstFetch = async () =>
new Response(JSON.stringify([{ version: "4.2.0" }]), {
status: 200,
headers: { "Content-Type": "application/json" },
});
const secondFetch = async () =>
new Response(JSON.stringify([{ version: "4.3.0" }]), {
status: 200,
headers: { "Content-Type": "application/json" },
});
assert.equal(await resolveAntigravityVersion(firstFetch as typeof fetch), "4.2.0");
now += ANTIGRAVITY_VERSION_CACHE_TTL_MS + 1;
assert.equal(await resolveAntigravityVersion(secondFetch as typeof fetch), "4.3.0");
assert.equal(getCachedAntigravityVersion(), "4.3.0");
});
test("resolveAntigravityVersion falls back to the last known good version or bundled fallback", async () => {
const failingFetch = async () => {
throw new Error("network down");
};
assert.equal(
await resolveAntigravityVersion(failingFetch as typeof fetch),
ANTIGRAVITY_FALLBACK_VERSION
);
seedAntigravityVersionCache("4.2.1", 0);
assert.equal(await resolveAntigravityVersion(failingFetch as typeof fetch), "4.2.1");
});
test("resolveAntigravityVersion parses GitHub-style tag_name payloads with or without a v prefix", async () => {
let calls = 0;
const fetchMock = async () => {
calls += 1;
if (calls === 1) {
return new Response(JSON.stringify({ malformed: true }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
return new Response(JSON.stringify({ tag_name: "v4.2.3" }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
};
assert.equal(await resolveAntigravityVersion(fetchMock as typeof fetch), "4.2.3");
});
+482
View File
@@ -0,0 +1,482 @@
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 { SignJWT } from "jose";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-api-auth-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.API_KEY_SECRET = "test-api-key-secret";
const core = await import("../../src/lib/db/core.ts");
const localDb = await import("../../src/lib/localDb.ts");
const apiKeysDb = await import("../../src/lib/db/apiKeys.ts");
const apiAuth = await import("../../src/shared/utils/apiAuth.ts");
const { requireManagementAuth } = await import("../../src/lib/api/requireManagementAuth.ts");
const { getLegacyCliTokenSync, getMachineTokenSync } =
await import("../../src/lib/machineToken.ts");
const { CLI_TOKEN_HEADER } = await import("../../src/server/authz/headers.ts");
const ORIGINAL_JWT_SECRET = process.env.JWT_SECRET;
const ORIGINAL_INITIAL_PASSWORD = process.env.INITIAL_PASSWORD;
async function resetStorage() {
core.resetDbInstance();
apiKeysDb.resetApiKeyState();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
delete process.env.JWT_SECRET;
delete process.env.INITIAL_PASSWORD;
}
function makeCookieRequest(token: string) {
return {
cookies: {
get(name: string) {
return name === "auth_token" && token ? { value: token } : undefined;
},
},
headers: new Headers(),
};
}
test.beforeEach(async () => {
await resetStorage();
});
test.after(() => {
core.resetDbInstance();
apiKeysDb.resetApiKeyState();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
if (ORIGINAL_JWT_SECRET === undefined) {
delete process.env.JWT_SECRET;
} 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;
}
});
test("isPublicRoute recognizes allowed API prefixes", () => {
assert.equal(apiAuth.isPublicRoute("/api/auth/login"), true);
assert.equal(apiAuth.isPublicRoute("/api/v1/chat/completions"), true);
assert.equal(apiAuth.isPublicRoute("/api/sync/bundle"), true);
assert.equal(apiAuth.isPublicRoute("/api/monitoring/health", "GET"), true);
assert.equal(apiAuth.isPublicRoute("/api/monitoring/health", "DELETE"), false);
assert.equal(apiAuth.isPublicRoute("/api/settings"), false);
});
test("verifyAuth accepts a valid JWT session cookie", async () => {
process.env.JWT_SECRET = "jwt-secret-for-tests";
const secret = new TextEncoder().encode(process.env.JWT_SECRET);
const token = await new SignJWT({ authenticated: true })
.setProtectedHeader({ alg: "HS256" })
.setIssuedAt()
.setExpirationTime("1h")
.sign(secret);
const result = await apiAuth.verifyAuth(makeCookieRequest(token));
assert.equal(result, null);
});
test("verifyAuth falls back to bearer API key validation after a bad JWT", async () => {
process.env.JWT_SECRET = "jwt-secret-for-tests";
const key = await apiKeysDb.createApiKey("integration", "machine1234567890");
const request = {
cookies: {
get() {
return { value: "definitely-not-a-valid-jwt" };
},
},
headers: new Headers({ authorization: `Bearer ${key.key}` }),
url: "https://example.com/api/v1/models",
};
const result = await apiAuth.verifyAuth(request);
assert.equal(result, null);
});
test("verifyAuth no longer accepts API keys supplied via query string (#3300 follow-up)", async () => {
// Query-string token fallbacks were removed (credential-in-URL leaks into logs).
const key = await apiKeysDb.createApiKey("query-auth", "machine1234567890");
const result = await apiAuth.verifyAuth({
cookies: {
get() {
return undefined;
},
},
headers: new Headers(),
url: `https://example.com/api/v1/models?token=${encodeURIComponent(key.key)}`,
});
// No usable credential → authentication fails (was incorrectly accepted before).
assert.notEqual(result, null);
});
test("isAuthenticated accepts API keys embedded in vscode path aliases", async () => {
const key = await apiKeysDb.createApiKey("path-auth", "machine1234567890");
const request = new Request(
`https://example.com/api/v1/vscode/${encodeURIComponent(key.key)}/models`
);
const result = await apiAuth.isAuthenticated(request);
assert.equal(result, true);
});
test("verifyAuth never honours a URL-borne token on MANAGEMENT routes (#3300 follow-up)", async () => {
// The historical escalation: a credential in the query string on a management
// route (/api/* but not /api/v1/*). It must not be extracted at all, so the
// failure is "Authentication required" (no credential) — NOT "Invalid
// management token" (which the pre-fix code returned, proving the URL token
// had been picked up and tried against management validation).
const key = await apiKeysDb.createApiKey("mgmt-url", "machine1234567890");
const result = await apiAuth.verifyAuth({
cookies: {
get() {
return undefined;
},
},
headers: new Headers(),
url: `https://example.com/api/providers?token=${encodeURIComponent(key.key)}`,
});
assert.equal(result, "Authentication required");
});
test("verifyAuth rejects bearer API keys on management routes", async () => {
const key = await apiKeysDb.createApiKey("integration", "machine1234567890");
const result = await apiAuth.verifyAuth({
cookies: {
get() {
return undefined;
},
},
headers: new Headers({ authorization: `Bearer ${key.key}` }),
url: "https://example.com/api/providers",
});
assert.equal(result, "Invalid management token");
});
test("verifyAuth rejects requests without valid credentials", async () => {
const result = await apiAuth.verifyAuth({
cookies: {
get() {
return undefined;
},
},
headers: new Headers({ authorization: "Bearer sk-invalid" }),
url: "https://example.com/api/v1/models",
});
assert.equal(result, "Authentication required");
});
test("isAuthenticated accepts bearer API keys on client-facing routes", async () => {
const key = await apiKeysDb.createApiKey("integration", "machine1234567890");
const request = new Request("https://example.com/api/v1/models", {
headers: { authorization: `Bearer ${key.key}` },
});
const result = await apiAuth.isAuthenticated(request);
assert.equal(result, true);
});
test("isAuthenticated rejects bearer API keys on management routes", async () => {
process.env.INITIAL_PASSWORD = "bootstrap-password";
await localDb.updateSettings({ requireLogin: true, password: "" });
const key = await apiKeysDb.createApiKey("integration", "machine1234567890");
const request = new Request("https://example.com/api/providers", {
headers: { authorization: `Bearer ${key.key}` },
});
const result = await apiAuth.isAuthenticated(request);
assert.equal(result, false);
});
test("verifyAuth accepts bearer API keys with manage scope on management routes", async () => {
const key = await apiKeysDb.createApiKey("mcp-management", "machine1234567890", ["manage"]);
const result = await apiAuth.verifyAuth({
cookies: {
get() {
return undefined;
},
},
headers: new Headers({ authorization: `Bearer ${key.key}` }),
url: "https://example.com/api/providers",
});
assert.equal(result, null);
});
test("verifyAuth accepts bearer API keys with admin scope on management routes", async () => {
const key = await apiKeysDb.createApiKey("mcp-admin", "machine1234567890", ["admin"]);
const result = await apiAuth.verifyAuth({
cookies: {
get() {
return undefined;
},
},
headers: new Headers({ authorization: `Bearer ${key.key}` }),
url: "https://example.com/api/settings",
});
assert.equal(result, null);
});
test("verifyAuth still rejects unscoped bearer API keys on management routes", async () => {
const key = await apiKeysDb.createApiKey("integration-no-scope", "machine1234567890");
const result = await apiAuth.verifyAuth({
cookies: {
get() {
return undefined;
},
},
headers: new Headers({ authorization: `Bearer ${key.key}` }),
url: "https://example.com/api/providers",
});
assert.equal(result, "Invalid management token");
});
test("isAuthenticated accepts bearer API keys with manage scope on management routes", async () => {
process.env.INITIAL_PASSWORD = "bootstrap-password";
await localDb.updateSettings({ requireLogin: true, password: "" });
const key = await apiKeysDb.createApiKey("mcp-management", "machine1234567890", ["manage"]);
const request = new Request("https://example.com/api/providers", {
headers: { authorization: `Bearer ${key.key}` },
});
const result = await apiAuth.isAuthenticated(request);
assert.equal(result, true);
});
test("monitoring health reset route requires dashboard authentication", async () => {
process.env.INITIAL_PASSWORD = "bootstrap-password";
await localDb.updateSettings({ requireLogin: true, password: "" });
const healthRoute = await import("../../src/app/api/monitoring/health/route.ts");
const response = await healthRoute.DELETE(
new Request("https://example.com/api/monitoring/health", { method: "DELETE" })
);
assert.equal(response.status, 401);
});
test("isAuthenticated returns false when auth is required without valid credentials", async () => {
// Force requireLogin to be active
process.env.INITIAL_PASSWORD = "bootstrap-password";
await localDb.updateSettings({ requireLogin: true, password: "" });
const request = new Request("https://example.com/api/providers");
const result = await apiAuth.isAuthenticated(request);
assert.equal(result, false);
});
test("isAuthRequired is disabled when requireLogin is false", async () => {
await localDb.updateSettings({ requireLogin: false });
const result = await apiAuth.isAuthRequired();
assert.equal(result, false);
});
test("isAuthRequired is disabled while no password exists", async () => {
await localDb.updateSettings({ requireLogin: true, password: "" });
const result = await apiAuth.isAuthRequired();
assert.equal(result, false);
});
test("isAuthRequired keeps fresh bootstrap open only on loopback", async () => {
await localDb.updateSettings({ requireLogin: true, password: "" });
assert.equal(await apiAuth.isAuthRequired(new Request("http://localhost/api/providers")), false);
assert.equal(await apiAuth.isAuthRequired(new Request("http://127.0.0.1/api/providers")), false);
assert.equal(
await apiAuth.isAuthRequired(new Request("https://example.com/api/providers")),
true
);
});
test("isAuthenticated rejects remote management bootstrap without a configured password", async () => {
await localDb.updateSettings({ requireLogin: true, password: "" });
const request = new Request("https://example.com/api/providers");
assert.equal(await apiAuth.isAuthenticated(request), false);
});
test("isAuthRequired stays enabled when a password exists", async () => {
await localDb.updateSettings({ requireLogin: true, password: "hashed-password" });
const result = await apiAuth.isAuthRequired();
assert.equal(result, true);
});
test("isAuthRequired stays enabled when INITIAL_PASSWORD is present", async () => {
process.env.INITIAL_PASSWORD = "bootstrap-password";
await localDb.updateSettings({ requireLogin: true, password: "" });
const result = await apiAuth.isAuthRequired();
assert.equal(result, true);
delete process.env.INITIAL_PASSWORD;
});
test("getApiKeyMetadata recognizes OMNIROUTE_API_KEY environment variable", async () => {
const envKey = "sk-test-env-key-" + Date.now();
process.env.OMNIROUTE_API_KEY = envKey;
const metadata = await apiKeysDb.getApiKeyMetadata(envKey);
assert.ok(metadata);
assert.equal(metadata.id, "env-key");
assert.equal(metadata.name, "Environment Key");
delete process.env.OMNIROUTE_API_KEY;
});
test("getApiKeyMetadata recognizes ROUTER_API_KEY environment variable", async () => {
const envKey = "sk-test-router-key-" + Date.now();
process.env.ROUTER_API_KEY = envKey;
const metadata = await apiKeysDb.getApiKeyMetadata(envKey);
assert.ok(metadata);
assert.equal(metadata.id, "env-key");
delete process.env.ROUTER_API_KEY;
});
// ──── requireManagementAuth ────
async function setupAuth() {
process.env.INITIAL_PASSWORD = "bootstrap-password";
await localDb.updateSettings({ requireLogin: true, password: "" });
}
function managementRequest(bearerKey?: string) {
return new Request("https://example.com/api/combos", {
headers: bearerKey ? { authorization: `Bearer ${bearerKey}` } : {},
});
}
function managementCliRequest(token: string) {
const request = new Request("http://localhost:20128/api/combos", {
headers: { [CLI_TOKEN_HEADER]: token },
});
return Object.assign(request, { ip: "127.0.0.1" }) as Request;
}
test("requireManagementAuth returns 401 with no credentials", async () => {
await setupAuth();
const res = await requireManagementAuth(managementRequest());
assert.ok(res);
assert.equal(res.status, 401);
});
test("requireManagementAuth returns 403 for an invalid management token", async () => {
await setupAuth();
const res = await requireManagementAuth(managementRequest("sk-not-a-real-key"));
assert.ok(res);
assert.equal(res.status, 403);
const body = await res.json();
assert.equal(body.error?.message, "Invalid management token");
});
test("requireManagementAuth returns 403 for valid key without manage scope", async () => {
await setupAuth();
const key = await apiKeysDb.createApiKey("inference-only", "machine-test");
const res = await requireManagementAuth(managementRequest(key.key));
assert.ok(res);
assert.equal(res.status, 403);
const body = await res.json();
assert.ok(body.error?.message?.includes("manage"));
});
test("requireManagementAuth returns null for valid key with manage scope", async () => {
await setupAuth();
const key = await apiKeysDb.createApiKey("admin-key", "machine-test", ["manage"]);
const res = await requireManagementAuth(managementRequest(key.key));
assert.equal(res, null);
});
test("requireManagementAuth accepts the 64-character local machine token", async () => {
await setupAuth();
const token = getMachineTokenSync();
assert.equal(token.length, 64);
const res = await requireManagementAuth(managementCliRequest(token));
assert.equal(res, null);
});
test("requireManagementAuth accepts the legacy 32-character local CLI token", async () => {
await setupAuth();
const token = getLegacyCliTokenSync();
assert.equal(token.length, 32);
const res = await requireManagementAuth(managementCliRequest(token));
assert.equal(res, null);
});
test("requireManagementAuth returns null for OMNIROUTE_API_KEY env passthrough", async () => {
await setupAuth();
const envKey = "sk-env-root-" + Date.now();
process.env.OMNIROUTE_API_KEY = envKey;
try {
const res = await requireManagementAuth(managementRequest(envKey));
assert.equal(res, null);
} finally {
delete process.env.OMNIROUTE_API_KEY;
}
});
test("requireManagementAuth returns 403 for revoked key with manage scope", async () => {
await setupAuth();
const key = await apiKeysDb.createApiKey("revoked-admin", "machine-test", ["manage"]);
await apiKeysDb.revokeApiKey(key.id);
const res = await requireManagementAuth(managementRequest(key.key));
assert.ok(res);
assert.equal(res.status, 403);
const body = await res.json();
assert.equal(body.error?.message, "Invalid management token");
});
test("requireManagementAuth returns null for valid JWT cookie", async () => {
await setupAuth();
process.env.JWT_SECRET = "jwt-secret-for-tests";
const secret = new TextEncoder().encode(process.env.JWT_SECRET);
const token = await new SignJWT({ authenticated: true })
.setProtectedHeader({ alg: "HS256" })
.setIssuedAt()
.setExpirationTime("1h")
.sign(secret);
const request = {
cookies: { get: (name: string) => (name === "auth_token" ? { value: token } : undefined) },
headers: new Headers(),
url: "https://example.com/api/combos",
};
const res = await requireManagementAuth(request as unknown as Request);
assert.equal(res, null);
});
+24
View File
@@ -0,0 +1,24 @@
import test from "node:test";
import assert from "node:assert/strict";
// GET /api/docs serves the Redoc-rendered API reference (#4781). Static HTML
// shell that loads Redoc from a CDN and points it at /openapi.yaml (the
// canonical spec served from public/openapi.yaml). PUBLIC tier — no auth.
const docsRoute = await import("../../src/app/api/docs/route.ts");
test("GET /api/docs returns a 200 text/html Redoc shell", async () => {
const response = docsRoute.GET();
assert.equal(response.status, 200);
assert.match(response.headers.get("content-type") ?? "", /text\/html/);
const body = await response.text();
// Renders Redoc and points it at the canonical spec.
assert.match(body, /redoc/i);
assert.ok(body.includes("/openapi.yaml"), "Redoc must load the canonical /openapi.yaml spec");
// No stack traces / secrets leaked into the static shell.
assert.ok(!body.includes("at /"), "static shell must not leak stack-trace frames");
});
test("GET /api/docs is statically renderable (no per-request work)", () => {
assert.equal(docsRoute.dynamic, "force-static");
});
+28
View File
@@ -0,0 +1,28 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { extractApiErrorMessage } from "@/shared/http/apiErrorMessage";
describe("extractApiErrorMessage (#5340)", () => {
it("surfaces the message from a structured error envelope", () => {
const body = {
error: { code: "INVALID_ORIGIN", message: "Invalid request origin", correlation_id: "x" },
};
assert.equal(extractApiErrorMessage(body, "fallback"), "Invalid request origin");
});
it("returns a plain string error as-is", () => {
assert.equal(extractApiErrorMessage({ error: "boom" }, "fallback"), "boom");
});
it("never renders a raw error object — falls back when message is missing", () => {
assert.equal(extractApiErrorMessage({ error: { code: "X" } }, "fallback"), "fallback");
});
it("falls back for empty, null, or malformed bodies", () => {
assert.equal(extractApiErrorMessage({ error: " " }, "fallback"), "fallback");
assert.equal(extractApiErrorMessage(null, "fallback"), "fallback");
assert.equal(extractApiErrorMessage({}, "fallback"), "fallback");
assert.equal(extractApiErrorMessage({ error: { message: 42 } }, "fallback"), "fallback");
});
});
+138
View File
@@ -0,0 +1,138 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omr-apikey-lifecycle-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.API_KEY_SECRET = "test-secret";
const core = await import("../../src/lib/db/core.ts");
const apiKeysDb = await import("../../src/lib/db/apiKeys.ts");
const ORIGINAL_OMNIROUTE_API_KEY = process.env.OMNIROUTE_API_KEY;
const ORIGINAL_ROUTER_API_KEY = process.env.ROUTER_API_KEY;
function reset() {
core.resetDbInstance();
apiKeysDb.resetApiKeyState();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
delete process.env.OMNIROUTE_API_KEY;
delete process.env.ROUTER_API_KEY;
}
test.beforeEach(() => {
reset();
});
test.after(() => {
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
if (ORIGINAL_OMNIROUTE_API_KEY === undefined) delete process.env.OMNIROUTE_API_KEY;
else process.env.OMNIROUTE_API_KEY = ORIGINAL_OMNIROUTE_API_KEY;
if (ORIGINAL_ROUTER_API_KEY === undefined) delete process.env.ROUTER_API_KEY;
else process.env.ROUTER_API_KEY = ORIGINAL_ROUTER_API_KEY;
});
async function makeKey(name = "lifecycle-test", machineId = "machine-lifecycle") {
const created = await apiKeysDb.createApiKey(name, machineId);
assert.ok(created?.key, "createApiKey returned a key");
return created;
}
test("validateApiKey returns true for a fresh active key", async () => {
const created = await makeKey();
assert.equal(await apiKeysDb.validateApiKey(created.key), true);
});
test("validateApiKey rejects revoked keys after revokeApiKey", async () => {
const created = await makeKey();
assert.equal(await apiKeysDb.validateApiKey(created.key), true);
const ok = await apiKeysDb.revokeApiKey(created.id);
assert.equal(ok, true);
assert.equal(await apiKeysDb.validateApiKey(created.key), false);
});
test("validateApiKey rejects keys whose expires_at has passed", async () => {
const created = await makeKey();
const past = new Date(Date.now() - 60_000).toISOString();
const ok = await apiKeysDb.setApiKeyExpiry(created.id, past);
assert.equal(ok, true);
assert.equal(await apiKeysDb.validateApiKey(created.key), false);
});
test("validateApiKey accepts keys with future expires_at", async () => {
const created = await makeKey();
const future = new Date(Date.now() + 60 * 60_000).toISOString();
const ok = await apiKeysDb.setApiKeyExpiry(created.id, future);
assert.equal(ok, true);
assert.equal(await apiKeysDb.validateApiKey(created.key), true);
});
test("validateApiKey rejects deactivated keys (is_active=false)", async () => {
const created = await makeKey();
const ok = await apiKeysDb.updateApiKeyPermissions(created.id, { isActive: false });
assert.equal(ok, true);
assert.equal(await apiKeysDb.validateApiKey(created.key), false);
});
test("revokeApiKey is idempotent and returns false for missing id", async () => {
const created = await makeKey();
assert.equal(await apiKeysDb.revokeApiKey(created.id), true);
assert.equal(await apiKeysDb.revokeApiKey(created.id), true);
assert.equal(await apiKeysDb.revokeApiKey("00000000-0000-0000-0000-000000000000"), false);
});
test("getApiKeyMetadata exposes lifecycle and policy fields", async () => {
const created = await makeKey();
await apiKeysDb.setApiKeyExpiry(created.id, new Date(Date.now() + 86_400_000).toISOString());
const md = await apiKeysDb.getApiKeyMetadata(created.key);
assert.ok(md);
assert.equal(md!.isActive, true);
assert.equal(md!.revokedAt, null);
assert.ok(md!.expiresAt && Date.parse(md!.expiresAt) > Date.now());
assert.deepEqual(md!.ipAllowlist, []);
assert.deepEqual(md!.scopes, []);
});
test("validateApiKey accepts configured environment API keys", async () => {
process.env.OMNIROUTE_API_KEY = "sk-env-lifecycle-test";
assert.equal(await apiKeysDb.validateApiKey("sk-env-lifecycle-test"), true);
});
test("validateApiKey updates last_used_at for persisted keys", async () => {
const created = await makeKey();
assert.equal(await apiKeysDb.validateApiKey(created.key), true);
const db = core.getDbInstance() as {
prepare(sql: string): { get(id: string): { last_used_at: string | null } | undefined };
};
const row = db.prepare("SELECT last_used_at FROM api_keys WHERE id = ?").get(created.id);
assert.ok(row?.last_used_at, "last_used_at should be set on successful validation");
assert.ok(Date.parse(row.last_used_at) > 0, "last_used_at should be an ISO timestamp");
});
test("getApiKeyMetadata returns proxyId for a key with proxy_id set", async () => {
const created = await makeKey("proxy-test", "machine-proxy");
const ok = await apiKeysDb.updateApiKeyPermissions(created.id, { proxyId: "test-proxy-id" });
assert.equal(ok, true);
const metadata = await apiKeysDb.getApiKeyMetadata(created.key);
assert.ok(metadata);
assert.equal(metadata!.proxyId, "test-proxy-id");
});
test("getApiKeyMetadata returns proxyId null for a key without proxy_id", async () => {
const created = await makeKey("no-proxy-test", "machine-no-proxy");
const metadata = await apiKeysDb.getApiKeyMetadata(created.key);
assert.ok(metadata);
assert.equal(metadata!.proxyId, null);
});
+225
View File
@@ -0,0 +1,225 @@
/**
* Unit tests for the masked API key fix (#523).
*
* GET /api/keys returns masked API keys (e.g. "sk-31c4e****8600").
* CLI tool card dropdowns used `key.key` (the masked value) as the select
* option value, so the masked key got written to config files, causing 401s.
*
* The fix: frontends send `keyId` (DB row id) instead, and backends resolve
* the full key from DB via `resolveApiKey()`.
*
* This test inlines the resolver logic (ESM modules are read-only) with a
* mock DB lookup function.
*/
import test, { describe, it } from "node:test";
import assert from "node:assert/strict";
// ─── Mock DB + inlined resolveApiKey ────────────────────────────────────
/** In-memory key store keyed by id */
const mockKeyStore = new Map();
/**
* Mock getApiKeyById — mirrors the real function's contract:
* returns the key record (with .key field) or null.
*/
async function getApiKeyById(id) {
return mockKeyStore.get(id) || null;
}
/**
* Inlined resolveApiKey from src/shared/services/apiKeyResolver.ts
* (can't import ESM modules in test runner without tsx overhead).
*/
async function resolveApiKey(apiKeyId, apiKey) {
if (apiKeyId) {
try {
const keyRecord = await getApiKeyById(apiKeyId);
if (keyRecord?.key) return keyRecord.key;
} catch {
/* fall through */
}
}
return apiKey || "sk_omniroute";
}
// ─── Server-side masking function (matches /api/keys endpoint) ─────────
/**
* Mask an API key for display: first 8 chars + "****" + last 4 chars.
* This is the server-side masking that created the original bug.
*/
function maskApiKey(key) {
if (!key || key.length <= 12) return key;
return key.slice(0, 8) + "****" + key.slice(-4);
}
// ─── Tests ──────────────────────────────────────────────────────────────
describe("resolveApiKey", () => {
it("resolves full key from apiKeyId when DB lookup succeeds", async () => {
mockKeyStore.clear();
mockKeyStore.set("key-001", { id: "key-001", key: "sk-31c4eabcd1234efgh8600" });
const result = await resolveApiKey("key-001", null);
assert.equal(result, "sk-31c4eabcd1234efgh8600");
});
it("falls back to apiKey when apiKeyId lookup returns null", async () => {
mockKeyStore.clear();
const result = await resolveApiKey("nonexistent-id", "sk-fallback-key");
assert.equal(result, "sk-fallback-key");
});
it("falls back to apiKey when apiKeyId lookup throws", async () => {
mockKeyStore.clear();
// Override getApiKeyById to throw
const originalGet = getApiKeyById;
const throwingGet = async () => {
throw new Error("DB connection failed");
};
// Temporarily replace
const savedRef = mockKeyStore.get.bind(mockKeyStore);
// We'll call resolveApiKey with a custom approach — since the inlined
// function calls our local getApiKeyById, let's just test by setting
// up the store to throw via a different mechanism
// Actually, let's just test the inline function directly with a mock:
async function resolveApiKeyWithThrowingDb(apiKeyId, apiKey) {
if (apiKeyId) {
try {
throw new Error("DB connection failed");
} catch {
/* fall through */
}
}
return apiKey || "sk_omniroute";
}
const result = await resolveApiKeyWithThrowingDb("key-001", "sk-fallback-key");
assert.equal(result, "sk-fallback-key");
});
it("falls back to sk_omniroute when both are null", async () => {
mockKeyStore.clear();
const result = await resolveApiKey(null, null);
assert.equal(result, "sk_omniroute");
});
it("falls back to sk_omniroute when both are undefined", async () => {
mockKeyStore.clear();
const result = await resolveApiKey(undefined, undefined);
assert.equal(result, "sk_omniroute");
});
it("prefers resolved key from apiKeyId over masked apiKey", async () => {
mockKeyStore.clear();
mockKeyStore.set("key-002", { id: "key-002", key: "sk-fullkey1234567890abcdef" });
// The masked apiKey is what /api/keys returns — should NOT be used
const maskedKey = maskApiKey("sk-fullkey1234567890abcdef");
const result = await resolveApiKey("key-002", maskedKey);
assert.equal(result, "sk-fullkey1234567890abcdef");
assert.notEqual(result, maskedKey);
});
});
describe("maskApiKey", () => {
it("masks a long key correctly", () => {
const result = maskApiKey("sk-31c4eabcd1234efgh8600");
assert.equal(result, "sk-31c4e****8600");
});
it("does not mask short keys", () => {
const result = maskApiKey("sk-short12");
assert.equal(result, "sk-short12");
});
it("handles null/undefined gracefully", () => {
assert.equal(maskApiKey(null), null);
assert.equal(maskApiKey(undefined), undefined);
});
it("produces a key that is NOT usable for auth", () => {
const fullKey = "sk-31c4eabcd1234efgh8600";
const masked = maskApiKey(fullKey);
assert.notEqual(masked, fullKey);
assert.ok(masked.includes("****"));
assert.ok(masked.startsWith(fullKey.slice(0, 8)));
assert.ok(masked.endsWith(fullKey.slice(-4)));
});
});
describe("Bug reproduction: masked key written to config", () => {
it("reproduces the original bug — masked key fails auth", () => {
const fullKey = "sk-31c4eabcd1234efgh8600";
const masked = maskApiKey(fullKey);
// Simulating what happened before the fix: dropdown used masked key as value
// and sent it directly to the backend, which wrote it to config
const writtenToConfig = masked; // BUG: masked key saved to config
// Auth with masked key would fail
assert.notEqual(writtenToConfig, fullKey);
assert.ok(writtenToConfig.includes("****"));
// This proves the bug: the config file contains "sk-31c4e****8600"
// which is NOT a valid API key and would cause 401 errors
});
it("verifies the fix — keyId resolves to full key", async () => {
mockKeyStore.clear();
const fullKey = "sk-31c4eabcd1234efgh8600";
mockKeyStore.set("key-003", { id: "key-003", key: fullKey });
// After the fix: frontend sends keyId, backend resolves full key
const resolved = await resolveApiKey("key-003", null);
assert.equal(resolved, fullKey);
assert.ok(!resolved.includes("****"));
});
it("simulates full flow: masked dropdown -> keyId -> resolved full key", async () => {
mockKeyStore.clear();
const fullKey = "sk-31c4eabcd1234efgh8600";
const keyId = "key-004";
mockKeyStore.set(keyId, { id: keyId, key: fullKey });
// Step 1: /api/keys returns masked list
const apiKeysResponse = [{ id: keyId, key: maskApiKey(fullKey) }];
// Step 2: Frontend dropdown now uses key.id as value (not key.key)
const selectedValue = apiKeysResponse[0].id; // "key-004" (was key.key before fix)
assert.equal(selectedValue, keyId);
// Step 3: Frontend sends keyId to backend
const requestBody = { keyId: selectedValue };
// Step 4: Backend resolves full key from DB
const resolvedKey = await resolveApiKey(requestBody.keyId, null);
assert.equal(resolvedKey, fullKey);
assert.ok(!resolvedKey.includes("****"));
});
it("handles prefix/suffix matching for restoring saved key from file", () => {
const fullKey = "sk-31c4eabcd1234efgh8600";
const masked = maskApiKey(fullKey);
// Simulates what ClaudeToolCard does when reading a key from file:
// The file contains the full key, and we match against the masked list
const fileKeyPrefix = fullKey.slice(0, 8); // "sk-31c4e"
const fileKeySuffix = fullKey.slice(-4); // "8600"
const apiKeysResponse = [{ id: "key-005", key: masked }];
// Match by prefix/suffix
const matchedKey = apiKeysResponse.find(
(k) => k.key && k.key.startsWith(fileKeyPrefix) && k.key.endsWith(fileKeySuffix)
);
assert.ok(matchedKey);
assert.equal(matchedKey.id, "key-005");
});
});
+645
View File
@@ -0,0 +1,645 @@
/**
* Unit tests for API key policy helpers:
* - parseIsActive (via parseAccessSchedule indirect coverage)
* - isWithinSchedule logic (tested directly via a re-export or by mocking Date)
*
* Because isWithinSchedule is module-private, we test it through observable
* behavior: feeding real Date overrides via globalThis.Date stubbing.
*
* Strategy:
* 1. Extract the schedule-check logic into a standalone helper exported only
* for tests — OR test it end-to-end through enforceApiKeyPolicy.
* 2. Since enforceApiKeyPolicy needs a full DB + HTTP Request, we isolate
* isWithinSchedule by copying its logic into this test file and verifying
* the exact same algorithm.
*/
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 { pathToFileURL } from "node:url";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-api-key-policy-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "task-607-api-key-secret";
const coreDb = await import("../../src/lib/db/core.ts");
const apiKeysDb = await import("../../src/lib/db/apiKeys.ts");
const combosDb = await import("../../src/lib/db/combos.ts");
const modelComboMappingsDb = await import("../../src/lib/db/modelComboMappings.ts");
const costRules = await import("../../src/domain/costRules.ts");
const rateLimiter = await import("../../src/shared/utils/rateLimiter.ts");
rateLimiter.setRateLimiterTestMode(true);
function getFsErrorCode(error: unknown): string | undefined {
if (typeof error !== "object" || error === null || !("code" in error)) return undefined;
const { code } = error as { code?: unknown };
return typeof code === "string" ? code : undefined;
}
async function resetStorage() {
apiKeysDb.resetApiKeyState();
costRules.resetCostData();
coreDb.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 = getFsErrorCode(error);
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 });
}
async function loadPolicy(label) {
const modulePath = path.join(process.cwd(), "src/shared/utils/apiKeyPolicy.ts");
return import(`${pathToFileURL(modulePath).href}?case=${label}-${Date.now()}`);
}
async function createKeyWithPolicy(update = {}) {
const created = await apiKeysDb.createApiKey("Policy Key", "machine-607");
if (Object.keys(update).length > 0) {
await apiKeysDb.updateApiKeyPermissions(created.id, update);
}
return created;
}
function makePolicyRequest(apiKey) {
return new Request("http://localhost/api/v1/chat/completions", {
method: "POST",
headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : {},
});
}
function makeAnthropicPolicyRequest(apiKey) {
return new Request("http://localhost/v1/messages", {
method: "POST",
headers: apiKey
? {
"x-api-key": apiKey,
"anthropic-version": "2023-06-01",
}
: { "anthropic-version": "2023-06-01" },
});
}
async function readErrorMessage(response) {
const body = (await response.json()) as { error?: { message?: unknown } };
return typeof body.error?.message === "string" ? body.error.message : "";
}
function getCurrentUtcDay() {
const dayName = new Intl.DateTimeFormat("en-US", {
timeZone: "UTC",
weekday: "short",
}).format(new Date());
return { Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6 }[dayName];
}
test.beforeEach(async () => {
delete process.env.DEFAULT_RATE_LIMIT_PER_DAY;
await resetStorage();
});
test.after(async () => {
apiKeysDb.resetApiKeyState();
costRules.resetCostData();
coreDb.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
// ─── Replicate the isWithinSchedule logic for pure unit testing ───────────────
//
// This mirrors the implementation in apiKeyPolicy.ts exactly.
// If the production code changes, update this copy too.
/**
* @param {{ enabled: boolean; from: string; until: string; days: number[]; tz: string }} schedule
* @param {Date} now — injectable "current time"
* @returns {boolean}
*/
function isWithinSchedule(schedule, now = new Date()) {
if (!schedule.enabled) return true;
let localTimeStr;
try {
localTimeStr = new Intl.DateTimeFormat("en-US", {
timeZone: schedule.tz,
hour: "2-digit",
minute: "2-digit",
hour12: false,
}).format(now);
} catch {
return true;
}
const normalizedTime = localTimeStr.replace(/^24:/, "00:");
const [localHour, localMin] = normalizedTime.split(":").map(Number);
const localMinutes = localHour * 60 + localMin;
let localDayStr;
try {
localDayStr = new Intl.DateTimeFormat("en-US", {
timeZone: schedule.tz,
weekday: "short",
}).format(now);
} catch {
return true;
}
const dayMap = { Sun: 0, Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6 };
const localDay = dayMap[localDayStr] ?? now.getDay();
if (!schedule.days.includes(localDay)) return false;
const [fromHour, fromMin] = schedule.from.split(":").map(Number);
const [untilHour, untilMin] = schedule.until.split(":").map(Number);
const fromMinutes = fromHour * 60 + fromMin;
const untilMinutes = untilHour * 60 + untilMin;
if (untilMinutes < fromMinutes) {
return localMinutes >= fromMinutes || localMinutes < untilMinutes;
}
return localMinutes >= fromMinutes && localMinutes < untilMinutes;
}
// ─── parseIsActive helper (mirrors production code) ──────────────────────────
function parseIsActive(value) {
if (value === 0 || value === "0" || value === false) return false;
return true;
}
// ─── parseAccessSchedule helper (mirrors production code) ────────────────────
function parseAccessSchedule(value) {
if (!value || typeof value !== "string" || value.trim() === "") return null;
try {
const parsed = JSON.parse(value);
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
if (
typeof parsed.enabled !== "boolean" ||
typeof parsed.from !== "string" ||
typeof parsed.until !== "string" ||
!Array.isArray(parsed.days) ||
typeof parsed.tz !== "string"
)
return null;
const days = parsed.days.filter(
(d) => typeof d === "number" && Number.isInteger(d) && d >= 0 && d <= 6
);
return { enabled: parsed.enabled, from: parsed.from, until: parsed.until, days, tz: parsed.tz };
} catch {
return null;
}
}
// ─── parseIsActive ────────────────────────────────────────────────────────────
test("parseIsActive: undefined → true (default active)", () => {
assert.equal(parseIsActive(undefined), true);
});
test("parseIsActive: null → true", () => {
assert.equal(parseIsActive(null), true);
});
test("parseIsActive: 1 → true", () => {
assert.equal(parseIsActive(1), true);
});
test("parseIsActive: true → true", () => {
assert.equal(parseIsActive(true), true);
});
test("parseIsActive: 0 → false", () => {
assert.equal(parseIsActive(0), false);
});
test("parseIsActive: false → false", () => {
assert.equal(parseIsActive(false), false);
});
test("parseIsActive: '0' → false", () => {
assert.equal(parseIsActive("0"), false);
});
// ─── parseAccessSchedule ──────────────────────────────────────────────────────
test("parseAccessSchedule: null/empty → null", () => {
assert.equal(parseAccessSchedule(null), null);
assert.equal(parseAccessSchedule(""), null);
assert.equal(parseAccessSchedule(" "), null);
});
test("parseAccessSchedule: valid JSON → object", () => {
const input = JSON.stringify({
enabled: true,
from: "08:00",
until: "18:00",
days: [1, 2, 3, 4, 5],
tz: "America/Sao_Paulo",
});
const result = parseAccessSchedule(input);
assert.deepEqual(result, {
enabled: true,
from: "08:00",
until: "18:00",
days: [1, 2, 3, 4, 5],
tz: "America/Sao_Paulo",
});
});
test("parseAccessSchedule: invalid day values are filtered out", () => {
const input = JSON.stringify({
enabled: true,
from: "08:00",
until: "18:00",
days: [1, 7, -1, 5],
tz: "UTC",
});
const result = parseAccessSchedule(input);
assert.deepEqual(result.days, [1, 5]);
});
test("parseAccessSchedule: missing required field → null", () => {
const input = JSON.stringify({ enabled: true, from: "08:00", until: "18:00", days: [1] });
assert.equal(parseAccessSchedule(input), null); // tz missing
});
test("parseAccessSchedule: invalid JSON → null", () => {
assert.equal(parseAccessSchedule("{broken json}"), null);
});
// ─── isWithinSchedule ────────────────────────────────────────────────────────
// Helper: create a Date at a specific UTC datetime
function utc(y, m, d, h, min) {
return new Date(Date.UTC(y, m - 1, d, h, min));
}
test("isWithinSchedule: enabled=false → always true", () => {
const schedule = { enabled: false, from: "00:00", until: "00:01", days: [1], tz: "UTC" };
// Even a time that would be blocked
assert.equal(isWithinSchedule(schedule, utc(2024, 3, 11, 12, 0)), true);
});
test("isWithinSchedule: time within window → true", () => {
// Monday 2024-03-11, 09:00 UTC (UTC timezone)
const schedule = { enabled: true, from: "08:00", until: "18:00", days: [1], tz: "UTC" };
assert.equal(isWithinSchedule(schedule, utc(2024, 3, 11, 9, 0)), true);
});
test("isWithinSchedule: time before window → false", () => {
const schedule = { enabled: true, from: "08:00", until: "18:00", days: [1], tz: "UTC" };
// 07:59
assert.equal(isWithinSchedule(schedule, utc(2024, 3, 11, 7, 59)), false);
});
test("isWithinSchedule: time exactly at 'from' → true (inclusive)", () => {
const schedule = { enabled: true, from: "08:00", until: "18:00", days: [1], tz: "UTC" };
assert.equal(isWithinSchedule(schedule, utc(2024, 3, 11, 8, 0)), true);
});
test("isWithinSchedule: time exactly at 'until' → false (exclusive)", () => {
const schedule = { enabled: true, from: "08:00", until: "18:00", days: [1], tz: "UTC" };
assert.equal(isWithinSchedule(schedule, utc(2024, 3, 11, 18, 0)), false);
});
test("isWithinSchedule: time after window → false", () => {
const schedule = { enabled: true, from: "08:00", until: "18:00", days: [1], tz: "UTC" };
assert.equal(isWithinSchedule(schedule, utc(2024, 3, 11, 20, 0)), false);
});
test("isWithinSchedule: wrong weekday → false", () => {
// Monday (day 1) schedule, but 2024-03-12 is Tuesday (day 2)
const schedule = { enabled: true, from: "08:00", until: "18:00", days: [1], tz: "UTC" };
assert.equal(isWithinSchedule(schedule, utc(2024, 3, 12, 10, 0)), false);
});
test("isWithinSchedule: multiple days — matching day → true", () => {
// Mon-Fri schedule, Wednesday (3)
const schedule = {
enabled: true,
from: "09:00",
until: "17:00",
days: [1, 2, 3, 4, 5],
tz: "UTC",
};
// 2024-03-13 is Wednesday
assert.equal(isWithinSchedule(schedule, utc(2024, 3, 13, 12, 0)), true);
});
test("isWithinSchedule: multiple days — Saturday blocked", () => {
const schedule = {
enabled: true,
from: "09:00",
until: "17:00",
days: [1, 2, 3, 4, 5],
tz: "UTC",
};
// 2024-03-09 is Saturday (day 6)
assert.equal(isWithinSchedule(schedule, utc(2024, 3, 9, 12, 0)), false);
});
// ─── Overnight schedule tests ─────────────────────────────────────────────────
test("isWithinSchedule: overnight window — time after midnight → true", () => {
// 22:00 → 06:00, Monday
const schedule = { enabled: true, from: "22:00", until: "06:00", days: [1], tz: "UTC" };
// 02:30 Monday
assert.equal(isWithinSchedule(schedule, utc(2024, 3, 11, 2, 30)), true);
});
test("isWithinSchedule: overnight window — time before start → false", () => {
const schedule = { enabled: true, from: "22:00", until: "06:00", days: [1], tz: "UTC" };
// 21:59 Monday
assert.equal(isWithinSchedule(schedule, utc(2024, 3, 11, 21, 59)), false);
});
test("isWithinSchedule: overnight window — time after end → false", () => {
const schedule = { enabled: true, from: "22:00", until: "06:00", days: [1], tz: "UTC" };
// 06:01 Monday
assert.equal(isWithinSchedule(schedule, utc(2024, 3, 11, 6, 1)), false);
});
test("isWithinSchedule: overnight window — time exactly at start → true", () => {
const schedule = { enabled: true, from: "22:00", until: "06:00", days: [1], tz: "UTC" };
assert.equal(isWithinSchedule(schedule, utc(2024, 3, 11, 22, 0)), true);
});
test("isWithinSchedule: invalid timezone → fail-open (true)", () => {
const schedule = {
enabled: true,
from: "08:00",
until: "18:00",
days: [1, 2, 3, 4, 5],
tz: "Invalid/Zone",
};
// Should not throw, should return true (fail-open)
assert.equal(isWithinSchedule(schedule, utc(2024, 3, 11, 12, 0)), true);
});
test("isWithinSchedule: America/Sao_Paulo timezone conversion", () => {
// UTC 2024-03-11 15:00 = BRT (UTC-3) 12:00, Monday
const schedule = {
enabled: true,
from: "08:00",
until: "18:00",
days: [1],
tz: "America/Sao_Paulo",
};
assert.equal(isWithinSchedule(schedule, utc(2024, 3, 11, 15, 0)), true);
});
test("isWithinSchedule: America/Sao_Paulo — outside window", () => {
// UTC 2024-03-11 22:00 = BRT 19:00, Monday — after 18:00
const schedule = {
enabled: true,
from: "08:00",
until: "18:00",
days: [1],
tz: "America/Sao_Paulo",
};
assert.equal(isWithinSchedule(schedule, utc(2024, 3, 11, 22, 0)), false);
});
test("enforceApiKeyPolicy bypasses local mode and unknown keys", async () => {
const policy = await loadPolicy("bypass");
assert.deepEqual(await policy.enforceApiKeyPolicy(makePolicyRequest(null), "openai/gpt-4.1"), {
apiKey: null,
apiKeyInfo: null,
rejection: null,
});
const unknown = await policy.enforceApiKeyPolicy(
makePolicyRequest("sk-unknown"),
"openai/gpt-4.1"
);
assert.equal(unknown.apiKey, "sk-unknown");
assert.equal(unknown.apiKeyInfo, null);
assert.equal(unknown.rejection, null);
});
test("enforceApiKeyPolicy rejects disabled keys and blocked schedules", async () => {
const disabledKey = await createKeyWithPolicy({ isActive: false });
const blockedDay = (getCurrentUtcDay() + 1) % 7;
const scheduledKey = await createKeyWithPolicy({
accessSchedule: {
enabled: true,
from: "00:00",
until: "23:59",
days: [blockedDay],
tz: "UTC",
},
});
const policy = await loadPolicy("disabled-and-schedule");
const disabled = await policy.enforceApiKeyPolicy(makePolicyRequest(disabledKey.key), null);
assert.equal(disabled.rejection.status, 403);
assert.equal(await readErrorMessage(disabled.rejection), "This API key is disabled");
const blocked = await policy.enforceApiKeyPolicy(makePolicyRequest(scheduledKey.key), null);
assert.equal(blocked.rejection.status, 403);
assert.match(await readErrorMessage(blocked.rejection), /Access denied outside allowed hours/);
});
test("enforceApiKeyPolicy rejects disallowed models and exhausted budgets", async () => {
const restrictedKey = await createKeyWithPolicy({
allowedModels: ["openai/gpt-4.1"],
});
const budgetedKey = await createKeyWithPolicy();
const policy = await loadPolicy("model-and-budget");
const disallowed = await policy.enforceApiKeyPolicy(
makePolicyRequest(restrictedKey.key),
"anthropic/claude-3-7-sonnet"
);
assert.equal(disallowed.rejection.status, 403);
assert.match(await readErrorMessage(disallowed.rejection), /not allowed/);
const budgetMeta = await apiKeysDb.getApiKeyMetadata(budgetedKey.key);
costRules.setBudget(budgetMeta.id, { dailyLimitUsd: 1, warningThreshold: 0.5 });
costRules.recordCost(budgetMeta.id, 2);
const overBudget = await policy.enforceApiKeyPolicy(
makePolicyRequest(budgetedKey.key),
"openai/gpt-4.1"
);
assert.equal(overBudget.rejection.status, 429);
assert.match(await readErrorMessage(overBudget.rejection), /Daily budget exceeded/);
});
test("enforceApiKeyPolicy returns Anthropic error envelope for /v1/messages model denials", async () => {
const restrictedKey = await createKeyWithPolicy({
allowedModels: ["cc/*"],
blockedModels: ["claude-fable*", "fable"],
});
const policy = await loadPolicy("anthropic-model-denial");
const denied = await policy.enforceApiKeyPolicy(
makeAnthropicPolicyRequest(restrictedKey.key),
"claude-fable-5"
);
assert.equal(denied.rejection.status, 400);
const body = await denied.rejection.json();
assert.equal(body.type, "error");
assert.equal(body.error.type, "invalid_request_error");
assert.match(body.error.message, /claude-fable-5/);
assert.equal(
body.error.message,
'Model "claude-fable-5" is not enabled or quota is insufficient. Choose another allowed model.'
);
assert.doesNotMatch(body.error.message, /login|authenticate|api key|credential|omniroute/i);
assert.equal(body.error.code, undefined);
});
test("enforceApiKeyPolicy does not rate-limit unrestricted keys by default", async () => {
const unrestrictedKey = await createKeyWithPolicy({ allowedModels: ["openai/*"] });
const policy = await loadPolicy("default-no-request-limit");
for (let i = 0; i < 1005; i += 1) {
const result = await policy.enforceApiKeyPolicy(
makePolicyRequest(unrestrictedKey.key),
"openai/gpt-4.1"
);
assert.equal(result.rejection, null);
}
});
test("enforceApiKeyPolicy enforces explicit env fallback request limits", async () => {
process.env.DEFAULT_RATE_LIMIT_PER_DAY = "1";
const unrestrictedKey = await createKeyWithPolicy({ allowedModels: ["openai/*"] });
const policy = await loadPolicy("env-request-limit");
const first = await policy.enforceApiKeyPolicy(
makePolicyRequest(unrestrictedKey.key),
"openai/gpt-4.1"
);
assert.equal(first.rejection, null);
const second = await policy.enforceApiKeyPolicy(
makePolicyRequest(unrestrictedKey.key),
"openai/gpt-4.1"
);
assert.equal(second.rejection.status, 429);
assert.match(await readErrorMessage(second.rejection), /Request limit exceeded/);
});
test("enforceApiKeyPolicy enforces custom multi-window rate limits", async () => {
const limitedKey = await createKeyWithPolicy({
allowedModels: ["openai/*"],
rateLimits: [{ limit: 1, window: 60 }],
});
const policy = await loadPolicy("custom-request-limits");
const first = await policy.enforceApiKeyPolicy(
makePolicyRequest(limitedKey.key),
"openai/gpt-4.1"
);
assert.equal(first.rejection, null);
const second = await policy.enforceApiKeyPolicy(
makePolicyRequest(limitedKey.key),
"openai/gpt-4.1"
);
assert.equal(second.rejection.status, 429);
assert.match(await readErrorMessage(second.rejection), /Request limit exceeded/);
});
test("enforceApiKeyPolicy enforces combo allowlists separately from model allowlists", async () => {
const allowedKey = await createKeyWithPolicy({
allowedModels: ["openai/*"],
allowedCombos: ["fast-chat", "mapped-chat"],
});
const blockedKey = await createKeyWithPolicy({
allowedCombos: ["slow-chat"],
});
await combosDb.createCombo({
name: "fast-chat",
strategy: "priority",
models: ["anthropic/claude-3-5-sonnet"],
});
await combosDb.createCombo({
name: "mapped-chat",
strategy: "priority",
models: ["openai/gpt-4.1"],
});
const mappedCombo = await combosDb.getComboByName("mapped-chat");
assert.ok(mappedCombo?.id);
await modelComboMappingsDb.createModelComboMapping({
pattern: "mapped-model-*",
comboId: mappedCombo.id as string,
});
const policy = await loadPolicy("combo-access");
const allowed = await policy.enforceApiKeyPolicy(
makePolicyRequest(allowedKey.key),
"combo/fast-chat"
);
assert.equal(allowed.rejection, null);
const blocked = await policy.enforceApiKeyPolicy(
makePolicyRequest(blockedKey.key),
"combo/fast-chat"
);
assert.equal(blocked.rejection.status, 403);
assert.match(await readErrorMessage(blocked.rejection), /Combo "fast-chat" is not allowed/);
const mapped = await policy.enforceApiKeyPolicy(
makePolicyRequest(allowedKey.key),
"mapped-model-1"
);
assert.equal(mapped.rejection, null);
});
test("enforceApiKeyPolicy applies configured throttle delay", async () => {
const delayedKey = await createKeyWithPolicy({ throttleDelayMs: 25 });
const policy = await loadPolicy("throttle-delay");
const startedAt = Date.now();
const result = await policy.enforceApiKeyPolicy(
makePolicyRequest(delayedKey.key),
"openai/gpt-4.1"
);
assert.equal(result.rejection, null);
assert.equal(result.apiKeyInfo.throttleDelayMs, 25);
assert.ok(Date.now() - startedAt >= 20);
});
test("enforceApiKeyPolicy enforces request-per-minute limits and returns success when allowed", async () => {
const limitedKey = await createKeyWithPolicy({
allowedModels: ["openai/*"],
maxRequestsPerMinute: 1,
});
const policy = await loadPolicy("request-limits");
const first = await policy.enforceApiKeyPolicy(
makePolicyRequest(limitedKey.key),
"openai/gpt-4.1"
);
assert.equal(first.rejection, null);
assert.equal(first.apiKeyInfo.maxRequestsPerMinute, 1);
const second = await policy.enforceApiKeyPolicy(
makePolicyRequest(limitedKey.key),
"openai/gpt-4.1"
);
assert.equal(second.rejection.status, 429);
assert.match(await readErrorMessage(second.rejection), /Request limit exceeded/);
});
@@ -0,0 +1,39 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import {
API_KEY_BYPASS_PROVIDER_QUOTA_SCOPE,
hasProviderQuotaBypassScope,
} from "../../src/shared/constants/apiKeyPolicyScopes.ts";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
test("provider quota bypass scope helper is explicit and strict", () => {
assert.equal(API_KEY_BYPASS_PROVIDER_QUOTA_SCOPE, "policy:bypass-provider-quota");
assert.equal(hasProviderQuotaBypassScope([API_KEY_BYPASS_PROVIDER_QUOTA_SCOPE]), true);
assert.equal(hasProviderQuotaBypassScope(["policy:other"]), false);
assert.equal(hasProviderQuotaBypassScope(null), false);
});
test("chat handler maps API key provider quota bypass scope to auth bypass option", () => {
const source = fs.readFileSync(path.join(repoRoot, "src/sse/handlers/chat.ts"), "utf8");
assert.match(source, /hasProviderQuotaBypassScope\(apiKeyInfo\?\.scopes\)/);
assert.match(source, /bypassProviderQuotaPolicy[\s\S]*bypassQuotaPolicy: true/);
assert.match(source, /relayOptions[\s\S]*bypassProviderQuotaPolicy: true/);
});
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"
);
assert.match(source, /relayOptions\?\.bypassProviderQuotaPolicy === true/);
assert.match(source, /quotaPreflight:[\s\S]*enabled: false/);
});
+62
View File
@@ -0,0 +1,62 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omr-apikey-regen-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.API_KEY_SECRET = "test-secret-regen";
const core = await import("../../src/lib/db/core.ts");
const apiKeysDb = await import("../../src/lib/db/apiKeys.ts");
function reset() {
core.resetDbInstance();
apiKeysDb.resetApiKeyState();
if (fs.existsSync(TEST_DATA_DIR)) {
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
}
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}
test.beforeEach(() => {
reset();
});
test.after(() => {
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
test("regenerateApiKey creates a new key and invalidates the old one", async () => {
const machineId = "test-machine-regen";
const created = await apiKeysDb.createApiKey("Regen Test", machineId);
const oldKey = created.key;
const oldId = created.id;
assert.ok(oldKey);
assert.equal(await apiKeysDb.validateApiKey(oldKey), true);
// Regenerate
const result = await apiKeysDb.regenerateApiKey(oldId);
assert.ok(result?.key);
const regenerated = result!.key;
assert.notEqual(regenerated, oldKey);
// New key should be valid
assert.equal(await apiKeysDb.validateApiKey(regenerated), true);
// Old key should be invalid
assert.equal(await apiKeysDb.validateApiKey(oldKey), false);
// Name and machineId should persist
const md = await apiKeysDb.getApiKeyMetadata(regenerated);
assert.equal(md?.name, "Regen Test");
assert.ok(regenerated.startsWith(`sk-${machineId}-`));
});
test("regenerateApiKey returns null for non-existent ID", async () => {
const result = await apiKeysDb.regenerateApiKey("00000000-0000-0000-0000-000000000000");
assert.equal(result, null);
});
+166
View File
@@ -0,0 +1,166 @@
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-api-key-reveal-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.API_KEY_SECRET = "test-api-key-secret";
const core = await import("../../src/lib/db/core.ts");
const apiKeysDb = await import("../../src/lib/db/apiKeys.ts");
const featureFlagsDb = await import("../../src/lib/db/featureFlags.ts");
const listRoute = await import("../../src/app/api/keys/route.ts");
const revealRoute = await import("../../src/app/api/keys/[id]/reveal/route.ts");
const MACHINE_ID = "1234567890abcdef";
async function resetStorage() {
delete process.env.ALLOW_API_KEY_REVEAL;
delete process.env.INITIAL_PASSWORD;
core.resetDbInstance();
apiKeysDb.resetApiKeyState();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}
function maskKey(key) {
return key.slice(0, 8) + "****" + key.slice(-4);
}
test.beforeEach(async () => {
await resetStorage();
});
test.after(async () => {
delete process.env.ALLOW_API_KEY_REVEAL;
core.resetDbInstance();
apiKeysDb.resetApiKeyState();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
test("GET /api/keys stays masked even when reveal is enabled", async () => {
process.env.ALLOW_API_KEY_REVEAL = "true";
const created = await apiKeysDb.createApiKey("Primary Key", MACHINE_ID);
const response = await listRoute.GET(new Request("http://localhost/api/keys"));
const body = (await response.json()) as any;
assert.equal(response.status, 200);
assert.equal(body.allowKeyReveal, true);
assert.equal(Array.isArray(body.keys), true);
assert.equal(body.keys[0].key, maskKey(created.key));
});
test("GET /api/keys falls back to default pagination for invalid query params", async () => {
await apiKeysDb.createApiKey("Alpha", MACHINE_ID);
await apiKeysDb.createApiKey("Beta", MACHINE_ID);
const response = await listRoute.GET(
new Request("http://localhost/api/keys?limit=abc&offset=xyz")
);
const body = (await response.json()) as any;
assert.equal(response.status, 200);
assert.equal(body.allowKeyReveal, false);
assert.equal(body.total, 2);
assert.equal(body.keys.length, 2);
assert.equal(
body.keys.every((entry) => entry.key.includes("****")),
true
);
});
test("GET /api/keys returns 500 when key loading fails unexpectedly", async () => {
await apiKeysDb.createApiKey("Primary Key", MACHINE_ID);
const db = core.getDbInstance();
const originalPrepare = db.prepare.bind(db);
db.prepare = (sql, ...args) => {
if (typeof sql === "string" && sql.includes("SELECT * FROM api_keys")) {
throw new Error("db exploded");
}
return originalPrepare(sql, ...args);
};
apiKeysDb.resetApiKeyState();
try {
const response = await listRoute.GET(new Request("http://localhost/api/keys"));
const body = (await response.json()) as any;
assert.equal(response.status, 500);
assert.equal(body.error, "Failed to fetch keys");
} finally {
db.prepare = originalPrepare;
apiKeysDb.resetApiKeyState();
}
});
test("GET /api/keys/[id]/reveal rejects requests when reveal is disabled", async () => {
const created = await apiKeysDb.createApiKey("Primary Key", MACHINE_ID);
const request = new Request(`http://localhost/api/keys/${created.id}/reveal`);
const response = await revealRoute.GET(request, {
params: Promise.resolve({ id: created.id }),
});
const body = (await response.json()) as any;
assert.equal(response.status, 403);
assert.equal(body.error, "API key reveal is disabled");
});
test("GET /api/keys/[id]/reveal returns the full key when reveal is enabled", async () => {
process.env.ALLOW_API_KEY_REVEAL = "true";
const created = await apiKeysDb.createApiKey("Primary Key", MACHINE_ID);
const request = new Request(`http://localhost/api/keys/${created.id}/reveal`);
const response = await revealRoute.GET(request, {
params: Promise.resolve({ id: created.id }),
});
const body = (await response.json()) as any;
assert.equal(response.status, 200);
assert.equal(body.key, created.key);
});
test("GET /api/keys/[id]/reveal honors the ALLOW_API_KEY_REVEAL feature flag override", async () => {
featureFlagsDb.setFeatureFlagOverride("ALLOW_API_KEY_REVEAL", "true");
const created = await apiKeysDb.createApiKey("Primary Key", MACHINE_ID);
const request = new Request(`http://localhost/api/keys/${created.id}/reveal`);
const response = await revealRoute.GET(request, {
params: Promise.resolve({ id: created.id }),
});
const body = (await response.json()) as any;
assert.equal(response.status, 200);
assert.equal(body.key, created.key);
});
test("GET /api/keys/[id]/reveal returns 404 for unknown keys even when reveal is enabled", async () => {
process.env.ALLOW_API_KEY_REVEAL = "true";
const request = new Request("http://localhost/api/keys/missing/reveal");
const response = await revealRoute.GET(request, {
params: Promise.resolve({ id: "missing" }),
});
const body = (await response.json()) as any;
assert.equal(response.status, 404);
assert.equal(body.error, "Key not found");
});
test("GET /api/keys/[id]/reveal returns 500 when params resolution fails", async () => {
process.env.ALLOW_API_KEY_REVEAL = "true";
const request = new Request("http://localhost/api/keys/broken/reveal");
const response = await revealRoute.GET(request, {
params: Promise.reject(new Error("params exploded")),
});
const body = (await response.json()) as any;
assert.equal(response.status, 500);
assert.equal(body.error, "Failed to reveal key");
});
+665
View File
@@ -0,0 +1,665 @@
/**
* apiKeyRotator 健康状态追踪测试
*
* 测试 T07 功能:API Key 健康状态追踪
* 当 API Key 连续认证失败 3 次后,应被标记为 "invalid" 并在轮换时自动跳过
*/
import { describe, it, beforeEach, afterEach } from "node:test";
import assert from "node:assert/strict";
import {
getValidApiKey,
recordKeyFailure,
recordKeySuccess,
connectionHasExtraKeys,
trackConnectionExtraKeys,
getInvalidKeyCount,
resetKeyStatus,
getAllKeyHealth,
syncHealthFromDB,
type KeyHealth,
} from "../../open-sse/services/apiKeyRotator.ts";
// Helper to reset module state between tests
function resetModuleState() {
// Clear all internal maps by re-importing or using exported reset functions
// Since apiKeyRotator doesn't export a clearAll function, we use resetKeyStatus
const allHealth = getAllKeyHealth();
for (const scopedKey of Object.keys(allHealth)) {
// scopedKey format is "connectionId:keyId"
const keyId = scopedKey.includes(":") ? scopedKey.split(":")[1] : scopedKey;
resetKeyStatus("test-conn", keyId);
}
}
describe("apiKeyRotator Health Tracking", () => {
beforeEach(() => {
resetModuleState();
});
afterEach(() => {
resetModuleState();
});
describe("getValidApiKey", () => {
it("should return primary key when no extra keys", () => {
const connectionId = "test-conn-1";
const primaryKey = "pk-test-123";
const result = getValidApiKey(connectionId, primaryKey, []);
assert.equal(result?.key, primaryKey);
assert.equal(result?.keyId, "primary");
});
it("should return null when no keys available", () => {
const connectionId = "test-conn-2";
const result = getValidApiKey(connectionId, "", []);
assert.equal(result, null);
});
it("should return object with key and keyId when a valid key is found", () => {
const connectionId = "test-conn-keyid";
const primaryKey = "pk-keyid-test";
const result = getValidApiKey(connectionId, primaryKey, []);
assert.ok(result, "should return an object");
assert.equal(typeof result!.key, "string");
assert.equal(typeof result!.keyId, "string");
assert.equal(result!.key, primaryKey);
assert.equal(result!.keyId, "primary");
});
it("should skip invalid primary key and return first extra key", () => {
const connectionId = "test-conn-3";
const primaryKey = "pk-invalid";
const extraKeys = ["extra-1", "extra-2"];
const health: Record<string, KeyHealth> = {
primary: {
status: "invalid",
failures: 3,
lastFailure: null,
lastSuccess: null,
totalRequests: 5,
totalFailures: 3,
},
};
const result = getValidApiKey(connectionId, primaryKey, extraKeys, health);
assert.equal(result?.key, "extra-1");
assert.equal(result?.keyId, "extra_0");
});
it("should skip all invalid keys and return null", () => {
const connectionId = "test-conn-4";
const primaryKey = "pk-invalid";
const extraKeys = ["extra-invalid"];
const health: Record<string, KeyHealth> = {
primary: {
status: "invalid",
failures: 3,
lastFailure: null,
lastSuccess: null,
totalRequests: 5,
totalFailures: 3,
},
extra_0: {
status: "invalid",
failures: 3,
lastFailure: null,
lastSuccess: null,
totalRequests: 3,
totalFailures: 3,
},
};
const result = getValidApiKey(connectionId, primaryKey, extraKeys, health);
assert.equal(result, null);
});
it("should return keyId in result", () => {
const connectionId = "test-conn-5";
const primaryKey = "pk-test";
const extraKeys = ["extra-1", "extra-2"];
const result = getValidApiKey(connectionId, primaryKey, extraKeys);
assert.ok(result, "should have a result");
assert.ok(["primary", "extra_0", "extra_1"].includes(result!.keyId), "keyId should be valid");
});
it("should round-robin among valid keys only (skipping invalid)", () => {
const connectionId = "test-conn-6";
const primaryKey = "pk-test";
const extraKeys = ["extra-1", "extra-2", "extra-3"];
const health: Record<string, KeyHealth> = {
extra_1: {
status: "invalid",
failures: 3,
lastFailure: null,
lastSuccess: null,
totalRequests: 3,
totalFailures: 3,
}, // extra-2 is index 1
};
// First call
const key1 = getValidApiKey(connectionId, primaryKey, extraKeys, health);
const key2 = getValidApiKey(connectionId, primaryKey, extraKeys, health);
const key3 = getValidApiKey(connectionId, primaryKey, extraKeys, health);
// Should only rotate among primary, extra-1 (index 0), extra-3 (index 2)
// Invalid extra_1 (which corresponds to extraKeys[1]) should be skipped
const validKeys = [primaryKey, "extra-1", "extra-3"];
assert.ok(validKeys.includes(key1!.key));
assert.ok(validKeys.includes(key2!.key));
assert.ok(validKeys.includes(key3!.key));
// extra-2 should never be selected
assert.notEqual(key1!.key, "extra-2");
assert.notEqual(key2!.key, "extra-2");
assert.notEqual(key3!.key, "extra-2");
});
});
describe("recordKeyFailure", () => {
it("should increment failure count", () => {
const health = recordKeyFailure("test-conn", "primary");
assert.equal(health.failures, 1);
assert.equal(health.status, "warning");
});
it("should mark as invalid after 3 consecutive failures", () => {
recordKeyFailure("test-conn", "primary");
recordKeyFailure("test-conn", "primary");
const health = recordKeyFailure("test-conn", "primary");
assert.equal(health.failures, 3);
assert.equal(health.status, "invalid");
});
it("should track totalRequests and totalFailures", () => {
const h1 = recordKeyFailure("test-conn", "test-key-1");
assert.equal(h1.totalRequests, 1);
assert.equal(h1.totalFailures, 1);
const h2 = recordKeyFailure("test-conn", "test-key-1");
assert.equal(h2.totalRequests, 2);
assert.equal(h2.totalFailures, 2);
});
it("should set lastFailure timestamp", () => {
const health = recordKeyFailure("test-conn", "primary");
assert.ok(health.lastFailure, "lastFailure should be set");
assert.ok(new Date(health.lastFailure!).getTime() > 0, "lastFailure should be valid date");
});
});
describe("recordKeySuccess", () => {
it("should reset failure count and mark as active", () => {
// First record some failures
recordKeyFailure("test-conn", "primary");
recordKeyFailure("test-conn", "primary");
// Then record success
const health = recordKeySuccess("test-conn", "primary");
assert.equal(health.failures, 0);
assert.equal(health.status, "active");
});
it("should recover from warning status", () => {
recordKeyFailure("test-conn", "primary"); // 1 failure -> warning
const afterSuccess = recordKeySuccess("test-conn", "primary");
assert.equal(afterSuccess.status, "active");
assert.equal(afterSuccess.failures, 0);
});
it("should recover from invalid status", () => {
recordKeyFailure("test-conn", "primary");
recordKeyFailure("test-conn", "primary");
recordKeyFailure("test-conn", "primary"); // 3 failures -> invalid
const afterSuccess = recordKeySuccess("test-conn", "primary");
assert.equal(afterSuccess.status, "active");
assert.equal(afterSuccess.failures, 0);
});
});
describe("getInvalidKeyCount", () => {
it("should return 0 when no invalid keys", () => {
const count = getInvalidKeyCount({});
assert.equal(count, 0);
});
it("should count invalid keys correctly", () => {
const health: Record<string, KeyHealth> = {
primary: {
status: "invalid",
failures: 3,
lastFailure: null,
lastSuccess: null,
totalRequests: 5,
totalFailures: 3,
},
extra_0: {
status: "active",
failures: 0,
lastFailure: null,
lastSuccess: null,
totalRequests: 10,
totalFailures: 0,
},
extra_1: {
status: "invalid",
failures: 3,
lastFailure: null,
lastSuccess: null,
totalRequests: 3,
totalFailures: 3,
},
};
const count = getInvalidKeyCount(health);
assert.equal(count, 2);
});
});
describe("trackConnectionExtraKeys", () => {
it("should track connection has extra keys", () => {
trackConnectionExtraKeys("conn-with-extras", ["key1", "key2"]);
assert.equal(connectionHasExtraKeys("conn-with-extras"), true);
});
it("should return false for connection without extra keys", () => {
trackConnectionExtraKeys("conn-no-extras", []);
assert.equal(connectionHasExtraKeys("conn-no-extras"), false);
});
it("should return false for unknown connection", () => {
assert.equal(connectionHasExtraKeys("unknown-conn"), false);
});
});
describe("syncHealthFromDB", () => {
it("should sync health status from DB to in-memory map", () => {
const connectionId = "test-sync-conn";
const health: Record<string, KeyHealth> = {
primary: {
status: "invalid",
failures: 3,
lastFailure: "2026-01-01T00:00:00Z",
lastSuccess: null,
totalRequests: 10,
totalFailures: 5,
},
};
syncHealthFromDB(connectionId, health);
// After sync, getAllKeyHealth should include the synced data
// Note: syncHealthFromDB uses `${connectionId}:${keyId}` as the key
const allHealth = getAllKeyHealth();
const syncedKey = `${connectionId}:primary`;
// This test will FAIL because syncHealthFromDB stores with connectionId prefix
// but getValidApiKey reads from health parameter without connectionId prefix
// This is a BUG - the key namespace mismatch
if (allHealth[syncedKey]) {
assert.equal(allHealth[syncedKey].status, "invalid");
}
});
});
describe("Key isolation per connection", () => {
it("should isolate health state per connection", () => {
// Connection A has primary key that failed
const connA = "conn-A";
const connB = "conn-B";
const primaryKeyA = "pk-A";
const primaryKeyB = "pk-B";
// Record failure for connection A's primary key
recordKeyFailure(connA, "primary");
recordKeyFailure(connA, "primary");
recordKeyFailure(connA, "primary"); // 3 failures -> invalid for connA only
// Connection B should NOT be affected - health is isolated
const keyA = getValidApiKey(connA, primaryKeyA, []);
const keyB = getValidApiKey(connB, primaryKeyB, []);
// keyA should be null (primary is invalid, no extra keys)
assert.equal(keyA, null);
// keyB should work (no failures recorded for connB)
assert.equal(keyB?.key, primaryKeyB);
});
it("should return correct keyId per connection", () => {
const connA = "conn-A";
const connB = "conn-B";
const resultA = getValidApiKey(connA, "pk-A", ["extra-A"]);
const resultB = getValidApiKey(connB, "pk-B", ["extra-B"]);
// Both should have a valid keyId
assert.ok(resultA, "connA should have a result");
assert.ok(resultB, "connB should have a result");
assert.ok(resultA!.keyId, "connA should have keyId");
assert.ok(resultB!.keyId, "connB should have keyId");
// keyId is per-connection scoped
});
});
});
describe("Integration: 401 handling should skip key not connection", () => {
it("should track extra keys for A3 guard", () => {
const connectionId = "conn-with-backup";
const extraKeys = ["backup-key-1", "backup-key-2"];
trackConnectionExtraKeys(connectionId, extraKeys);
assert.equal(connectionHasExtraKeys(connectionId), true);
});
it("should not mark connection unavailable when extra keys exist (A3 guard)", () => {
// This test documents the expected behavior:
// When 401 occurs and connection has extra keys,
// the system should:
// 1. Record failure against the specific key
// 2. Skip that key in future rotation
// 3. NOT disable the entire connection
const connectionId = "test-a3-guard";
const primaryKey = "pk-failing";
const extraKeys = ["extra-1-valid", "extra-2-valid"];
// Track that this connection has extra keys
trackConnectionExtraKeys(connectionId, extraKeys);
// Record 3 failures on primary key
recordKeyFailure(connectionId, "primary");
recordKeyFailure(connectionId, "primary");
const finalHealth = recordKeyFailure(connectionId, "primary");
// Primary key should now be invalid
assert.equal(finalHealth.status, "invalid");
// Build health state
const health: Record<string, KeyHealth> = {
primary: finalHealth,
};
// getValidApiKey should now skip primary and return an extra key
const nextKey = getValidApiKey(connectionId, primaryKey, extraKeys, health);
assert.ok(nextKey, "should return an extra key");
assert.notEqual(nextKey!.key, primaryKey, "should NOT return primary (invalid)");
assert.ok(extraKeys.includes(nextKey!.key), "should return one of the extra keys");
// The connection should still be usable because extra keys are available
// This is the core behavior the A3 guard should enable
});
});
describe("E2E: Complete 401 flow simulation", () => {
beforeEach(() => {
resetModuleState();
});
afterEach(() => {
resetModuleState();
});
it("should simulate full 401 failure → key rotation → success flow", () => {
const connectionId = "e2e-test-conn";
const primaryKey = "pk-primary";
const extraKeys = ["extra-key-1", "extra-key-2"];
// Step 1: Initial state - track extra keys
trackConnectionExtraKeys(connectionId, extraKeys);
assert.equal(connectionHasExtraKeys(connectionId), true);
// Step 2: First request - should get primary key
const key1 = getValidApiKey(connectionId, primaryKey, extraKeys);
assert.equal(key1?.key, primaryKey);
assert.equal(key1?.keyId, "primary");
// Step 3: Simulate 401 failure on primary key
const health1 = recordKeyFailure(connectionId, "primary");
assert.equal(health1.status, "warning");
assert.equal(health1.failures, 1);
// Step 4: Retry - should get a valid key (primary is still active, but round-robin may pick extra)
const key2 = getValidApiKey(connectionId, primaryKey, extraKeys, { primary: health1 });
assert.ok(
key2!.key === primaryKey || extraKeys.includes(key2!.key),
"should return a valid key"
);
// Step 5: Second 401 failure
const health2 = recordKeyFailure(connectionId, "primary");
assert.equal(health2.failures, 2);
assert.equal(health2.status, "invalid");
// Step 6: Third 401 failure - now primary becomes invalid
const health3 = recordKeyFailure(connectionId, "primary");
assert.equal(health3.failures, 3);
assert.equal(health3.status, "invalid");
// Step 7: Next request - should skip invalid primary and return extra key
const healthState = { primary: health3 };
const key3 = getValidApiKey(connectionId, primaryKey, extraKeys, healthState);
assert.ok(key3, "should return a key");
assert.notEqual(key3!.key, primaryKey, "should NOT return invalid primary key");
assert.ok(extraKeys.includes(key3!.key), "should return an extra key");
// Step 8: Verify the invalid key count
assert.equal(getInvalidKeyCount(healthState), 1);
// Step 9: A3 guard check - connection with extra keys should NOT be disabled
// This simulates what chat.ts does: check connectionHasExtraKeys before markAccountUnavailable
const shouldSkipConnectionDisable = connectionHasExtraKeys(connectionId);
assert.equal(shouldSkipConnectionDisable, true);
// Step 10: Success on extra key - mark it as successful
assert.ok(key3!.keyId.startsWith("extra_"));
const successHealth = recordKeySuccess(connectionId, key3!.keyId);
assert.equal(successHealth.status, "active");
assert.equal(successHealth.failures, 0);
// Step 11: Recover primary key
const recoveredHealth = recordKeySuccess(connectionId, "primary");
assert.equal(recoveredHealth.status, "active");
assert.equal(recoveredHealth.failures, 0);
// Step 12: Next request - can use primary again
const recoveredState = { primary: recoveredHealth };
const key4 = getValidApiKey(connectionId, primaryKey, extraKeys, recoveredState);
assert.ok(
key4!.key === primaryKey || extraKeys.includes(key4!.key),
"should rotate among valid keys"
);
});
it("should handle multiple invalid keys and still find valid ones", () => {
const connectionId = "multi-invalid-test";
const primaryKey = "pk-primary";
const extraKeys = ["extra-1", "extra-2", "extra-3"];
trackConnectionExtraKeys(connectionId, extraKeys);
// Mark primary and extra_0 as invalid
for (let i = 0; i < 3; i++) {
recordKeyFailure(connectionId, "primary");
recordKeyFailure(connectionId, "extra_0");
}
const health: Record<string, KeyHealth> = {
primary: {
status: "invalid",
failures: 3,
lastFailure: null,
lastSuccess: null,
totalRequests: 3,
totalFailures: 3,
},
extra_0: {
status: "invalid",
failures: 3,
lastFailure: null,
lastSuccess: null,
totalRequests: 3,
totalFailures: 3,
},
};
// Should still find valid keys (extra_1 or extra_2)
const validKeys: string[] = [];
for (let i = 0; i < 10; i++) {
const key = getValidApiKey(connectionId, primaryKey, extraKeys, health);
if (key) validKeys.push(key.key);
}
assert.ok(validKeys.length > 0, "should return valid keys");
// Verify no invalid keys are returned
const invalidKeys = ["pk-primary", "extra-1"];
for (const invalidKey of invalidKeys) {
assert.ok(!validKeys.includes(invalidKey), `${invalidKey} should be skipped`);
}
// Verify only valid keys (extra-2, extra-3) are returned
const validSet = new Set(["extra-2", "extra-3"]);
assert.ok(
validKeys.every((k) => validSet.has(k)),
"only extra_2 and extra_3 should be returned"
);
});
it("should handle edge case: all keys invalid", () => {
const connectionId = "all-invalid-test";
const primaryKey = "pk-primary";
const extraKeys = ["extra-1"];
trackConnectionExtraKeys(connectionId, extraKeys);
// Mark all keys as invalid
const health: Record<string, KeyHealth> = {
primary: {
status: "invalid",
failures: 3,
lastFailure: null,
lastSuccess: null,
totalRequests: 3,
totalFailures: 3,
},
extra_0: {
status: "invalid",
failures: 3,
lastFailure: null,
lastSuccess: null,
totalRequests: 3,
totalFailures: 3,
},
};
// Should return null - no valid keys
const key = getValidApiKey(connectionId, primaryKey, extraKeys, health);
assert.equal(key, null);
// A3 guard: even with extra keys, if ALL keys are invalid,
// the connection might need to be disabled
// But current behavior: don't disable, just return null and let caller handle
assert.equal(connectionHasExtraKeys(connectionId), true);
assert.equal(getInvalidKeyCount(health), 2);
});
it("should track health persistence round-trip", () => {
const connectionId = "persist-test";
const primaryKey = "pk-persist";
const extraKeys = ["extra-1"];
trackConnectionExtraKeys(connectionId, extraKeys);
// Record failures
const healthBefore = recordKeyFailure(connectionId, "primary");
assert.equal(healthBefore.failures, 1);
// Simulate DB sync (persist health)
const dbHealth: Record<string, KeyHealth> = {
primary: healthBefore,
};
// Sync from DB
syncHealthFromDB(connectionId, dbHealth);
// Verify key is still marked in internal state (with connection prefix)
const allHealth = getAllKeyHealth();
const prefixedKey = `${connectionId}:primary`;
assert.equal(allHealth[prefixedKey]?.status, "warning");
// Get valid key should still work
const key = getValidApiKey(connectionId, primaryKey, extraKeys, dbHealth);
assert.equal(key?.key, primaryKey); // Still valid (only 1 failure)
});
});
describe("A3 Guard Integration Test", () => {
beforeEach(() => {
resetModuleState();
});
afterEach(() => {
resetModuleState();
});
it("should document the expected A3 guard behavior for chat.ts", () => {
// This test documents how chat.ts should behave with A3 guard
const connectionId = "a3-guard-test";
const primaryKey = "pk-401";
const extraKeys = ["backup-1", "backup-2"];
// Simulate: Connection has extra keys
trackConnectionExtraKeys(connectionId, extraKeys);
assert.equal(connectionHasExtraKeys(connectionId), true);
// Simulate: 401 occurs on primary key
// Step 1: T07 code in chatCore.ts records key failure
const health = recordKeyFailure(connectionId, "primary");
// Step 2: A3 guard in chat.ts checks if connection has extra keys
const hasExtraKeys = connectionHasExtraKeys(connectionId);
const is401 = true; // Simulated 401
const skipConnectionDisable = is401 && hasExtraKeys;
// Step 3: With A3 guard, markAccountUnavailable should be skipped
assert.equal(skipConnectionDisable, true);
// Step 4: Next request should get backup key
const nextKey = getValidApiKey(connectionId, primaryKey, extraKeys, {
primary: health,
});
if (health.status === "invalid") {
assert.ok(extraKeys.includes(nextKey!.key!), "should use backup key");
}
// Step 5: Connection remains usable
// This is the key behavior: connection is NOT disabled
});
it("should document behavior when NO extra keys exist", () => {
const connectionId = "no-extras-test";
const extraKeys: string[] = [];
// No extra keys
trackConnectionExtraKeys(connectionId, extraKeys);
assert.equal(connectionHasExtraKeys(connectionId), false);
// Simulate: 401 occurs
recordKeyFailure(connectionId, "primary");
// A3 guard check
const hasExtraKeys = connectionHasExtraKeys(connectionId);
const is401 = true;
const skipConnectionDisable = is401 && hasExtraKeys;
// Without extra keys, connection SHOULD be disabled
assert.equal(skipConnectionDisable, false);
// chat.ts would call markAccountUnavailable here
// This is the expected behavior: disable the connection
});
});
@@ -0,0 +1,56 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import {
DEFAULT_SELF_SERVICE_SCOPES,
SELF_ACCOUNT_QUOTA_SCOPE,
SELF_USAGE_SCOPE,
hasSelfAccountQuotaScope,
hasSelfUsageScope,
normalizeSelfServiceScopesForCreate,
} from "../../src/shared/constants/selfServiceScopes.ts";
import { createKeySchema, updateKeyPermissionsSchema } from "../../src/shared/validation/schemas.ts";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
test("self-service scope constants are distinct and usage defaults on create", () => {
assert.equal(SELF_USAGE_SCOPE, "self:usage");
assert.equal(SELF_ACCOUNT_QUOTA_SCOPE, "self:account-quota");
assert.deepEqual(DEFAULT_SELF_SERVICE_SCOPES, [SELF_USAGE_SCOPE]);
assert.deepEqual(normalizeSelfServiceScopesForCreate(undefined), [SELF_USAGE_SCOPE]);
assert.deepEqual(normalizeSelfServiceScopesForCreate([]), [SELF_USAGE_SCOPE]);
assert.deepEqual(normalizeSelfServiceScopesForCreate(["manage"]), ["manage", SELF_USAGE_SCOPE]);
assert.deepEqual(normalizeSelfServiceScopesForCreate([SELF_ACCOUNT_QUOTA_SCOPE]), [
SELF_ACCOUNT_QUOTA_SCOPE,
SELF_USAGE_SCOPE,
]);
});
test("self-service scope helpers do not treat account quota as own-usage visibility", () => {
assert.equal(hasSelfUsageScope([SELF_USAGE_SCOPE]), true);
assert.equal(hasSelfUsageScope([SELF_ACCOUNT_QUOTA_SCOPE]), false);
assert.equal(hasSelfAccountQuotaScope([SELF_ACCOUNT_QUOTA_SCOPE]), true);
assert.equal(hasSelfAccountQuotaScope([SELF_USAGE_SCOPE]), false);
});
test("api key validation accepts more than sixteen scopes", () => {
const scopes = Array.from({ length: 18 }, (_, index) => `custom:${index}`);
assert.equal(createKeySchema.safeParse({ name: "heavy-scope-key", scopes }).success, true);
assert.equal(updateKeyPermissionsSchema.safeParse({ scopes }).success, true);
});
test("api key create route normalizes omitted scopes to self-service usage", () => {
const source = fs.readFileSync(path.join(repoRoot, "src/app/api/keys/route.ts"), "utf8");
assert.match(source, /normalizeSelfServiceScopesForCreate/);
assert.ok(
source.indexOf("normalizeSelfServiceScopesForCreate(scopes)") <
source.indexOf("createApiKey(name, machineId"),
"create route must add default self-service scope before persistence"
);
});
+427
View File
@@ -0,0 +1,427 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import DatabaseSync from "better-sqlite3";
import { SELF_ACCOUNT_QUOTA_SCOPE, SELF_USAGE_SCOPE } from "../../src/shared/constants/selfServiceScopes.ts";
import { buildApiKeySelfServiceStatus } from "../../src/lib/usage/apiKeySelfService.ts";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const migrationPath = path.join(
repoRoot,
"src/lib/db/migrations/075_api_key_self_service_usage_scopes.sql"
);
test("self-service scope migration backfills own usage once and preserves explicit account quota opt-in", () => {
const sql = fs.readFileSync(migrationPath, "utf8");
const db = new DatabaseSync(":memory:");
db.exec(`
CREATE TABLE api_keys (
id TEXT PRIMARY KEY,
scopes TEXT
);
INSERT INTO api_keys (id, scopes) VALUES
('legacy-empty', '[]'),
('legacy-null', NULL),
('custom', '["custom:scope"]'),
('quota-opt-in', '["${SELF_ACCOUNT_QUOTA_SCOPE}"]'),
('already-disabled-after-migration', '["custom:scope"]');
`);
db.exec(sql);
db.prepare("UPDATE api_keys SET scopes = ? WHERE id = ?").run(
JSON.stringify(["custom:scope"]),
"already-disabled-after-migration"
);
db.exec(sql);
const rows = db.prepare("SELECT id, scopes FROM api_keys ORDER BY id").all() as Array<{
id: string;
scopes: string;
}>;
const scopesById = new Map(rows.map((row) => [row.id, JSON.parse(row.scopes) as string[]]));
assert.deepEqual(scopesById.get("legacy-empty"), [SELF_USAGE_SCOPE]);
assert.deepEqual(scopesById.get("legacy-null"), [SELF_USAGE_SCOPE]);
assert.deepEqual(scopesById.get("custom"), ["custom:scope", SELF_USAGE_SCOPE]);
assert.deepEqual(scopesById.get("quota-opt-in"), [
SELF_ACCOUNT_QUOTA_SCOPE,
SELF_USAGE_SCOPE,
]);
assert.deepEqual(scopesById.get("already-disabled-after-migration"), ["custom:scope"]);
});
function makeDeps(overrides: Record<string, unknown> = {}) {
const tokenRows = overrides.tokenRows ?? {
inputTokens: 900,
outputTokens: 30,
cacheReadTokens: 120,
cacheCreationTokens: 10,
reasoningTokens: 5,
};
const dbParams: unknown[][] = [];
return {
dbParams,
deps: {
now: () => Date.UTC(2026, 4, 29, 12, 0, 0),
getCostSummary: () => ({
budget: null,
totalCostMonth: 12.34,
totalCostPeriod: 0,
activeLimitUsd: 0,
resetInterval: null,
resetTime: null,
budgetResetAt: null,
lastBudgetResetAt: null,
periodStartAt: null,
nextResetAt: null,
warningThreshold: null,
}),
checkBudget: () => ({ allowed: true }),
getDbInstance: () => ({
prepare: () => ({
get: (...params: unknown[]) => {
dbParams.push(params);
return tokenRows;
},
}),
}),
getProviderConnectionById: async () => null,
getProviderConnections: async () => [],
fetchAndPersistProviderLimits: async () => {
throw new Error("unexpected quota fetch");
},
...overrides,
},
};
}
test("self-service status reports own cost and token usage with null budget fields when no budget exists", async () => {
const metadata = {
id: "key-a",
name: "team-a",
scopes: [SELF_USAGE_SCOPE],
allowedConnections: [],
};
const { deps, dbParams } = makeDeps();
const status = await buildApiKeySelfServiceStatus(metadata, deps);
assert.deepEqual(status.apiKey, { id: "key-a", name: "team-a" });
assert.equal(status.usage.cost.usedUsd, 12.34);
assert.equal(status.usage.cost.limitUsd, null);
assert.equal(status.usage.cost.remainingUsd, null);
assert.equal(status.usage.cost.usedPercent, null);
assert.equal(status.usage.cost.period, "monthly");
assert.equal(status.usage.tokens.totalTokens, 1065);
assert.equal(dbParams[0][0], "key-a");
assert.equal(dbParams[0][1], "2026-05-01T00:00:00.000Z");
assert.equal("accountQuota" in status, false);
});
test("self-service status reports USD budget percentage using the budget period", async () => {
const metadata = {
id: "key-budget",
name: "budgeted",
scopes: [SELF_USAGE_SCOPE],
allowedConnections: [],
};
const periodStart = Date.UTC(2026, 4, 1, 0, 0, 0);
const nextReset = Date.UTC(2026, 5, 1, 0, 0, 0);
const { deps } = makeDeps({
getCostSummary: () => ({
budget: { resetInterval: "monthly" },
totalCostMonth: 99,
totalCostPeriod: 12.5,
activeLimitUsd: 50,
resetInterval: "monthly",
resetTime: "00:00",
budgetResetAt: nextReset,
lastBudgetResetAt: periodStart,
periodStartAt: periodStart,
nextResetAt: nextReset,
warningThreshold: 0.8,
}),
});
const status = await buildApiKeySelfServiceStatus(metadata, deps);
assert.equal(status.usage.cost.usedUsd, 12.5);
assert.equal(status.usage.cost.limitUsd, 50);
assert.equal(status.usage.cost.remainingUsd, 37.5);
assert.equal(status.usage.cost.usedPercent, 25);
assert.equal(status.usage.cost.periodStartAt, "2026-05-01T00:00:00.000Z");
assert.equal(status.usage.cost.resetAt, "2026-06-01T00:00:00.000Z");
});
test("self-service status preserves ISO and Date budget timestamps", async () => {
const metadata = {
id: "key-budget-date",
name: "budgeted date",
scopes: [SELF_USAGE_SCOPE],
allowedConnections: [],
};
const { deps, dbParams } = makeDeps({
getCostSummary: () => ({
budget: { resetInterval: "weekly" },
totalCostMonth: 99,
totalCostPeriod: 15,
activeLimitUsd: 60,
resetInterval: "weekly",
resetTime: "00:00",
budgetResetAt: null,
lastBudgetResetAt: null,
periodStartAt: "2026-05-18T00:00:00.000Z",
nextResetAt: new Date("2026-05-25T00:00:00.000Z"),
warningThreshold: 0.8,
}),
});
const status = await buildApiKeySelfServiceStatus(metadata, deps);
assert.equal(status.usage.cost.periodStartAt, "2026-05-18T00:00:00.000Z");
assert.equal(status.usage.cost.resetAt, "2026-05-25T00:00:00.000Z");
assert.equal(dbParams[0][1], "2026-05-18T00:00:00.000Z");
});
test("self-service status reports all explicitly allowed provider account quotas", async () => {
const metadata = {
id: "key-multi",
name: "multi",
scopes: [SELF_USAGE_SCOPE, SELF_ACCOUNT_QUOTA_SCOPE],
allowedConnections: ["conn-codex", "conn-claude"],
};
const fetches: string[] = [];
const { deps } = makeDeps({
getProviderConnectionById: async (connectionId: string) => ({
id: connectionId,
provider: connectionId === "conn-codex" ? "codex" : "claude",
isActive: true,
}),
fetchAndPersistProviderLimits: async (connectionId: string) => {
fetches.push(connectionId);
if (connectionId === "conn-codex") {
return {
connection: { id: connectionId, provider: "codex" },
usage: {
plan: "ChatGPT Plus",
quotas: {
session: { used: 1, remaining: 99, resetAt: "2026-05-29T18:11:44.000Z" },
},
},
cache: { quotas: null, plan: null, message: null, fetchedAt: "" },
};
}
return {
connection: { id: connectionId, provider: "claude" },
usage: {
plan: "Claude Max",
quotas: {
daily: { usedPercentage: 35, remainingPercentage: 65, resetAt: "2026-05-30T00:00:00.000Z" },
},
},
cache: { quotas: null, plan: null, message: null, fetchedAt: "" },
};
},
});
const status = await buildApiKeySelfServiceStatus(metadata, deps);
assert.deepEqual(fetches, ["conn-codex", "conn-claude"]);
assert.deepEqual(
status.accountQuotas.map((quota: { connectionId: string }) => quota.connectionId),
["conn-codex", "conn-claude"]
);
assert.equal(status.accountQuotas[0].provider, "codex");
assert.equal(status.accountQuotas[0].plan, "ChatGPT Plus");
assert.equal(status.accountQuotas[0].quotas.session.remainingPercentage, 99);
assert.equal(status.accountQuotas[1].provider, "claude");
assert.equal(status.accountQuotas[1].plan, "Claude Max");
assert.equal(status.accountQuotas[1].quotas.daily.usedPercentage, 35);
});
test("self-service status reports all active provider account quotas for unrestricted keys", async () => {
const metadata = {
id: "key-unrestricted",
name: "unrestricted",
scopes: [SELF_USAGE_SCOPE, SELF_ACCOUNT_QUOTA_SCOPE],
allowedConnections: [],
};
const { deps } = makeDeps({
getProviderConnections: async () => [
{ id: "conn-codex", provider: "codex", isActive: true },
{ id: "conn-cursor", provider: "cursor", isActive: true },
{ id: "conn-disabled", provider: "claude", isActive: false },
],
fetchAndPersistProviderLimits: async (connectionId: string) => ({
connection: { id: connectionId, provider: connectionId === "conn-codex" ? "codex" : "cursor" },
usage: {
plan: connectionId === "conn-codex" ? "ChatGPT Plus" : "Cursor Pro",
quotas: {
monthly: { used: 25, remaining: 75, resetAt: "2026-06-01T00:00:00.000Z" },
},
},
cache: { quotas: null, plan: null, message: null, fetchedAt: "" },
}),
});
const status = await buildApiKeySelfServiceStatus(metadata, deps);
assert.deepEqual(
status.accountQuotas.map((quota: { connectionId: string }) => quota.connectionId),
["conn-codex", "conn-cursor"]
);
assert.equal(status.accountQuotas[0].quotas.monthly.remainingPercentage, 75);
assert.equal(status.accountQuotas[1].plan, "Cursor Pro");
});
test("self-service status isolates provider account quota fetch failures per connection", async () => {
const metadata = {
id: "key-partial",
name: "partial",
scopes: [SELF_USAGE_SCOPE, SELF_ACCOUNT_QUOTA_SCOPE],
allowedConnections: ["conn-codex", "conn-cursor"],
};
const { deps } = makeDeps({
getProviderConnectionById: async (connectionId: string) => ({
id: connectionId,
provider: connectionId === "conn-codex" ? "codex" : "cursor",
isActive: true,
}),
fetchAndPersistProviderLimits: async (connectionId: string) => {
if (connectionId === "conn-cursor") throw new Error("upstream unavailable");
return {
connection: { id: connectionId, provider: "codex" },
usage: {
quotas: {
weekly: { used: 40, remaining: 60, resetAt: "2026-06-01T00:00:00.000Z" },
},
},
cache: { quotas: null, plan: null, message: null, fetchedAt: "" },
};
},
});
const status = await buildApiKeySelfServiceStatus(metadata, deps);
assert.equal(status.accountQuotas[0].connectionId, "conn-codex");
assert.equal(status.accountQuotas[0].quotas.weekly.remainingPercentage, 60);
assert.deepEqual(status.accountQuotas[1], {
provider: "cursor",
connectionId: "conn-cursor",
shared: true,
available: false,
reason: "fetch_failed",
});
});
test("self-service status isolates explicit provider connection lookup failures", async () => {
const metadata = {
id: "key-lookup-partial",
name: "lookup partial",
scopes: [SELF_USAGE_SCOPE, SELF_ACCOUNT_QUOTA_SCOPE],
allowedConnections: ["conn-codex", "conn-missing"],
};
const { deps } = makeDeps({
getProviderConnectionById: async (connectionId: string) => {
if (connectionId === "conn-missing") throw new Error("database unavailable");
return {
id: connectionId,
provider: "codex",
isActive: true,
};
},
fetchAndPersistProviderLimits: async (connectionId: string) => ({
connection: { id: connectionId, provider: "codex" },
usage: {
quotas: {
weekly: { used: 40, remaining: 60, resetAt: "2026-06-01T00:00:00.000Z" },
},
},
cache: { quotas: null, plan: null, message: null, fetchedAt: "" },
}),
});
const status = await buildApiKeySelfServiceStatus(metadata, deps);
assert.equal(status.accountQuotas[0].connectionId, "conn-codex");
assert.equal(status.accountQuotas[0].quotas.weekly.remainingPercentage, 60);
assert.deepEqual(status.accountQuotas[1], {
provider: "unknown",
connectionId: "conn-missing",
shared: true,
available: false,
reason: "connection_lookup_failed",
});
});
test("self-service status keeps usage visible when unrestricted provider lookup fails", async () => {
const metadata = {
id: "key-lookup-failed",
name: "lookup failed",
scopes: [SELF_USAGE_SCOPE, SELF_ACCOUNT_QUOTA_SCOPE],
allowedConnections: [],
};
const { deps } = makeDeps({
getProviderConnections: async () => {
throw new Error("database unavailable");
},
});
const status = await buildApiKeySelfServiceStatus(metadata, deps);
assert.equal(status.usage.cost.usedUsd, 12.34);
assert.deepEqual(status.accountQuotas, []);
assert.equal("accountQuota" in status, false);
});
test("self-service status normalizes Codex account quota for one explicit connection", async () => {
const metadata = {
id: "key-codex",
name: "codex",
scopes: [SELF_USAGE_SCOPE, SELF_ACCOUNT_QUOTA_SCOPE],
allowedConnections: ["conn-codex"],
};
const { deps } = makeDeps({
getProviderConnectionById: async (connectionId: string) => ({
id: connectionId,
provider: "codex",
}),
fetchAndPersistProviderLimits: async () => ({
connection: { id: "conn-codex", provider: "codex" },
usage: {
quotas: {
session: { used: 1, remaining: 99, resetAt: "2026-05-29T18:11:44.000Z" },
weekly: { used: 97, remaining: 3, resetAt: "2026-05-31T01:23:38.000Z" },
},
},
cache: { quotas: null, plan: null, message: null, fetchedAt: "" },
}),
});
const status = await buildApiKeySelfServiceStatus(metadata, deps);
assert.equal(status.accountQuotas.length, 1);
assert.deepEqual(status.accountQuotas[0], status.accountQuota);
assert.deepEqual(status.accountQuota, {
provider: "codex",
connectionId: "conn-codex",
shared: true,
quotas: {
session: {
usedPercentage: 1,
remainingPercentage: 99,
resetAt: "2026-05-29T18:11:44.000Z",
},
weekly: {
usedPercentage: 97,
remainingPercentage: 3,
resetAt: "2026-05-31T01:23:38.000Z",
},
},
});
});
+436
View File
@@ -0,0 +1,436 @@
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-api-key-usage-limits-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "usage-limit-test-secret";
const core = await import("../../src/lib/db/core.ts");
const localDb = await import("../../src/lib/localDb.ts");
const apiKeysDb = await import("../../src/lib/db/apiKeys.ts");
const usageHistory = await import("../../src/lib/usage/usageHistory.ts");
const usageLimits = await import("../../src/lib/usage/apiKeyUsageLimits.ts");
const NOW = Date.parse("2026-06-19T20:00:00.000Z");
async function resetStorage() {
core.resetDbInstance();
apiKeysDb.resetApiKeyState();
usageHistory.clearPendingRequests();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}
test.beforeEach(async () => {
await resetStorage();
});
test.after(() => {
core.resetDbInstance();
apiKeysDb.resetApiKeyState();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
test("API key USD usage limits persist and default off", async () => {
const created = await apiKeysDb.createApiKey("Usage Limit Key", "machine-limit-01");
let metadata = await apiKeysDb.getApiKeyMetadata(created.key);
assert.equal(metadata?.usageLimitEnabled, false);
assert.equal(metadata?.dailyUsageLimitUsd, null);
assert.equal(metadata?.weeklyUsageLimitUsd, null);
await apiKeysDb.updateApiKeyPermissions(created.id, {
usageLimitEnabled: true,
dailyUsageLimitUsd: 10.5,
weeklyUsageLimitUsd: 50,
});
apiKeysDb.clearApiKeyCaches();
metadata = await apiKeysDb.getApiKeyMetadata(created.key);
assert.equal(metadata?.usageLimitEnabled, true);
assert.equal(metadata?.dailyUsageLimitUsd, 10.5);
assert.equal(metadata?.weeklyUsageLimitUsd, 50);
});
test("getApiKeyUsageLimitStatus aligns weekly USD spend with provider resetAt when available", async () => {
await localDb.updatePricing({
claude: {
"claude-opus-4-8": {
input: 1,
cached: 1,
output: 1,
reasoning: 1,
cache_creation: 1,
},
},
});
const created = await apiKeysDb.createApiKey("Metered Key", "machine-limit-02");
await apiKeysDb.updateApiKeyPermissions(created.id, {
usageLimitEnabled: true,
dailyUsageLimitUsd: 10,
weeklyUsageLimitUsd: 20,
});
await usageHistory.saveRequestUsage({
provider: "claude",
model: "claude-opus-4-8",
apiKeyId: created.id,
apiKeyName: "Metered Key",
tokens: { input: 2_000_000, output: 0 },
success: true,
timestamp: "2026-06-19T12:00:00.000Z",
});
await usageHistory.saveRequestUsage({
provider: "claude",
model: "claude-opus-4-8",
apiKeyId: created.id,
apiKeyName: "Metered Key",
tokens: { input: 3_000_000, output: 0 },
success: true,
timestamp: "2026-06-18T21:00:00.000Z",
});
await usageHistory.saveRequestUsage({
provider: "claude",
model: "claude-opus-4-8",
apiKeyId: created.id,
apiKeyName: "Metered Key",
tokens: { input: 7_000_000, output: 0 },
success: true,
timestamp: "2026-06-18T12:00:00.000Z",
});
const metadata = await apiKeysDb.getApiKeyMetadata(created.key);
assert.ok(metadata);
const weeklyResetAt = "2026-06-25T20:00:00.000Z";
const status = await usageLimits.getApiKeyUsageLimitStatus(
{ ...metadata, allowedConnections: ["conn-claude"] },
{
now: () => NOW,
getProviderConnectionById: async () => ({
id: "conn-claude",
provider: "claude",
isActive: true,
}),
getProviderConnections: async () => [],
getProviderLimitsCache: () => ({
plan: "Claude Max",
quotas: {
"weekly (7d)": {
used: 27,
total: 100,
resetAt: weeklyResetAt,
},
},
message: null,
fetchedAt: new Date(NOW).toISOString(),
}),
getAllProviderLimitsCache: () => ({}),
}
);
assert.equal(status.enabled, true);
assert.equal(status.dailySpentUsd, 2);
assert.equal(status.weeklySpentUsd, 5);
assert.equal(status.dailyLimitUsd, 10);
assert.equal(status.weeklyLimitUsd, 20);
assert.equal(status.dailyResetAtIso, "2026-06-20T03:00:00.000Z");
assert.equal(status.weeklyWindowStartIso, "2026-06-18T20:00:00.000Z");
assert.equal(status.weeklyResetAtIso, weeklyResetAt);
assert.equal(status.dailyExceeded, false);
assert.equal(status.weeklyExceeded, false);
});
test("getApiKeyUsageLimitStatus cuts weekly USD spend at observed provider quota reset", async () => {
await localDb.updatePricing({
claude: {
"claude-opus-4-8": {
input: 1,
cached: 1,
output: 1,
reasoning: 1,
cache_creation: 1,
},
},
});
const created = await apiKeysDb.createApiKey("Reset Cut Key", "machine-limit-reset");
await apiKeysDb.updateApiKeyPermissions(created.id, {
usageLimitEnabled: true,
dailyUsageLimitUsd: 10,
weeklyUsageLimitUsd: 20,
});
await usageHistory.saveRequestUsage({
provider: "claude",
model: "claude-opus-4-8",
apiKeyId: created.id,
apiKeyName: "Reset Cut Key",
tokens: { input: 7_000_000, output: 0 },
success: true,
timestamp: "2026-06-19T23:30:00.000Z",
});
await usageHistory.saveRequestUsage({
provider: "claude",
model: "claude-opus-4-8",
apiKeyId: created.id,
apiKeyName: "Reset Cut Key",
tokens: { input: 2_000_000, output: 0 },
success: true,
timestamp: "2026-06-20T02:00:00.000Z",
});
const db = core.getDbInstance();
const insertSnapshot = db.prepare(`
INSERT INTO quota_snapshots (
provider,
connection_id,
window_key,
remaining_percentage,
is_exhausted,
next_reset_at,
window_duration_ms,
raw_data,
created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
insertSnapshot.run(
"claude",
"conn-claude",
"weekly (7d)",
72,
0,
"2026-06-25T23:00:00.000Z",
null,
null,
"2026-06-19T23:55:00.000Z"
);
insertSnapshot.run(
"claude",
"conn-claude",
"weekly (7d)",
100,
0,
"2026-06-25T23:00:00.000Z",
null,
null,
"2026-06-20T01:42:52.590Z"
);
insertSnapshot.run(
"claude",
"conn-claude",
"weekly (7d)",
99,
0,
"2026-06-25T23:00:00.000Z",
null,
null,
"2026-06-20T02:10:00.000Z"
);
db.prepare(
`
INSERT INTO provider_quota_reset_events
(provider, connection_id, window_key, window_started_at, window_resets_at,
observed_at, previous_remaining_percentage, new_remaining_percentage,
previous_used_percentage, new_used_percentage, raw_data)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`
).run(
"claude",
"conn-claude",
"weekly (7d)",
"2026-06-18T23:00:00.000Z",
"2026-06-25T23:00:00.000Z",
"2026-06-18T23:04:00.000Z",
0,
100,
100,
0,
null
);
const metadata = await apiKeysDb.getApiKeyMetadata(created.key);
assert.ok(metadata);
const weeklyResetAt = "2026-06-25T23:00:00.000Z";
const status = await usageLimits.getApiKeyUsageLimitStatus(
{ ...metadata, allowedConnections: ["conn-claude"] },
{
now: () => Date.parse("2026-06-20T14:30:00.000Z"),
getProviderConnectionById: async () => ({
id: "conn-claude",
provider: "claude",
isActive: true,
}),
getProviderConnections: async () => [],
getProviderLimitsCache: () => ({
plan: "Claude Max",
quotas: {
"weekly (7d)": {
used: 5,
total: 100,
resetAt: weeklyResetAt,
},
},
message: null,
fetchedAt: "2026-06-20T14:30:00.000Z",
}),
getAllProviderLimitsCache: () => ({}),
}
);
assert.equal(status.weeklyWindowStartIso, "2026-06-20T01:42:52.590Z");
assert.equal(status.weeklySpentUsd, 2);
});
test("buildApiKeyUsageLimitText returns API-key quota spend percentage and reset lines", async () => {
const text = usageLimits.buildApiKeyUsageLimitText(
{
enabled: true,
dailyLimitUsd: 10,
weeklyLimitUsd: 50,
dailySpentUsd: 2,
weeklySpentUsd: 5.25,
dailyWindowStartIso: "2026-06-19T03:00:00.000Z",
dailyResetAtIso: "2026-06-20T03:00:00.000Z",
weeklyWindowStartIso: "2026-06-12T20:00:00.000Z",
weeklyResetAtIso: "2026-06-25T20:00:00.000Z",
dailyExceeded: false,
weeklyExceeded: false,
},
Date.parse("2026-06-19T20:00:00.000Z")
);
assert.equal(
text,
[
"Daily quota",
"$10.00",
"Daily spent",
"$2.00",
"Daily used",
"20%",
"Resets in 7h 0m",
"",
"Weekly quota",
"$50.00",
"Weekly spent",
"$5.25",
"Weekly used",
"11%",
"Resets in 6d 0h 0m",
].join("\n")
);
});
test("buildApiKeyUsageLimitPercentText returns remaining percentages only", () => {
const text = usageLimits.buildApiKeyUsageLimitPercentText(
{
enabled: true,
dailyLimitUsd: 10,
weeklyLimitUsd: 50,
dailySpentUsd: 2,
weeklySpentUsd: 5.25,
dailyWindowStartIso: "2026-06-19T03:00:00.000Z",
dailyResetAtIso: "2026-06-20T03:00:00.000Z",
weeklyWindowStartIso: "2026-06-12T20:00:00.000Z",
weeklyResetAtIso: "2026-06-25T20:00:00.000Z",
dailyExceeded: false,
weeklyExceeded: false,
},
Date.parse("2026-06-19T20:00:00.000Z")
);
assert.equal(
text,
["Daily", "80% left", "⏱ reset in 7h 0m", "", "Weekly", "90% left", "⏱ reset in 6d 0h 0m"].join(
"\n"
)
);
});
test("buildApiKeyUsageLimitRejection includes over-quota percentage and reset hint", async () => {
const response = usageLimits.buildApiKeyUsageLimitRejection(
new Request("http://localhost/v1/messages", {
headers: { "anthropic-version": "2023-06-01" },
}),
{
enabled: true,
dailyLimitUsd: 10,
weeklyLimitUsd: 1,
dailySpentUsd: 0.25,
weeklySpentUsd: 1.09,
dailyWindowStartIso: "2026-06-19T03:00:00.000Z",
dailyResetAtIso: "2026-06-20T03:00:00.000Z",
weeklyWindowStartIso: "2026-06-12T20:00:00.000Z",
weeklyResetAtIso: "2026-06-25T20:00:00.000Z",
dailyExceeded: false,
weeklyExceeded: true,
},
Date.parse("2026-06-19T20:00:00.000Z")
);
assert.equal(response.status, 400);
const body = (await response.json()) as { error: { message: string } };
assert.equal(
body.error.message,
"This API key reached its weekly USD usage quota ($1.09 of $1.00, 109%). Resets in 6d 0h 0m. Choose another allowed model after reset."
);
});
test("buildApiKeyUsageLimitRejection can hide USD amounts for client-facing policy errors", async () => {
const response = usageLimits.buildApiKeyUsageLimitRejection(
new Request("http://localhost/v1/messages", {
headers: { "anthropic-version": "2023-06-01" },
}),
{
enabled: true,
dailyLimitUsd: 10,
weeklyLimitUsd: 1,
dailySpentUsd: 0.25,
weeklySpentUsd: 1.09,
dailyWindowStartIso: "2026-06-19T03:00:00.000Z",
dailyResetAtIso: "2026-06-20T03:00:00.000Z",
weeklyWindowStartIso: "2026-06-12T20:00:00.000Z",
weeklyResetAtIso: "2026-06-25T20:00:00.000Z",
dailyExceeded: false,
weeklyExceeded: true,
},
Date.parse("2026-06-19T20:00:00.000Z"),
{ showUsd: false }
);
assert.equal(response.status, 400);
const body = (await response.json()) as { error: { message: string } };
assert.equal(
body.error.message,
"This API key reached its weekly usage quota (109%). Resets in 6d 0h 0m. Choose another allowed model after reset."
);
});
test("buildApiKeyUsageLimitRejection uses 400 so Claude Code does not trigger login", () => {
const response = usageLimits.buildApiKeyUsageLimitRejection(
new Request("http://localhost/v1/messages", {
headers: { "anthropic-version": "2023-06-01" },
}),
{
enabled: true,
dailyLimitUsd: 10,
weeklyLimitUsd: 50,
dailySpentUsd: 12,
weeklySpentUsd: 20,
dailyWindowStartIso: "2026-06-19T03:00:00.000Z",
dailyResetAtIso: "2026-06-20T03:00:00.000Z",
weeklyWindowStartIso: "2026-06-12T20:00:00.000Z",
weeklyResetAtIso: "2026-06-19T20:00:00.000Z",
dailyExceeded: true,
weeklyExceeded: false,
}
);
assert.equal(response.status, 400);
});
@@ -0,0 +1,29 @@
import test from "node:test";
import assert from "node:assert/strict";
process.env.API_KEY_SECRET = "test-api-key-utils-secret";
const apiKeyUtils = await import("../../src/shared/utils/apiKey.ts");
test("api key utility public surface keeps generation and parsing only", () => {
const machineId = "testmachine";
const { key, keyId } = apiKeyUtils.generateApiKeyWithMachine(machineId);
assert.equal(typeof key, "string");
assert.equal(typeof keyId, "string");
assert.match(key, new RegExp(`^sk-${machineId}-${keyId}-[a-f0-9]{8}$`));
assert.deepEqual(apiKeyUtils.parseApiKey(key), {
machineId,
keyId,
isNewFormat: true,
});
assert.deepEqual(apiKeyUtils.parseApiKey("sk-legacykey"), {
machineId: null,
keyId: "legacykey",
isNewFormat: false,
});
assert.equal(apiKeyUtils.parseApiKey("not-a-key"), null);
assert.equal("verifyApiKeyCrc" in apiKeyUtils, false);
assert.equal("isNewFormatKey" in apiKeyUtils, false);
});
+336
View File
@@ -0,0 +1,336 @@
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import {
isKeyActive,
isExpired,
isRestricted,
classifyKeyStatus,
classifyKeyType,
computeApiKeyCounts,
} from "../../src/app/(dashboard)/dashboard/api-manager/apiManagerPageUtils.js";
import type { ApiKeyShape } from "../../src/app/(dashboard)/dashboard/api-manager/apiManagerPageUtils.js";
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
const futureDate = new Date(Date.now() + 86_400_000).toISOString(); // +1 day
const pastDate = new Date(Date.now() - 86_400_000).toISOString(); // -1 day
function makeKey(overrides: Partial<ApiKeyShape> = {}): ApiKeyShape {
return {
isActive: true,
isBanned: false,
expiresAt: null,
scopes: [],
allowedModels: null,
allowedConnections: null,
...overrides,
};
}
// ---------------------------------------------------------------------------
// Scenario 1 — isKeyActive
// ---------------------------------------------------------------------------
describe("isKeyActive", () => {
it("returns true for a fully active key", () => {
assert.equal(isKeyActive(makeKey()), true);
});
it("returns false when isBanned is true", () => {
assert.equal(isKeyActive(makeKey({ isBanned: true })), false);
});
it("returns false when isActive is false", () => {
assert.equal(isKeyActive(makeKey({ isActive: false })), false);
});
it("returns false when expiresAt is in the past", () => {
assert.equal(isKeyActive(makeKey({ expiresAt: pastDate })), false);
});
it("returns true when expiresAt is in the future", () => {
assert.equal(isKeyActive(makeKey({ expiresAt: futureDate })), true);
});
it("returns false when banned even if isActive true and not expired", () => {
assert.equal(
isKeyActive(makeKey({ isBanned: true, isActive: true, expiresAt: futureDate })),
false
);
});
});
// ---------------------------------------------------------------------------
// Scenario 2 — isExpired
// ---------------------------------------------------------------------------
describe("isExpired", () => {
it("returns false when expiresAt is null", () => {
assert.equal(isExpired(makeKey({ expiresAt: null })), false);
});
it("returns false when expiresAt is in the future", () => {
assert.equal(isExpired(makeKey({ expiresAt: futureDate })), false);
});
it("returns true when expiresAt is in the past", () => {
assert.equal(isExpired(makeKey({ expiresAt: pastDate })), true);
});
it("returns false for an invalid date string (NaN)", () => {
assert.equal(isExpired(makeKey({ expiresAt: "not-a-date" })), false);
});
});
// ---------------------------------------------------------------------------
// Scenario 3 — isRestricted
// ---------------------------------------------------------------------------
describe("isRestricted", () => {
it("returns false when both allowedModels and allowedConnections are null", () => {
assert.equal(isRestricted(makeKey()), false);
});
it("returns false when allowedModels is an empty array", () => {
assert.equal(isRestricted(makeKey({ allowedModels: [] })), false);
});
it("returns true when allowedModels has entries", () => {
assert.equal(isRestricted(makeKey({ allowedModels: ["gpt-4"] })), true);
});
it("returns true when allowedConnections has entries", () => {
assert.equal(isRestricted(makeKey({ allowedConnections: ["conn-1"] })), true);
});
it("returns true when both have entries", () => {
assert.equal(
isRestricted(makeKey({ allowedModels: ["gpt-4"], allowedConnections: ["conn-1"] })),
true
);
});
});
// ---------------------------------------------------------------------------
// Scenario 4 — classifyKeyStatus (banned takes highest priority)
// ---------------------------------------------------------------------------
describe("classifyKeyStatus", () => {
it("returns 'banned' for a banned key (highest priority)", () => {
assert.equal(classifyKeyStatus(makeKey({ isBanned: true })), "banned");
});
it("returns 'banned' even if also expired", () => {
assert.equal(classifyKeyStatus(makeKey({ isBanned: true, expiresAt: pastDate })), "banned");
});
it("returns 'expired' before 'disabled' when key has past expiry and isActive false", () => {
// expiresAt in the past AND isActive false — expired is checked before disabled
assert.equal(classifyKeyStatus(makeKey({ expiresAt: pastDate, isActive: false })), "expired");
});
it("returns 'expired' for a non-banned, expired key", () => {
assert.equal(classifyKeyStatus(makeKey({ expiresAt: pastDate })), "expired");
});
it("returns 'disabled' when isActive false and not expired/banned", () => {
assert.equal(classifyKeyStatus(makeKey({ isActive: false })), "disabled");
});
it("returns 'active' for a healthy key", () => {
assert.equal(classifyKeyStatus(makeKey()), "active");
});
it("returns 'active' for key with future expiry and no bans", () => {
assert.equal(classifyKeyStatus(makeKey({ expiresAt: futureDate })), "active");
});
});
// ---------------------------------------------------------------------------
// Scenario 4 continued — classifyKeyType
// ---------------------------------------------------------------------------
describe("classifyKeyType", () => {
it("returns 'standard' for a plain key", () => {
assert.equal(classifyKeyType(makeKey()), "standard");
});
it("returns 'manage' when scopes includes manage", () => {
assert.equal(classifyKeyType(makeKey({ scopes: ["manage"] })), "manage");
});
it("returns 'manage' even if also restricted (manage takes priority)", () => {
assert.equal(
classifyKeyType(makeKey({ scopes: ["manage"], allowedModels: ["gpt-4"] })),
"manage"
);
});
it("returns 'restricted' when has allowedModels and no manage scope", () => {
assert.equal(classifyKeyType(makeKey({ allowedModels: ["gpt-4"] })), "restricted");
});
it("returns 'restricted' when has allowedConnections and no manage scope", () => {
assert.equal(classifyKeyType(makeKey({ allowedConnections: ["c1"] })), "restricted");
});
});
// ---------------------------------------------------------------------------
// Scenario 5 — computeApiKeyCounts with 10 varied keys
// ---------------------------------------------------------------------------
describe("computeApiKeyCounts", () => {
it("returns all zeros for empty array (edge case)", () => {
const counts = computeApiKeyCounts([]);
assert.equal(counts.total, 0);
assert.equal(counts.active, 0);
assert.equal(counts.banned, 0);
assert.equal(counts.expired, 0);
assert.equal(counts.disabled, 0);
assert.equal(counts.standard, 0);
assert.equal(counts.manage, 0);
assert.equal(counts.restricted, 0);
});
it("correctly tallies 10 mixed keys", () => {
const keys: ApiKeyShape[] = [
makeKey(), // active + standard
makeKey({ expiresAt: futureDate }), // active + standard
makeKey({ scopes: ["manage"] }), // active + manage
makeKey({ allowedModels: ["gpt-4"] }), // active + restricted
makeKey({ isActive: false }), // disabled + standard
makeKey({ isBanned: true }), // banned + standard
makeKey({ expiresAt: pastDate }), // expired + standard
makeKey({ expiresAt: pastDate, allowedModels: ["gpt-4"] }), // expired + restricted (type=restricted)
makeKey({ scopes: ["manage"], allowedConnections: ["c1"] }), // active + manage (manage priority)
makeKey({ allowedConnections: ["c1"] }), // active + restricted
];
const counts = computeApiKeyCounts(keys);
assert.equal(counts.total, 10);
// statuses: active(6: keys 1,2,3,4,9,10), disabled(1), banned(1), expired(2)
assert.equal(counts.active, 6);
assert.equal(counts.disabled, 1);
assert.equal(counts.banned, 1);
assert.equal(counts.expired, 2);
// types: manage(2), restricted(3), standard(5 — active×2, disabled, banned, expired×1)
assert.equal(counts.manage, 2);
assert.equal(counts.restricted, 3);
assert.equal(counts.standard, 5);
});
});
// ---------------------------------------------------------------------------
// Scenario 6 — filteredKeys logic (inline simulation of useMemo)
// ---------------------------------------------------------------------------
function applyFilters(
keys: ApiKeyShape[],
opts: {
activeOnly?: boolean;
statusFilter?: string | null;
typeFilter?: string | null;
searchQuery?: string;
}
): ApiKeyShape[] {
let list = keys;
if (opts.activeOnly) list = list.filter(isKeyActive);
if (opts.statusFilter === "active") list = list.filter(isKeyActive);
else if (opts.statusFilter === "disabled") list = list.filter((k) => k.isActive === false);
else if (opts.statusFilter === "banned") list = list.filter((k) => k.isBanned === true);
else if (opts.statusFilter === "expired") list = list.filter(isExpired);
if (opts.typeFilter === "manage") list = list.filter((k) => k.scopes?.includes("manage"));
else if (opts.typeFilter === "restricted") list = list.filter(isRestricted);
else if (opts.typeFilter === "standard")
list = list.filter((k) => !k.scopes?.includes("manage") && !isRestricted(k));
if (opts.searchQuery?.trim()) {
const q = opts.searchQuery.toLowerCase();
// We cast to any to allow name/key fields for the filter simulation
list = list.filter(
(k) =>
((k as Record<string, unknown>)["name"] as string | undefined)?.toLowerCase().includes(q) ||
((k as Record<string, unknown>)["key"] as string | undefined)?.toLowerCase().includes(q)
);
}
return list;
}
interface TestApiKey extends ApiKeyShape {
name: string;
key: string;
}
function makeTestKey(name: string, key: string, overrides: Partial<ApiKeyShape> = {}): TestApiKey {
return { ...makeKey(overrides), name, key };
}
describe("filter composition", () => {
const testKeys: TestApiKey[] = [
makeTestKey("Alpha", "sk-alpha", {}),
makeTestKey("Beta", "sk-beta", { isBanned: true }),
makeTestKey("Gamma", "sk-gamma", { isActive: false }),
makeTestKey("Delta", "sk-delta", { expiresAt: pastDate }),
makeTestKey("Epsilon", "sk-epsilon", { scopes: ["manage"] }),
];
it("activeOnly=true excludes banned and inactive keys", () => {
const result = applyFilters(testKeys, { activeOnly: true });
const names = result.map((k) => (k as TestApiKey).name);
assert.ok(names.includes("Alpha"), "Active key should be included");
assert.ok(names.includes("Epsilon"), "Manage key should be included");
assert.ok(!names.includes("Beta"), "Banned should be excluded");
assert.ok(!names.includes("Gamma"), "Disabled should be excluded");
assert.ok(!names.includes("Delta"), "Expired should be excluded");
});
it("statusFilter='banned' returns only banned keys", () => {
const result = applyFilters(testKeys, { statusFilter: "banned" });
assert.equal(result.length, 1);
assert.equal((result[0] as TestApiKey).name, "Beta");
});
it("statusFilter='disabled' returns only disabled keys", () => {
const result = applyFilters(testKeys, { statusFilter: "disabled" });
assert.equal(result.length, 1);
assert.equal((result[0] as TestApiKey).name, "Gamma");
});
it("typeFilter='manage' returns only manage-scoped keys", () => {
const result = applyFilters(testKeys, { typeFilter: "manage" });
assert.equal(result.length, 1);
assert.equal((result[0] as TestApiKey).name, "Epsilon");
});
});
// ---------------------------------------------------------------------------
// Scenario 7 — searchQuery filters by name case-insensitively
// ---------------------------------------------------------------------------
describe("searchQuery filtering", () => {
const testKeys: TestApiKey[] = [
makeTestKey("Production Key", "sk-prod-abc123", {}),
makeTestKey("Development Key", "sk-dev-xyz789", {}),
makeTestKey("Staging", "sk-stg-qwerty", {}),
];
it("matches by name case-insensitively", () => {
const result = applyFilters(testKeys, { searchQuery: "production" });
assert.equal(result.length, 1);
assert.equal((result[0] as TestApiKey).name, "Production Key");
});
it("matches by key prefix case-insensitively", () => {
const result = applyFilters(testKeys, { searchQuery: "SK-DEV" });
assert.equal(result.length, 1);
assert.equal((result[0] as TestApiKey).name, "Development Key");
});
it("returns all keys when searchQuery is empty", () => {
const result = applyFilters(testKeys, { searchQuery: "" });
assert.equal(result.length, 3);
});
it("returns empty when no key matches", () => {
const result = applyFilters(testKeys, { searchQuery: "zzznomatch" });
assert.equal(result.length, 0);
});
it("matches multiple keys with partial substring", () => {
const result = applyFilters(testKeys, { searchQuery: "key" });
assert.equal(result.length, 2);
});
});
@@ -0,0 +1,55 @@
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import {
maskKey,
toggleKeyVisibility,
} from "../../src/app/(dashboard)/dashboard/api-manager/apiManagerPageUtils.js";
describe("maskKey", () => {
it("returns empty string when key is missing or empty", () => {
assert.equal(maskKey(""), "");
assert.equal(maskKey(null), "");
assert.equal(maskKey(undefined), "");
});
it("returns the key untouched when it fits the visible budget (<=8 chars)", () => {
assert.equal(maskKey("sk"), "sk");
assert.equal(maskKey("sk-12345"), "sk-12345");
});
it("does not double-mask API keys that are already masked by the server", () => {
assert.equal(maskKey("sk-live-****1002"), "sk-live-****1002");
});
it("keeps the first 8 chars and appends an ellipsis when the key is longer", () => {
const full = "sk-or-1234567890abcdef";
const masked = maskKey(full);
assert.equal(masked.startsWith("sk-or-12"), true);
assert.equal(masked.endsWith("..."), true);
// Must not leak the tail
assert.equal(masked.includes("90abcdef"), false);
});
});
describe("toggleKeyVisibility", () => {
it("adds an id when it is not present", () => {
const next = toggleKeyVisibility(new Set<string>(), "k1");
assert.equal(next.has("k1"), true);
assert.equal(next.size, 1);
});
it("removes an id when it is already present", () => {
const next = toggleKeyVisibility(new Set<string>(["k1", "k2"]), "k1");
assert.equal(next.has("k1"), false);
assert.equal(next.has("k2"), true);
assert.equal(next.size, 1);
});
it("returns a NEW Set (does not mutate the input — React state safety)", () => {
const input = new Set<string>(["k1"]);
const output = toggleKeyVisibility(input, "k2");
assert.notEqual(output, input);
assert.equal(input.has("k2"), false);
assert.equal(output.has("k2"), true);
});
});

Some files were not shown because too many files have changed in this diff Show More