import assert from "node:assert/strict"; import { execFileSync } from "node:child_process"; import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join, resolve } from "node:path"; import test from "node:test"; import { fileURLToPath } from "node:url"; import Fastify from "fastify"; import sensible from "@fastify/sensible"; import type { AgentInlineInvoker } from "../src/agent-inline.js"; import type { VisibleChatHistoryMessage } from "../src/chat-history.js"; const serverRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const databaseDir = mkdtempSync(join(tmpdir(), "sybil-agent-inline-")); process.env.DATABASE_URL = `file:${join(databaseDir, "test.db")}`; process.env.ADMIN_TOKEN = "inline-agent-test-token"; process.env.OPENAI_API_KEY = ""; writeFileSync(join(databaseDir, "test.db"), ""); execFileSync(process.execPath, [join(serverRoot, "node_modules/prisma/build/index.js"), "migrate", "deploy"], { cwd: serverRoot, env: { ...process.env, PRISMA_HIDE_UPDATE_MESSAGE: "1" }, stdio: "pipe", }); const [{ registerRoutes }, { prisma }, { createAgentInlineInvoker }, { buildInlineAgentThreadContext }] = await Promise.all([ import("../src/routes.js"), import("../src/db.js"), import("../src/agent-inline.js"), import("../src/chat-history.js"), ]); const auth = { authorization: "Bearer inline-agent-test-token" }; const invocations: Parameters[0][] = []; const app = Fastify({ logger: false }); await app.register(sensible); await registerRoutes(app, { agentInlineInvoker: async (request) => { invocations.push(request); return "Synthetic Sybil reply"; }, }); await app.ready(); test.after(async () => { await app.close(); await prisma.$disconnect(); rmSync(databaseDir, { recursive: true, force: true }); }); test("mention invokes only the inline Agent, stores both sides, and replays by request ID", async () => { const chat = await prisma.chat.create({ data: { title: "Inline mention" } }); const body = { chatId: chat.id, clientRequestId: "inline-request-1", provider: "openai", model: "synthetic-unused-model", messages: [{ role: "user", content: "@SYBIL answer using this thread" }], }; const first = await app.inject({ method: "POST", url: "/v1/chat-completions", headers: auth, payload: body }); assert.equal(first.statusCode, 200, first.body); assert.equal(first.json().provider, "hermes-agent"); assert.equal(first.json().message.name, "Sybil Agent"); assert.equal(first.json().message.agentAside, true); assert.equal(first.json().message.content, "Synthetic Sybil reply"); assert.deepEqual(invocations.map(({ chatId, requestId }) => ({ chatId, requestId })), [{ chatId: chat.id, requestId: "inline-request-1" }]); const retry = await app.inject({ method: "POST", url: "/v1/chat-completions", headers: auth, payload: body }); assert.equal(retry.statusCode, 200, retry.body); assert.equal(retry.json().provider, "hermes-agent"); assert.equal(retry.json().message.name, "Sybil Agent"); assert.equal(retry.json().message.agentAside, true); assert.equal(retry.json().message.content, "Synthetic Sybil reply"); assert.equal(invocations.length, 1); const messages = await prisma.message.findMany({ where: { chatId: chat.id }, orderBy: [{ createdAt: "asc" }, { id: "asc" }] }); assert.equal(messages.length, 2); assert.equal((messages[0]!.metadata as Record).agentAside, true); assert.equal(messages[1]!.name, "Sybil Agent"); assert.equal((messages[1]!.metadata as Record).agentAside, true); }); test("ordinary message writes cannot claim backend-owned Agent aside provenance", async () => { const chat = await prisma.chat.create({ data: { title: "Reserved metadata" } }); const response = await app.inject({ method: "POST", url: `/v1/chats/${chat.id}/messages`, headers: auth, payload: { role: "user", content: "ordinary user message", metadata: { agentAside: true, retained: "metadata" } }, }); assert.equal(response.statusCode, 200, response.body); assert.deepEqual(response.json().message.metadata, { retained: "metadata" }); }); test("the actual inline Agent payload contains all paged visible messages and usable attachments", async () => { const chat = await prisma.chat.create({ data: { title: "Full inline context" } }); const timestamp = new Date("2026-09-02T12:00:00.000Z"); await prisma.message.createMany({ data: Array.from({ length: 121 }, (_, index) => ({ id: `inline-context-${String(index).padStart(3, "0")}`, chatId: chat.id, createdAt: timestamp, role: index % 2 ? "assistant" : "user", content: index === 0 ? "oldest-visible-message" : index === 120 ? "latest-visible-message" : `thread-${index}`, ...(index === 61 ? { metadata: { agentAside: true } } : {}), ...(index === 70 ? { metadata: { attachments: [ { kind: "text", id: "full-text-id", filename: "notes.txt", mimeType: "text/plain", sizeBytes: 13, text: "complete text" }, { kind: "image", id: "full-image-id", filename: "photo.png", mimeType: "image/png", sizeBytes: 3, dataUrl: "data:image/png;base64,YWJj" }, ] } } : {}), })), }); const response = await app.inject({ method: "POST", url: "/v1/chat-completions", headers: auth, payload: { chatId: chat.id, clientRequestId: "inline-context-request", provider: "openai", model: "synthetic-unused-model", messages: [{ role: "user", content: "@sybil use this whole thread" }], }, }); assert.equal(response.statusCode, 200, response.body); const context = invocations.at(-1)!.context; assert.equal(context.messages.length, 122); assert.equal(context.messages[0]!.content, "oldest-visible-message"); assert.equal(context.messages.at(-2)!.content, "latest-visible-message"); assert.equal(context.messages.at(-1)!.content, "@sybil use this whole thread"); assert.equal(context.messages.find((message) => message.id === "inline-context-061")?.agentAside, true); assert.deepEqual(context.messages.find((message) => message.id === "inline-context-070")?.attachments, [ { kind: "text", id: "full-text-id", filename: "notes.txt", mimeType: "text/plain", sizeBytes: 13, text: "complete text" }, { kind: "image", id: "full-image-id", filename: "photo.png", mimeType: "image/png", sizeBytes: 3, dataUrl: "data:image/png;base64,YWJj" }, ]); }); test("inline Agent HTTP invocation fails before sending when complete context exceeds its request limit", async () => { const messages: VisibleChatHistoryMessage[] = [{ id: "synthetic-message", createdAt: "2026-09-02T12:00:00.000Z", role: "user", content: "complete payload fixture", }]; const context = buildInlineAgentThreadContext("synthetic-chat", "@sybil answer", messages); let sent = 0; const invoke = createAgentInlineInvoker({ apiKey: "synthetic-agent-key", baseUrl: "http://agent.invalid", adminToken: "synthetic-admin-key", maxRequestBytes: 16, fetcher: async () => { sent += 1; return new Response(JSON.stringify({ status: "done", answerText: "must not be used" }), { status: 200 }); }, }); await assert.rejects(invoke({ chatId: "synthetic-chat", requestId: "synthetic-request", context }), /complete visible thread exceeds the Agent request limit/); assert.equal(sent, 0); assert.throws(() => buildInlineAgentThreadContext("synthetic-chat", "@sybil answer", [{ ...messages[0]!, attachments: [{ kind: "text", id: "partial", filename: "partial.txt", mimeType: "text/plain", sizeBytes: 4, text: "part", truncated: true }], }]), /stored only partially/); }); test("inline Agent HTTP request carries the complete thread as mandatory initial context", async () => { const messages: VisibleChatHistoryMessage[] = Array.from({ length: 121 }, (_, index) => ({ id: `payload-${index}`, createdAt: `2026-09-02T12:${String(Math.floor(index / 60)).padStart(2, "0")}:${String(index % 60).padStart(2, "0")}.000Z`, role: index % 2 ? "assistant" : "user", content: index === 0 ? "earliest payload turn" : index === 120 ? "latest payload turn" : `payload turn ${index}`, ...(index === 58 ? { agentAside: true as const } : {}), ...(index === 77 ? { attachments: [ { kind: "text" as const, id: "text-source", filename: "data.txt", mimeType: "text/plain", sizeBytes: 11, text: "full text data" }, { kind: "image" as const, id: "image-source", filename: "frame.png", mimeType: "image/png" as const, sizeBytes: 3, dataUrl: "data:image/png;base64,YWJj" }, ] } : {}), })); const context = buildInlineAgentThreadContext("payload-chat", "@Sybil answer this", messages); let sentBody: Record | undefined; const invoke = createAgentInlineInvoker({ apiKey: "synthetic-agent-key", baseUrl: "http://agent.invalid", adminToken: "synthetic-admin-key", fetcher: async (_input, init) => { sentBody = JSON.parse(String(init?.body)); return new Response(JSON.stringify({ status: "done", answerText: "synthetic inline response" }), { status: 200 }); }, }); assert.equal(await invoke({ chatId: "payload-chat", requestId: "payload-request", context }), "synthetic inline response"); assert.equal(sentBody?.channel, "ordinary-chat"); assert.equal(sentBody?.ordinaryContext.messages.length, 121); assert.equal(sentBody?.ordinaryContext.messages[0].content, "earliest payload turn"); assert.equal(sentBody?.ordinaryContext.messages.at(-1).content, "latest payload turn"); assert.equal(sentBody?.ordinaryContext.messages[58].agentAside, true); assert.equal(sentBody?.ordinaryContext.messages[77].attachments[0].text, "full text data"); assert.equal(sentBody?.ordinaryContext.messages[77].attachments[1].id, "image-source"); assert.equal(sentBody?.ordinaryContext.messages[77].attachments[1].dataUrl, "data:image/png;base64,YWJj"); assert.equal(sentBody?.ordinaryContext.currentRequest, "@Sybil answer this"); }); test("streamed mention is idempotent and identifies the inline Agent", async () => { const chat = await prisma.chat.create({ data: { title: "Inline stream" } }); let started!: () => void; const startedPromise = new Promise((resolve) => { started = resolve; }); let finish!: (value: string) => void; const reply = new Promise((resolve) => { finish = resolve; }); let calls = 0; const streamingApp = Fastify({ logger: false }); await streamingApp.register(sensible); await registerRoutes(streamingApp, { agentInlineInvoker: async () => { calls += 1; started(); return reply; }, }); await streamingApp.ready(); try { const payload = { chatId: chat.id, clientRequestId: "inline-stream-1", provider: "openai", model: "synthetic-unused-model", messages: [{ role: "user", content: "@sybil check the current thread" }], }; const first = streamingApp.inject({ method: "POST", url: "/v1/chat-completions/stream", headers: auth, payload }); await startedPromise; const retry = streamingApp.inject({ method: "POST", url: "/v1/chat-completions/stream", headers: auth, payload }); finish("Synthetic streamed reply"); const [firstResponse, retryResponse] = await Promise.all([first, retry]); for (const response of [firstResponse, retryResponse]) { assert.equal(response.statusCode, 200, response.body); assert.match(response.body, /"agentAside":true/); assert.match(response.body, /Synthetic streamed reply/); } assert.equal(calls, 1); const messages = await prisma.message.findMany({ where: { chatId: chat.id } }); assert.equal(messages.length, 2); assert.equal((messages[1]!.metadata as Record).agentAside, true); } finally { await streamingApp.close(); } }); test("paged thread history returns all visible text and attachments but excludes internal rows and metadata", async () => { const timestamp = new Date("2026-09-01T12:00:00.000Z"); const chat = await prisma.chat.create({ data: { title: "Paginated history", messages: { create: Array.from({ length: 121 }, (_, index) => ({ id: `history-${String(index).padStart(4, "0")}`, createdAt: timestamp, role: index === 3 ? "tool" : index === 4 ? "system" : index % 2 ? "assistant" : "user", content: index === 3 || index === 4 ? "INTERNAL_NOT_VISIBLE" : `visible-${index}`, ...(index === 8 ? { metadata: { privateDiagnostic: "must not leak", attachments: [ { kind: "text", id: "text-1", filename: "notes.txt", mimeType: "text/plain", sizeBytes: 9, text: "full attachment text" }, { kind: "image", id: "image-1", filename: "photo.png", mimeType: "image/png", sizeBytes: 3, dataUrl: "data:image/png;base64,YWJj" }, ] } } : {}), ...(index === 10 ? { metadata: { clientRequestId: "private-id", agentAside: true, privateDiagnostic: "must not leak" } } : {}), })), }, }, }); const unauthenticated = await app.inject({ method: "GET", url: `/v1/chats/${chat.id}/messages?limit=17` }); assert.equal(unauthenticated.statusCode, 401); const wrongAuth = await app.inject({ method: "GET", url: `/v1/chats/${chat.id}/messages?limit=17`, headers: { authorization: "Bearer wrong-token" } }); assert.equal(wrongAuth.statusCode, 403); const invalidCursor = await app.inject({ method: "GET", url: `/v1/chats/${chat.id}/messages?cursor=not-a-cursor`, headers: auth }); assert.equal(invalidCursor.statusCode, 400); let cursor: string | null = null; const history: any[] = []; let pages = 0; do { const query = new URLSearchParams({ limit: "17", ...(cursor ? { cursor } : {}) }); const response = await app.inject({ method: "GET", url: `/v1/chats/${chat.id}/messages?${query}`, headers: auth }); assert.equal(response.statusCode, 200, response.body); const page = response.json(); history.push(...page.messages); cursor = page.nextCursor; pages += 1; } while (cursor); assert.ok(pages > 1); assert.equal(history.length, 119); assert.deepEqual(history.map((message) => message.id), history.map((message) => message.id).toSorted()); assert.equal(history[0].content, "visible-0"); assert.equal(history.at(-1).content, "visible-120"); assert.deepEqual(history.find((message) => message.id === "history-0008").attachments, [ { kind: "text", id: "text-1", filename: "notes.txt", mimeType: "text/plain", sizeBytes: 9, text: "full attachment text" }, { kind: "image", id: "image-1", filename: "photo.png", mimeType: "image/png", sizeBytes: 3, dataUrl: "data:image/png;base64,YWJj" }, ]); assert.equal(history.find((message) => message.id === "history-0010").agentAside, true); assert.ok(!JSON.stringify(history).includes("INTERNAL_NOT_VISIBLE")); assert.ok(!JSON.stringify(history).includes("privateDiagnostic")); assert.ok(!JSON.stringify(history).includes("private-id")); }); test("existing-chat list pagination returns older ordinary threads without exposing chat settings", async () => { const timestamp = new Date("2026-08-01T00:00:00.000Z"); await prisma.chat.createMany({ data: Array.from({ length: 105 }, (_, index) => ({ id: `listed-${String(index).padStart(4, "0")}`, title: `Listed ${index}`, createdAt: timestamp, updatedAt: timestamp, additionalSystemPrompt: "private chat configuration", enabledTools: ["private-tool-setting"], })), }); let cursor: string | null = null; const chats: any[] = []; do { const query = new URLSearchParams({ limit: "19", ...(cursor ? { cursor } : {}) }); const response = await app.inject({ method: "GET", url: `/v1/chats?${query}`, headers: auth }); assert.equal(response.statusCode, 200, response.body); const page = response.json(); chats.push(...page.chats); cursor = page.nextCursor; } while (cursor); const listed = chats.filter((chat) => chat.id.startsWith("listed-")); assert.equal(listed.length, 105); assert.equal(new Set(chats.map((chat) => chat.id)).size, chats.length); assert.ok(!JSON.stringify(chats).includes("private chat configuration")); assert.ok(!JSON.stringify(chats).includes("private-tool-setting")); });