299 lines
9.8 KiB
TypeScript
Executable File
299 lines
9.8 KiB
TypeScript
Executable File
// Wave-4 verification-pass fixes: governance coverage of the translated
|
|
// compat families, usage-shape normalization, streaming cost accounting,
|
|
// cache-hit exemption, binary-body estimates, Retry-After fidelity,
|
|
// /metrics auth, /mcp batch rejection, rate-limit window integrity,
|
|
// fail-open semantic cache, and plugin isolation.
|
|
|
|
import { assert, assertEquals } from "@std/assert";
|
|
import { createHandler } from "../../apps/gateway/main.ts";
|
|
import { type AppContext, VERSION } from "../../apps/gateway/context.ts";
|
|
import { governanceMiddleware } from "../../apps/gateway/routes/governance.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 {
|
|
type VirtualKey,
|
|
VirtualKeyManager,
|
|
} from "../../packages/governance/src/virtual_keys.ts";
|
|
import { PricingCatalog } from "../../packages/governance/src/pricing.ts";
|
|
import { RateLimiter } from "../../packages/governance/src/rate_limit.ts";
|
|
import { MCPRegistry } from "../../packages/mcp/src/registry.ts";
|
|
import { PluginManager } from "../../packages/plugins/src/lifecycle.ts";
|
|
import { SemanticCache } from "../../packages/cache/src/semantic.ts";
|
|
import { InMemoryVectorStore } from "../../packages/cache/src/vector.ts";
|
|
|
|
const TOKEN = "vk-wave4-secret";
|
|
|
|
function keyed(overrides: Partial<VirtualKey> = {}): VirtualKey {
|
|
return {
|
|
id: "k1",
|
|
name: "wave4",
|
|
token: TOKEN,
|
|
enabled: true,
|
|
usedRequests: 0,
|
|
usedCostMicroUsd: 0,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function govContext(key: VirtualKey = keyed()): AppContext {
|
|
const mcp = new MCPRegistry();
|
|
return {
|
|
providers: new ProviderManager(),
|
|
metrics: new Metrics(),
|
|
logBus: new LogBus(),
|
|
virtualKeys: new VirtualKeyManager([key]),
|
|
pricing: new PricingCatalog(),
|
|
mcp,
|
|
plugins: new PluginManager(),
|
|
toolExecutor: mcp.executor(),
|
|
version: VERSION,
|
|
};
|
|
}
|
|
|
|
function authed(
|
|
url: string,
|
|
body = "{}",
|
|
headers: Record<string, string> = {},
|
|
): Request {
|
|
return new Request(url, {
|
|
method: "POST",
|
|
headers: {
|
|
Authorization: `Bearer ${TOKEN}`,
|
|
"Content-Type": "application/json",
|
|
...headers,
|
|
},
|
|
body,
|
|
});
|
|
}
|
|
|
|
Deno.test("governance covers the /genai and /cohere translated families", async () => {
|
|
const handler = createHandler(govContext());
|
|
for (
|
|
const path of [
|
|
"/genai/v1beta/models/gpt-4o:generateContent",
|
|
"/cohere/v2/chat",
|
|
]
|
|
) {
|
|
const res = await handler(
|
|
new Request(`http://g.test${path}`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: "{}",
|
|
}),
|
|
);
|
|
assertEquals(res.status, 401, path);
|
|
// A genai path answers in the GenAI error envelope (decision-log 86), so
|
|
// the gateway code rides `details[].reason`; /cohere keeps the canonical
|
|
// envelope. Admission itself is identical on both - that is what this
|
|
// test guards.
|
|
const body = await res.json() as {
|
|
error: { code: unknown; details?: Array<{ reason?: string }> };
|
|
};
|
|
const gatewayCode = typeof body.error.code === "string"
|
|
? body.error.code
|
|
: body.error.details?.[0]?.reason;
|
|
assertEquals(gatewayCode, "missing_virtual_key", path);
|
|
}
|
|
});
|
|
|
|
Deno.test("Anthropic-shaped usage (input/output_tokens) is billed", async () => {
|
|
const ctx = govContext(keyed({ budget: { maxCostUsd: 10 } }));
|
|
const mw = governanceMiddleware(ctx);
|
|
const res = await mw(
|
|
authed("http://g.test/v1/messages"),
|
|
() =>
|
|
Promise.resolve(Response.json({
|
|
model: "gpt-4o",
|
|
usage: { input_tokens: 1_000_000, output_tokens: 500_000 },
|
|
})),
|
|
);
|
|
assertEquals(res.status, 200);
|
|
// 1M in @ $2.5/M + 0.5M out @ $10/M = $7.50 = 7_500_000 micro-USD.
|
|
assertEquals(ctx.virtualKeys.get("k1")?.usedCostMicroUsd, 7_500_000);
|
|
});
|
|
|
|
Deno.test("streaming responses are billed from the SSE usage tail", async () => {
|
|
const ctx = govContext(keyed({ budget: { maxCostUsd: 10 } }));
|
|
const mw = governanceMiddleware(ctx);
|
|
const sse = [
|
|
`data: {"model":"gpt-4o","choices":[{"delta":{"content":"hi"}}]}`,
|
|
"",
|
|
`data: {"model":"gpt-4o","choices":[],"usage":{"prompt_tokens":1000000,"completion_tokens":500000}}`,
|
|
"",
|
|
"data: [DONE]",
|
|
"",
|
|
].join("\n");
|
|
const res = await mw(
|
|
authed("http://g.test/v1/chat/completions"),
|
|
() =>
|
|
Promise.resolve(
|
|
new Response(sse, {
|
|
headers: { "Content-Type": "text/event-stream" },
|
|
}),
|
|
),
|
|
);
|
|
const body = await res.text(); // consume: accounting runs at flush
|
|
assert(body.includes("[DONE]"));
|
|
assertEquals(ctx.virtualKeys.get("k1")?.usedCostMicroUsd, 7_500_000);
|
|
});
|
|
|
|
Deno.test("cache hits are not billed against cost budgets", async () => {
|
|
const ctx = govContext(keyed({ budget: { maxCostUsd: 10 } }));
|
|
const mw = governanceMiddleware(ctx);
|
|
const res = await mw(
|
|
authed("http://g.test/v1/chat/completions"),
|
|
() => {
|
|
const hit = Response.json({
|
|
model: "gpt-4o",
|
|
usage: { prompt_tokens: 1_000_000, completion_tokens: 500_000 },
|
|
});
|
|
hit.headers.set("x-frosty-cache", "hit");
|
|
return Promise.resolve(hit);
|
|
},
|
|
);
|
|
assertEquals(res.status, 200);
|
|
assertEquals(ctx.virtualKeys.get("k1")?.usedCostMicroUsd, 0);
|
|
});
|
|
|
|
Deno.test("binary bodies are not token-estimated (JSON only)", async () => {
|
|
const ctx = govContext(
|
|
keyed({ tokenLimit: { maxTokens: 10, windowMs: 60_000 } }),
|
|
);
|
|
const mw = governanceMiddleware(ctx);
|
|
const next = () => Promise.resolve(new Response("ok"));
|
|
|
|
// 4KB binary upload: estimate must be 0 (admitted, consuming 1 token).
|
|
const binary = await mw(
|
|
new Request("http://g.test/v1/audio/transcriptions", {
|
|
method: "POST",
|
|
headers: {
|
|
Authorization: `Bearer ${TOKEN}`,
|
|
"Content-Type": "application/octet-stream",
|
|
},
|
|
body: new Uint8Array(4096),
|
|
}),
|
|
next,
|
|
);
|
|
assertEquals(binary.status, 200);
|
|
|
|
// The same size as JSON blows the 10-token window.
|
|
const json = await mw(
|
|
authed("http://g.test/v1/chat/completions", "x".repeat(4096)),
|
|
next,
|
|
);
|
|
assertEquals(json.status, 429);
|
|
});
|
|
|
|
Deno.test("Retry-After reflects the actual window, not a fixed 60s", async () => {
|
|
const ctx = govContext(
|
|
keyed({ rateLimit: { maxRequests: 1, windowMs: 5_000 } }),
|
|
);
|
|
const mw = governanceMiddleware(ctx);
|
|
const next = () => Promise.resolve(new Response("ok"));
|
|
assertEquals((await mw(authed("http://g.test/v1/x"), next)).status, 200);
|
|
const limited = await mw(authed("http://g.test/v1/x"), next);
|
|
assertEquals(limited.status, 429);
|
|
const retryAfter = Number(limited.headers.get("Retry-After"));
|
|
assert(retryAfter >= 1 && retryAfter <= 5, `Retry-After=${retryAfter}`);
|
|
});
|
|
|
|
Deno.test("/metrics requires the admin token when one is configured", async () => {
|
|
const ctx = { ...govContext(), adminToken: "admin-secret-1" };
|
|
const handler = createHandler(ctx);
|
|
const anon = await handler(new Request("http://g.test/metrics"));
|
|
assertEquals(anon.status, 401);
|
|
const authed_ = await handler(
|
|
new Request("http://g.test/metrics", {
|
|
headers: { Authorization: "Bearer admin-secret-1" },
|
|
}),
|
|
);
|
|
assertEquals(authed_.status, 200);
|
|
});
|
|
|
|
Deno.test("/mcp rejects JSON-RPC batches instead of swallowing them", async () => {
|
|
const ctx = govContext();
|
|
ctx.virtualKeys = new VirtualKeyManager(); // ungoverned for this test
|
|
const handler = createHandler(ctx);
|
|
const res = await handler(
|
|
new Request("http://g.test/mcp", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify([{
|
|
jsonrpc: "2.0",
|
|
id: 1,
|
|
method: "tools/list",
|
|
}]),
|
|
}),
|
|
);
|
|
const body = await res.json() as { error: { code: number } };
|
|
assertEquals(body.error.code, -32600);
|
|
});
|
|
|
|
Deno.test("rate limiter: denied oversized request cannot poison a fresh window", () => {
|
|
const limiter = new RateLimiter({ maxRequests: 60, windowMs: 60_000 });
|
|
assertEquals(limiter.consume("k", 8000, 60_000, 10_000), false);
|
|
// The window must still admit normal traffic afterwards.
|
|
assertEquals(limiter.consume("k", 8000, 60_000, 10), true);
|
|
});
|
|
|
|
Deno.test("semantic cache fails open when the embedder is down", async () => {
|
|
const cache = new SemanticCache({
|
|
embedder: () => Promise.reject(new Error("embeddings outage")),
|
|
vectorStore: new InMemoryVectorStore(),
|
|
});
|
|
const request = { model: "m", messages: [{ role: "user", content: "q" }] };
|
|
assertEquals(await cache.get(request), null); // miss, not throw
|
|
const response = {
|
|
id: "r1",
|
|
object: "chat.completion",
|
|
created: 0,
|
|
model: "m",
|
|
choices: [],
|
|
};
|
|
await cache.set(request, response as never); // stores exact, skips vector
|
|
assert(cache.size() > 0);
|
|
});
|
|
|
|
Deno.test("expired vector entries do not shadow fresh ones", async () => {
|
|
const store = new InMemoryVectorStore();
|
|
const vector = [1, 0, 0];
|
|
const fresh = { id: "fresh" };
|
|
await store.upsert("old", vector, {
|
|
response: { id: "stale" },
|
|
storedAt: Date.now() - 3_600_000,
|
|
});
|
|
await store.upsert("new", vector, {
|
|
response: fresh,
|
|
storedAt: Date.now(),
|
|
});
|
|
const cache = new SemanticCache({
|
|
ttlMs: 60_000,
|
|
embedder: () => Promise.resolve(vector),
|
|
vectorStore: store,
|
|
});
|
|
const got = await cache.get({
|
|
model: "m",
|
|
messages: [{ role: "user", content: "q" }],
|
|
}) as { id: string } | null;
|
|
assertEquals(got?.id, "fresh");
|
|
});
|
|
|
|
Deno.test("a throwing stream-complete plugin neither aborts nor starves peers", async () => {
|
|
const plugins = new PluginManager();
|
|
const seen: string[] = [];
|
|
plugins.register({
|
|
name: "boom",
|
|
onStreamComplete: () => Promise.reject(new Error("down")),
|
|
});
|
|
plugins.register({
|
|
name: "tail",
|
|
onStreamComplete: (text) => {
|
|
seen.push(text);
|
|
return Promise.resolve();
|
|
},
|
|
});
|
|
await plugins.executeStreamComplete("hello"); // must not throw
|
|
assertEquals(seen, ["hello"]);
|
|
});
|