SysDeck/klanker-gate/tests/e2e/ui_serving_test.ts

112 lines
3.9 KiB
TypeScript
Executable File

// Same-origin UI serving: the gateway serves the built SPA and its assets
// alongside the API with no CORS involvement.
import { assert, assertEquals } from "@std/assert";
import { fromFileUrl } from "@std/path";
import { createHandler } from "../../apps/gateway/main.ts";
import {
type AppContext,
NullToolExecutor,
VERSION,
} from "../../apps/gateway/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";
const distUrl = new URL("../../apps/control-ui/dist", import.meta.url);
let distPath: string | undefined;
try {
if (Deno.statSync(distUrl).isDirectory) {
distPath = fromFileUrl(distUrl);
}
} catch {
distPath = undefined;
}
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,
uiRoot: distPath,
};
}
Deno.test({
name: "e2e: gateway serves the control UI same-origin",
ignore: !distPath, // run `deno task build-ui` first
fn: async (t) => {
const server = Deno.serve(
{ port: 0, onListen: () => {} },
createHandler(makeContext()),
);
const base = `http://127.0.0.1:${(server.addr as Deno.NetAddr).port}`;
try {
let assetPath = "";
await t.step("/ serves index.html", async () => {
const res = await fetch(`${base}/`);
assertEquals(res.status, 200);
assert(res.headers.get("Content-Type")?.includes("text/html"));
const html = await res.text();
assert(html.includes("Klanker Gateway Manager"));
const match = html.match(/src="(\/assets\/[^"]+\.js)"/);
assert(match, "index.html references a bundled asset");
assetPath = match[1];
});
await t.step("bundled assets are served", async () => {
const res = await fetch(`${base}${assetPath}`);
assertEquals(res.status, 200);
await res.body?.cancel();
});
await t.step("unknown SPA routes fall back to index.html", async () => {
const res = await fetch(`${base}/some/client/route`);
assertEquals(res.status, 200);
assert((await res.text()).includes("Klanker Gateway Manager"));
});
await t.step("API routes still win over static serving", async () => {
const res = await fetch(`${base}/v1/models`);
assertEquals(res.status, 200);
assertEquals((await res.json()).object, "list");
const missing = await fetch(`${base}/api/providers/nope`, {
method: "DELETE",
});
assertEquals(missing.status, 404);
const body = await missing.json();
assertEquals(typeof body.error.message, "string");
});
await t.step("log stream is live SSE", async () => {
const controller = new AbortController();
const res = await fetch(`${base}/api/logs/stream`, {
signal: controller.signal,
});
assertEquals(res.headers.get("Content-Type"), "text/event-stream");
// trigger a request so the stream has something to say
const health = await fetch(`${base}/healthz`);
await health.body?.cancel();
const reader = res.body!.getReader();
const { value } = await reader.read();
assert(new TextDecoder().decode(value).startsWith("data: "));
controller.abort();
await reader.cancel().catch(() => {});
});
} finally {
await server.shutdown();
}
},
});