Files
next-tjwater-agent/tests/routes/chatInteractionBridge.test.ts
T

53 lines
3.8 KiB
TypeScript

import { expect, test } from "bun:test";
import express from "express";
import { mkdtemp, rm } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { registerChatInteractionBridge } from "../../src/routes/chat-interaction-bridge.js";
import { setRuntimeSessionContext, removeRuntimeSessionContext } from "../../src/runtime/sessionContext.js";
import { InteractionRequestStore } from "../../src/sessions/interaction-request-store.js";
import { SessionWorkspaceStore } from "../../src/sessions/workspace-store.js";
test("trusted gate binds host identity, rejects parallel tools and persists waiting before tool leave/idle", async () => {
const root = await mkdtemp(join(tmpdir(), "interaction-bridge-"));
const context = { sessionId: "bridge-session", clientSessionId: "bridge-session", actorKey: "trusted-actor", projectKey: "trusted-project", traceId: "test", hostTurnId: "host-turn", hostMessageId: "host-message", capabilities: ["host-question@1"], purpose: "ordinary_chat" as const };
const workspace = new SessionWorkspaceStore(root); await workspace.ensure(context);
const interactions = new InteractionRequestStore(workspace);
setRuntimeSessionContext(context);
const app = express(); app.use(express.json()); registerChatInteractionBridge(app, { internalToken: "token", interactions });
const server = app.listen(0, "127.0.0.1"); await new Promise<void>((resolve) => server.once("listening", resolve));
const base = `http://127.0.0.1:${(server.address() as { port: number }).port}`;
const post = (operation: string, body: unknown, headers: Record<string, string> = {}) => fetch(`${base}/internal/chat-interactions/${operation}`, { method: "POST", headers: { "Content-Type": "application/json", "x-agent-internal-token": "token", ...headers }, body: JSON.stringify({ session_id: context.sessionId, ...body as object }) });
const ask = { tool: "ask_user", call_id: "ask" };
const query = { tool: "mcp_query", call_id: "query" };
const questions = [{ question_id: "q", title: "Title", body: "Question", options: [], mode: "single", allow_custom: true, required: true }];
try {
expect((await post("enter", ask, { "x-agent-internal-token": "wrong" })).status).toBe(403);
expect((await post("enter", ask, { Origin: "https://browser.test" })).status).toBe(403);
expect((await post("create", { ...ask, questions, actorKey: "forged" })).status).toBe(400);
expect((await post("create", { ...ask, questions })).status).toBe(409);
expect((await post("enter", query)).status).toBe(200);
expect((await post("enter", ask)).status).toBe(409);
await post("leave", query);
expect((await post("enter", ask)).status).toBe(200);
expect((await post("enter", query)).status).toBe(409);
const response = await post("create", { ...ask, questions });
expect(response.status).toBe(200);
const record = await response.json();
expect(record).toMatchObject({ message_id: "host-message", turn_id: "host-turn", call_id: "ask", status: "pending" });
expect(await interactions.get(context, record.interaction_id)).toEqual(record);
expect(await (await post("policy", {})).json()).toEqual({ enabled: true, waiting: true });
await post("leave", ask);
expect((await post("enter", query)).status).toBe(409);
expect((await post("enter", { tool: "question", call_id: "native" })).status).toBe(409);
await interactions.cancelPending(context);
expect((await post("enter", query)).status).toBe(409);
setRuntimeSessionContext({ ...context, hostTurnId: "next-turn" });
expect((await post("enter", query)).status).toBe(200);
} finally {
removeRuntimeSessionContext(context.sessionId);
await new Promise<void>((resolve) => server.close(() => resolve()));
await rm(root, { recursive: true, force: true });
}
});