53 lines
3.8 KiB
TypeScript
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 });
|
|
}
|
|
});
|