SysDeck/klanker-gate/apps/gateway/routes/admin_test.ts

278 lines
9.1 KiB
TypeScript
Executable File

import { assert, assertEquals } from "@std/assert";
import { createHandler } from "../main.ts";
import { type AppContext, NullToolExecutor, VERSION } from "../context.ts";
import { ProviderManager } from "../../../packages/providers/src/mod.ts";
import { Metrics } from "../../../packages/telemetry/src/metrics.ts";
import { LogBus } from "../../../packages/telemetry/src/logbus.ts";
import { VirtualKeyManager } from "../../../packages/governance/src/virtual_keys.ts";
import { MCPRegistry } from "../../../packages/mcp/src/registry.ts";
import { PluginManager } from "../../../packages/plugins/src/lifecycle.ts";
function makeContext(): AppContext {
return {
providers: new ProviderManager([]),
metrics: new Metrics(),
logBus: new LogBus(),
virtualKeys: new VirtualKeyManager(),
mcp: new MCPRegistry(),
plugins: new PluginManager(),
toolExecutor: new NullToolExecutor(),
version: VERSION,
};
}
const base = "http://gateway.test";
const JSON_HEADERS = { "Content-Type": "application/json" };
Deno.test("POST /api/providers round-trips 6-tab fields and redacts secrets", async () => {
const handler = createHandler(makeContext());
const res = await handler(
new Request(`${base}/api/providers`, {
method: "POST",
headers: JSON_HEADERS,
body: JSON.stringify({
id: "anthropic",
type: "anthropic",
apiKey: "sk-ant-secret",
models: ["claude-3-5-sonnet"],
proxyUrl: "http://user:pw@proxy.local",
network: {
maxRetries: 4,
extraHeaders: [{ name: "X-Org", value: "acme" }],
caCertPem: "-----BEGIN CERTIFICATE-----secret",
},
proxy: {
proxyType: "http",
proxyUsername: "u",
proxyPassword: "shh",
noProxy: [".private"],
},
governance: { budgetUsd: 50, budgetResetPeriod: "monthly" },
betaHeaders: { overrides: { "prompt-caching-2024-07-31": "enabled" } },
debugging: { sendBackRawResponse: true },
}),
}),
);
assertEquals(res.status, 201);
const body = await res.json();
// Non-secret 6-tab fields survive the round-trip.
assertEquals(body.network.maxRetries, 4);
assertEquals(body.network.extraHeaders[0], { name: "X-Org", hasValue: true });
assertEquals(body.proxy.proxyUsername, "u");
assertEquals(body.proxy.noProxyCount, 1);
assertEquals(body.governance.budgetUsd, 50);
assertEquals(
body.betaHeaders.overrides["prompt-caching-2024-07-31"],
"enabled",
);
assertEquals(body.debugging.sendBackRawResponse, true);
// Presence flags replace secrets.
assertEquals(body.hasApiKey, true);
assertEquals(body.hasProxy, true);
assertEquals(body.hasProxyPassword, true);
assertEquals(body.hasCaCert, true);
// No secret material is echoed anywhere in the response.
const serialized = JSON.stringify(body);
for (
const secret of [
"sk-ant-secret",
"pw@proxy.local",
"shh",
"BEGIN CERTIFICATE",
"acme",
".private",
]
) {
assert(!serialized.includes(secret), `leaked secret: ${secret}`);
}
});
Deno.test("PUT /api/providers/:id merges new-group fields", async () => {
const ctx = makeContext();
const handler = createHandler(ctx);
await handler(
new Request(`${base}/api/providers`, {
method: "POST",
headers: JSON_HEADERS,
body: JSON.stringify({
id: "openai",
type: "openai",
apiKey: "sk",
models: ["gpt-4o"],
}),
}),
);
const res = await handler(
new Request(`${base}/api/providers/openai`, {
method: "PUT",
headers: JSON_HEADERS,
body: JSON.stringify({
performance: { maxConcurrentRequests: 8 },
network: { timeoutSec: 30 },
}),
}),
);
assertEquals(res.status, 200);
const body = await res.json();
assertEquals(body.performance.maxConcurrentRequests, 8);
assertEquals(body.network.timeoutSec, 30);
// Existing fields are preserved through the merge.
assertEquals(body.hasApiKey, true);
assertEquals(body.models, ["gpt-4o"]);
// The stored config carries the secret internally but the listing hides it.
const stored = ctx.providers.get("openai");
assertEquals(stored?.apiKey, "sk");
const listRes = await handler(new Request(`${base}/api/providers`));
const list = await listRes.json();
assertEquals(list.providers[0].network.timeoutSec, 30);
assert(!("apiKey" in list.providers[0]));
});
Deno.test("GET /api/config exposes the EUR display rate", async () => {
const handler = createHandler(makeContext());
const res = await handler(new Request(`${base}/api/config`));
assertEquals(res.status, 200);
const body = await res.json();
assertEquals(typeof body.eurRate, "number");
assert(body.eurRate > 0);
});
Deno.test("GET /api/providers/health reports per-provider status", async () => {
const handler = createHandler(makeContext());
// Disabled account: reported without a probe.
await handler(
new Request(`${base}/api/providers`, {
method: "POST",
headers: JSON_HEADERS,
body: JSON.stringify({ id: "off", type: "openai", enabled: false }),
}),
);
// Enabled but no credentials: cannot be probed -> "unknown", never "ok".
await handler(
new Request(`${base}/api/providers`, {
method: "POST",
headers: JSON_HEADERS,
body: JSON.stringify({ id: "nokey", type: "openai", enabled: true }),
}),
);
const res = await handler(new Request(`${base}/api/providers/health`));
assertEquals(res.status, 200);
const body = await res.json();
const byId = Object.fromEntries(
body.health.map((h: { id: string }) => [h.id, h]),
);
assertEquals(byId["off"].status, "disabled");
assertEquals(byId["nokey"].status, "unknown");
// Un-probed accounts carry no error text.
assertEquals(byId["off"].lastError, undefined);
assertEquals(byId["nokey"].lastError, undefined);
});
Deno.test("GET /api/providers/health reports 'ok' for a reachable enabled provider", async () => {
// Regression: the probe must invoke listModels AS A METHOD (bound `this`).
// A detached call throws a TypeError before any I/O, which the probe would
// mislabel as "error" for every enabled provider (the vLLM online-badge bug).
const originalFetch = globalThis.fetch;
let hitModels = false;
globalThis.fetch = ((input: RequestInfo | URL) => {
if (String(input).endsWith("/models")) {
hitModels = true;
return Promise.resolve(
new Response(JSON.stringify({ data: [{ id: "gpt-4o-mini" }] }), {
headers: { "Content-Type": "application/json" },
}),
);
}
return Promise.resolve(new Response("not found", { status: 404 }));
}) as typeof fetch;
try {
const handler = createHandler(makeContext());
await handler(
new Request(`${base}/api/providers`, {
method: "POST",
headers: JSON_HEADERS,
body: JSON.stringify({
id: "live",
type: "openai",
apiKey: "sk-test",
enabled: true,
}),
}),
);
const res = await handler(new Request(`${base}/api/providers/health`));
assertEquals(res.status, 200);
const body = await res.json();
const live = body.health.find((h: { id: string }) => h.id === "live");
assertEquals(live.status, "ok");
assertEquals(live.lastError, undefined);
assert(hitModels, "probe should have hit the live /models endpoint");
} finally {
globalThis.fetch = originalFetch;
}
});
Deno.test("GET /api/providers/:id/available-models lists live models without persisting", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = ((input: RequestInfo | URL) => {
if (String(input).endsWith("/models")) {
return Promise.resolve(
new Response(
JSON.stringify({
data: [{ id: "gpt-4o" }, { id: "gpt-4o-mini" }, { id: "o1" }],
}),
{ headers: { "Content-Type": "application/json" } },
),
);
}
return Promise.resolve(new Response("not found", { status: 404 }));
}) as typeof fetch;
try {
const ctx = makeContext();
const handler = createHandler(ctx);
// Enabled subset is a single model; the live provider advertises three.
await handler(
new Request(`${base}/api/providers`, {
method: "POST",
headers: JSON_HEADERS,
body: JSON.stringify({
id: "oai",
type: "openai",
apiKey: "sk-test",
models: ["gpt-4o"],
enabled: true,
}),
}),
);
const res = await handler(
new Request(`${base}/api/providers/oai/available-models`),
);
assertEquals(res.status, 200);
const body = await res.json();
assertEquals(body.id, "oai");
assertEquals(body.models, ["gpt-4o", "gpt-4o-mini", "o1"]);
// Read-only: the enabled set the gateway routes on is left untouched.
assertEquals(ctx.providers.get("oai")?.models, ["gpt-4o"]);
} finally {
globalThis.fetch = originalFetch;
}
});
Deno.test("GET /api/providers/:id/available-models 404s an unknown provider", async () => {
const handler = createHandler(makeContext());
const res = await handler(
new Request(`${base}/api/providers/nope/available-models`),
);
assertEquals(res.status, 404);
await res.body?.cancel();
});