Files
Sybil-2/server/tests/agent-inline.test.ts
T
Sybil Agent 5d738f897b
TestFlight / Build and upload (push) Successful in 1m57s
Add inline persistent Agent mentions
2026-10-29 18:28:52 -07:00

324 lines
16 KiB
TypeScript

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<AgentInlineInvoker>[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<string, unknown>).agentAside, true);
assert.equal(messages[1]!.name, "Sybil Agent");
assert.equal((messages[1]!.metadata as Record<string, unknown>).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<string, any> | 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<void>((resolve) => { started = resolve; });
let finish!: (value: string) => void;
const reply = new Promise<string>((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<string, unknown>).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"));
});