137 lines
4.2 KiB
TypeScript
137 lines
4.2 KiB
TypeScript
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
import express, { Router } from "express";
|
|
import type { Server } from "node:http";
|
|
|
|
import { generateAgentOpenApi } from "../../src/contracts/openapi.js";
|
|
import { buildChatRouter } from "../../src/routes/chat.js";
|
|
import { buildAgentPublicRouter } from "../../src/routes/publicApi.js";
|
|
|
|
describe("Agent public REST router", () => {
|
|
let baseUrl = "";
|
|
let server: Server;
|
|
|
|
beforeAll(async () => {
|
|
const chatRouter = Router();
|
|
chatRouter.use(express.json());
|
|
chatRouter.post("/sessions/:session_id/runs", (req, res) => {
|
|
res.json({ method: req.method, path: req.path, body: req.body });
|
|
});
|
|
chatRouter.delete("/sessions/:session_id/runs/current", (req, res) => {
|
|
res.json({ method: req.method, path: req.path, body: req.body });
|
|
});
|
|
chatRouter.get("/auth-failure", (_req, res) => {
|
|
res.status(503).json({ message: "authentication service unavailable" });
|
|
});
|
|
|
|
const app = express();
|
|
app.use(express.json());
|
|
app.use("/api/v1/agent", buildAgentPublicRouter(chatRouter));
|
|
server = app.listen(0);
|
|
await new Promise<void>((resolve) => server.once("listening", resolve));
|
|
const address = server.address();
|
|
if (!address || typeof address === "string") {
|
|
throw new Error("test server did not expose a TCP port");
|
|
}
|
|
baseUrl = `http://127.0.0.1:${address.port}`;
|
|
});
|
|
|
|
afterAll(() => {
|
|
server.close();
|
|
});
|
|
|
|
test("matches the published OpenAPI method and path set", () => {
|
|
type RouterLayer = {
|
|
route?: {
|
|
methods: Record<string, boolean>;
|
|
path: string;
|
|
};
|
|
};
|
|
const router = buildChatRouter(
|
|
undefined as never,
|
|
undefined as never,
|
|
undefined as never,
|
|
undefined as never,
|
|
undefined as never,
|
|
undefined as never,
|
|
undefined as never,
|
|
undefined as never,
|
|
);
|
|
const layers = (router as unknown as { stack: RouterLayer[] }).stack;
|
|
const runtimeOperations = layers
|
|
.flatMap((layer) => {
|
|
if (!layer.route) {
|
|
return [];
|
|
}
|
|
const path = `/api/v1/agent${layer.route.path.replace(
|
|
/:([^/]+)/g,
|
|
"{$1}",
|
|
)}`;
|
|
return Object.entries(layer.route.methods)
|
|
.filter(([, enabled]) => enabled)
|
|
.map(([method]) => `${method.toUpperCase()} ${path}`);
|
|
})
|
|
.sort();
|
|
const document = generateAgentOpenApi();
|
|
const contractOperations = Object.entries(document.paths)
|
|
.flatMap(([path, pathItem]) =>
|
|
Object.entries(pathItem ?? {})
|
|
.filter(([, operation]) => {
|
|
return (
|
|
operation &&
|
|
typeof operation === "object" &&
|
|
"responses" in operation
|
|
);
|
|
})
|
|
.map(([method]) => `${method.toUpperCase()} ${path}`),
|
|
)
|
|
.sort();
|
|
|
|
expect(runtimeOperations).toEqual(contractOperations);
|
|
});
|
|
|
|
test("serves session runs without rewriting the REST request", async () => {
|
|
const response = await fetch(
|
|
`${baseUrl}/api/v1/agent/sessions/session-1/runs`,
|
|
{
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ message: "hello" }),
|
|
},
|
|
);
|
|
|
|
expect(await response.json()).toEqual({
|
|
method: "POST",
|
|
path: "/sessions/session-1/runs",
|
|
body: { message: "hello" },
|
|
});
|
|
});
|
|
|
|
test("serves DELETE current run without rewriting the HTTP method", async () => {
|
|
const response = await fetch(
|
|
`${baseUrl}/api/v1/agent/sessions/session-2/runs/current`,
|
|
{ method: "DELETE" },
|
|
);
|
|
|
|
expect(await response.json()).toEqual({
|
|
method: "DELETE",
|
|
path: "/sessions/session-2/runs/current",
|
|
body: {},
|
|
});
|
|
});
|
|
|
|
test("wraps authentication middleware failures as Problem Details", async () => {
|
|
const response = await fetch(`${baseUrl}/api/v1/agent/auth-failure`);
|
|
const body = await response.json();
|
|
|
|
expect(response.status).toBe(503);
|
|
expect(response.headers.get("content-type")).toContain(
|
|
"application/problem+json",
|
|
);
|
|
expect(body).toMatchObject({
|
|
status: 503,
|
|
code: "dependency_unavailable",
|
|
detail: "authentication service unavailable",
|
|
});
|
|
});
|
|
});
|