logicsrc/plugins/agentmail/src/domain.test.ts
Anthony Ettinger e304eac761 Add AgentMail plugin: paid-member mailbox client for humans + agents
@logicsrc/plugin-agentmail provides read/search/compose/send/flag/delete
over an injected MailTransport, gated to Founding Lifetime (paid) members.
Returns plain JSON-serializable domain objects so the same API serves a
human TUI, the CLI, MCP, and bots.

- domain.ts: transport-agnostic types + pure helpers (parse/format address,
  normalizeDraft, isValidEmail, snippet)
- ports.ts: MailTransport seam
- service.ts: AgentMailService (inbox/list/read/search/send/reply/flag/delete)
- access.ts: paid-member gate (assertPaid) + capability constants
- transports/memory.ts: complete in-memory backend (tests/dev/reference)
- transports/mailu.ts: self-hosted Mailu seam (mail.profullstack.com IMAP +
  smtp.profullstack.com submission) with injected IMAP/SMTP drivers
- index.ts: PluginDefinition (manifest, routes, capabilities, tuiPanels)
- registered in the root build; 20 vitest tests pass

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 15:37:17 +00:00

63 lines
2.3 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { DraftError, formatAddress, isValidEmail, mailboxAddress, normalizeDraft, parseAddress, snippet } from "./domain.js";
describe("isValidEmail", () => {
it("accepts normal addresses", () => {
expect(isValidEmail("alice@mail.profullstack.com")).toBe(true);
});
it("rejects malformed addresses", () => {
for (const bad of ["", "alice", "alice@", "@host.com", "a b@host.com", "alice@host"]) {
expect(isValidEmail(bad)).toBe(false);
}
});
});
describe("mailboxAddress", () => {
it("joins local part and domain", () => {
expect(mailboxAddress("alice", "mail.profullstack.com")).toBe("alice@mail.profullstack.com");
});
});
describe("parseAddress / formatAddress", () => {
it("parses a named address", () => {
expect(parseAddress("Ada Lovelace <ada@mail.profullstack.com>")).toEqual({
name: "Ada Lovelace",
address: "ada@mail.profullstack.com"
});
});
it("parses a bare address", () => {
expect(parseAddress("ada@mail.profullstack.com")).toEqual({ address: "ada@mail.profullstack.com" });
});
it("round-trips and quotes names with specials", () => {
expect(formatAddress({ name: "Ada", address: "ada@x.com" })).toBe("Ada <ada@x.com>");
expect(formatAddress({ address: "ada@x.com" })).toBe("ada@x.com");
expect(formatAddress({ name: "Doe, John", address: "j@x.com" })).toBe('"Doe, John" <j@x.com>');
});
});
describe("snippet", () => {
it("collapses whitespace and truncates", () => {
expect(snippet("hello world\n\nthere")).toBe("hello world there");
expect(snippet("abcdef", 4)).toBe("abc…");
});
});
describe("normalizeDraft", () => {
it("trims subject, drops empty cc/bcc, keeps valid recipients", () => {
const d = normalizeDraft({
to: [{ address: " ada@x.com " }],
cc: [],
subject: " hi ",
text: "yo"
});
expect(d.to).toEqual([{ address: "ada@x.com" }]);
expect(d.subject).toBe("hi");
expect(d.cc).toBeUndefined();
});
it("rejects a draft with no recipients", () => {
expect(() => normalizeDraft({ to: [], subject: "x", text: "y" })).toThrow(DraftError);
});
it("rejects an invalid recipient", () => {
expect(() => normalizeDraft({ to: [{ address: "nope" }], subject: "x", text: "y" })).toThrow(/invalid recipient/);
});
});