import { randomUUID } from "node:crypto";
import { describe, expect, it, vi } from "vitest";
import { Webhook } from "svix";
import {
agentmailApi,
agentmailMessageSchema,
emailText,
emailReplyRecipients,
isAutomaticEmail,
isFilteredEmail,
normalizeAgentmailEvent,
verifyAgentmailWebhook,
} from "../services/agentmail-api.js";
import { emailSendSchema } from "@paperclipai/shared";
import { buildRunnerApiCatalog } from "../services/native-runtime/runner-api-catalog.js";
const message = (extra = {}) =>
agentmailMessageSchema.parse({
inbox_id: "agent@agentmail.to",
thread_id: "thread",
message_id: "message",
timestamp: new Date().toISOString(),
...extra,
});
describe("AgentMail protocol boundary", () => {
it("verifies the exact raw body and rejects forged or stale Svix signatures", () => {
const secret = `whsec_${Buffer.from("a-test-secret-only").toString("base64")}`;
const body = JSON.stringify({
event_type: "message.received",
message: message(),
});
const timestamp = new Date();
const id = randomUUID();
const headers = {
"svix-id": id,
"svix-timestamp": String(Math.floor(timestamp.getTime() / 1000)),
"svix-signature": new Webhook(secret).sign(id, timestamp, body),
};
expect(verifyAgentmailWebhook(Buffer.from(body), headers, secret)).toEqual(
JSON.parse(body),
);
expect(() =>
verifyAgentmailWebhook(Buffer.from(body + " "), headers, secret),
).toThrow();
expect(() =>
verifyAgentmailWebhook(
Buffer.from(body),
{ ...headers, "svix-timestamp": "1" },
secret,
),
).toThrow();
});
it("normalizes both transports and keeps delivery receipts separate from incoming mail", () => {
const m = { inbox_id: "inbox", message_id: "message" };
expect(
normalizeAgentmailEvent({ event_type: "message.received", message: m })
?.kind,
).toBe("message.received");
expect(
normalizeAgentmailEvent({ type: "message_received", message: m })?.kind,
).toBe("message.received");
expect(
normalizeAgentmailEvent({ type: "message_delivered", message: m })?.kind,
).toBe("message.delivered");
expect(normalizeAgentmailEvent({ type: "subscribed" })).toBeNull();
expect(() =>
normalizeAgentmailEvent({ event_type: "message.received", message: {} }),
).toThrow();
});
it.each([
["message.sent", "send"],
["message.delivered", "delivery"],
["message.bounced", "bounce"],
["message.complained", "complaint"],
["message.rejected", "reject"],
])("admits the documented %s receipt envelope through either transport", (kind, field) => {
for (const transport of [{ type: "event", event_type: kind }, { type: kind.replace(".", "_") }]) {
expect(normalizeAgentmailEvent({
...transport,
event_id: "provider-event",
[field]: { inbox_id: "inbox", thread_id: "thread", message_id: "sent-message" },
})).toEqual({ kind, inbox_id: "inbox", message_id: "sent-message", eventId: "provider-event" });
}
});
it("prefers extracted text, strips HTML and recognizes provider filtering and auto-replies", () => {
expect(
emailText(
message({ extracted_text: "New reply", text: "Quoted history" }),
),
).toBe("New reply");
expect(
emailText(
message({
html: '
Hello
', }), ), ).not.toContain("tracking.test"); expect( isAutomaticEmail( message({ headers: { "Auto-Submitted": "auto-replied" } }), ), ).toBe(true); expect( isAutomaticEmail(message({ headers: { "Auto-Submitted": "no" } })), ).toBe(false); for (const label of ["spam", "blocked", "unauthenticated"]) expect(isFilteredEmail(message({ labels: [label] }))).toBe(true); }); it("pins the API host, encodes message IDs and preserves the provider idempotency key", async () => { const fetcher = vi .fn() .mockResolvedValue( new Response( JSON.stringify({ message_id: "sent", thread_id: "thread" }), ), ); await agentmailApi("private-key", fetcher).send( "agent@agentmail.to", { text: "Reply", reply_all: false }, "stable-key", "