import { afterEach, beforeEach, describe, expect, it } from "bun:test"; import express from "express"; import { mkdtemp, rm } from "node:fs/promises"; import { request as httpRequest, type Server } from "node:http"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { buildChatRouter } from "../../src/routes/chat.js"; import { SessionMetadataStore } from "../../src/sessions/metadataStore.js"; import { toActorKey, toProjectKey } from "../../src/utils/fileStore.js"; const originalFetch = globalThis.fetch; describe("chat session HTTP isolation", () => { let metadataDir: string; let server: Server; beforeEach(async () => { metadataDir = await mkdtemp(join(tmpdir(), "tjwater-route-sessions-")); }); afterEach(async () => { globalThis.fetch = originalFetch; if (server) { await new Promise((resolve, reject) => { server.close((error) => (error ? reject(error) : resolve())); }); } await rm(metadataDir, { force: true, recursive: true }); }); it("hides task sessions from ordinary history and scopes every list to the verified user and project", async () => { globalThis.fetch = (async (_input, init) => { const headers = new Headers(init?.headers); const authorization = headers.get("authorization"); const projectId = headers.get("x-project-id"); const userId = authorization === "Bearer token-a" ? "user-a" : "user-b"; return Response.json({ user_id: userId, project_id: projectId, network: "supply", }); }) as typeof fetch; const sessionMetadataStore = new SessionMetadataStore(metadataDir); await sessionMetadataStore.initialize(); const ownerContext = { actorKey: toActorKey("product:user-a"), projectId: "10000000-0000-4000-8000-000000000001", projectKey: toProjectKey("product:10000000-0000-4000-8000-000000000001"), userId: "user-a", }; await sessionMetadataStore.ensure({ ...ownerContext, sessionId: "ordinary-session", }); await sessionMetadataStore.ensure({ ...ownerContext, sessionId: "execution-session", purpose: "task_execution", taskId: "50000000-0000-4000-8000-000000000001", requestId: "60000000-0000-4000-8000-000000000001", }); const app = express(); app.use(express.json()); app.use("/api/v1/agent/chat", buildChatRouter({ frontendActionCoordinator: {} as never, learningOrchestrator: {} as never, memoryStore: {} as never, resultReferenceResolver: {} as never, runtime: {} as never, sessionBridge: {} as never, sessionMetadataStore, sessionTranscriptStore: {} as never, sessionUiStateStore: {} as never, })); server = app.listen(0, "127.0.0.1"); await new Promise((resolve) => server.once("listening", resolve)); const address = server.address(); if (!address || typeof address === "string") throw new Error("expected HTTP server address"); const ownedOrdinary = await getJson(address.port, "/api/v1/agent/chat/sessions", "token-a", ownerContext.projectId); const ownedExecution = await getJson(address.port, "/api/v1/agent/chat/sessions?purpose=task_execution", "token-a", ownerContext.projectId); const otherUser = await getJson(address.port, "/api/v1/agent/chat/sessions?purpose=task_execution", "token-b", ownerContext.projectId); const otherProject = await getJson(address.port, "/api/v1/agent/chat/sessions?purpose=task_execution", "token-a", "10000000-0000-4000-8000-000000000002"); expect(ownedOrdinary.body.sessions.map((item: { id: string }) => item.id)).toEqual(["ordinary-session"]); expect(ownedExecution.body.sessions.map((item: { id: string }) => item.id)).toEqual(["execution-session"]); expect(otherUser.body.sessions).toEqual([]); expect(otherProject.body.sessions).toEqual([]); }); }); const getJson = ( port: number, path: string, token: string, projectId: string, ) => new Promise<{ status: number; body: Record }>((resolve, reject) => { const request = httpRequest({ hostname: "127.0.0.1", port, path, method: "GET", headers: { Authorization: `Bearer ${token}`, "X-Project-Id": projectId, }, }, (response) => { const chunks: Buffer[] = []; response.on("data", (chunk) => chunks.push(Buffer.from(chunk))); response.on("end", () => { resolve({ status: response.statusCode ?? 0, body: JSON.parse(Buffer.concat(chunks).toString("utf8")), }); }); }); request.on("error", reject); request.end(); });