openclaw/extensions/nylas/index.test.ts

2037 lines
66 KiB
TypeScript

import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
import { parseNylasConfig, validateNylasConfig, type NylasConfig } from "./src/config.js";
import { NylasClient, NylasApiError } from "./src/client.js";
import { nylasTools, discoverGrants } from "./src/tools/index.js";
import { registerNylasCli } from "./src/cli.js";
import {
listEmails,
getEmail,
sendEmail,
createDraft,
listThreads,
listFolders,
} from "./src/tools/email.js";
import {
listCalendars,
listEvents,
getEvent,
createEvent,
updateEvent,
deleteEvent,
checkAvailability,
} from "./src/tools/calendar.js";
import { listContacts, getContact } from "./src/tools/contacts.js";
import nylasPlugin from "./index.js";
// Mock the Nylas SDK with a class to work with both node and bun
vi.mock("nylas", () => {
const MockNylas = class {
grants = {
list: vi.fn(),
find: vi.fn(),
};
messages = {
list: vi.fn(),
find: vi.fn(),
send: vi.fn(),
};
threads = {
list: vi.fn(),
};
folders = {
list: vi.fn(),
};
drafts = {
create: vi.fn(),
};
calendars = {
list: vi.fn(),
getAvailability: vi.fn(),
};
events = {
list: vi.fn(),
find: vi.fn(),
create: vi.fn(),
update: vi.fn(),
destroy: vi.fn(),
};
contacts = {
list: vi.fn(),
find: vi.fn(),
};
};
return { default: MockNylas };
});
describe("Nylas Plugin", () => {
// ===========================================================================
// Config Tests
// ===========================================================================
describe("Config", () => {
describe("parseNylasConfig", () => {
it("parses minimal config with defaults", () => {
const config = parseNylasConfig({});
expect(config.enabled).toBe(true);
expect(config.apiUri).toBe("https://api.us.nylas.com");
expect(config.defaultTimezone).toBe("UTC");
expect(config.grants).toEqual({});
expect(config.apiKey).toBeUndefined();
expect(config.defaultGrantId).toBeUndefined();
});
it("parses full config", () => {
const config = parseNylasConfig({
enabled: false,
apiKey: "test-key",
apiUri: "https://api.eu.nylas.com",
defaultGrantId: "grant-123",
defaultTimezone: "America/New_York",
grants: { work: "work-grant", personal: "personal-grant" },
});
expect(config.enabled).toBe(false);
expect(config.apiKey).toBe("test-key");
expect(config.apiUri).toBe("https://api.eu.nylas.com");
expect(config.defaultGrantId).toBe("grant-123");
expect(config.defaultTimezone).toBe("America/New_York");
expect(config.grants).toEqual({ work: "work-grant", personal: "personal-grant" });
});
it("handles non-object input", () => {
const config = parseNylasConfig(null);
expect(config.enabled).toBe(true);
expect(config.apiUri).toBe("https://api.us.nylas.com");
});
it("handles array input", () => {
const config = parseNylasConfig([]);
expect(config.enabled).toBe(true);
});
it("handles string input", () => {
const config = parseNylasConfig("invalid");
expect(config.enabled).toBe(true);
});
it("preserves boolean enabled=false", () => {
const config = parseNylasConfig({ enabled: false });
expect(config.enabled).toBe(false);
});
it("uses EU API URI when specified", () => {
const config = parseNylasConfig({ apiUri: "https://api.eu.nylas.com" });
expect(config.apiUri).toBe("https://api.eu.nylas.com");
});
});
describe("validateNylasConfig", () => {
it("validates missing apiKey", () => {
const config = parseNylasConfig({ defaultGrantId: "grant-123" });
const validation = validateNylasConfig(config);
expect(validation.valid).toBe(false);
if (!validation.valid) {
expect(validation.errors).toContain("apiKey is required");
}
});
it("validates missing grant", () => {
const config = parseNylasConfig({ apiKey: "test-key" });
const validation = validateNylasConfig(config);
expect(validation.valid).toBe(false);
if (!validation.valid) {
expect(validation.errors).toContain("defaultGrantId or at least one named grant is required");
}
});
it("validates with defaultGrantId", () => {
const config = parseNylasConfig({ apiKey: "test-key", defaultGrantId: "grant-123" });
const validation = validateNylasConfig(config);
expect(validation.valid).toBe(true);
});
it("validates with named grants only", () => {
const config = parseNylasConfig({ apiKey: "test-key", grants: { work: "grant-123" } });
const validation = validateNylasConfig(config);
expect(validation.valid).toBe(true);
});
it("returns multiple errors", () => {
const config = parseNylasConfig({});
const validation = validateNylasConfig(config);
expect(validation.valid).toBe(false);
if (!validation.valid) {
expect(validation.errors.length).toBe(2);
expect(validation.errors).toContain("apiKey is required");
expect(validation.errors).toContain("defaultGrantId or at least one named grant is required");
}
});
});
});
// ===========================================================================
// Client Tests
// ===========================================================================
describe("Client", () => {
const testConfig: NylasConfig = {
enabled: true,
apiKey: "test-api-key",
apiUri: "https://api.us.nylas.com",
defaultGrantId: "default-grant",
defaultTimezone: "UTC",
grants: { work: "work-grant", personal: "personal-grant" },
};
it("creates client with config", () => {
const client = new NylasClient({ config: testConfig });
expect(client).toBeInstanceOf(NylasClient);
expect(client.sdk).toBeDefined();
});
describe("resolveGrantId", () => {
it("returns default grant when no argument", () => {
const client = new NylasClient({ config: testConfig });
expect(client.resolveGrantId()).toBe("default-grant");
});
it("resolves named grant", () => {
const client = new NylasClient({ config: testConfig });
expect(client.resolveGrantId("work")).toBe("work-grant");
expect(client.resolveGrantId("personal")).toBe("personal-grant");
});
it("returns raw grant ID if not a named grant", () => {
const client = new NylasClient({ config: testConfig });
expect(client.resolveGrantId("some-random-id")).toBe("some-random-id");
});
it("throws when no default grant and no argument", () => {
const config = { ...testConfig, defaultGrantId: undefined };
const client = new NylasClient({ config });
expect(() => client.resolveGrantId()).toThrow("No grant ID provided");
});
});
describe("listGrants", () => {
it("calls SDK grants.list", async () => {
const client = new NylasClient({ config: testConfig });
const mockData = {
data: [
{ id: "grant-1", email: "test@example.com", provider: "google", grantStatus: "valid" },
],
};
(client.sdk.grants.list as any).mockResolvedValue(mockData);
const result = await client.listGrants();
expect(client.sdk.grants.list).toHaveBeenCalled();
expect(result.data).toHaveLength(1);
expect(result.data[0].email).toBe("test@example.com");
});
it("passes query params", async () => {
const client = new NylasClient({ config: testConfig });
(client.sdk.grants.list as any).mockResolvedValue({ data: [] });
await client.listGrants({ limit: 10, provider: "google" });
expect(client.sdk.grants.list).toHaveBeenCalledWith({
queryParams: { limit: 10, provider: "google", offset: undefined, email: undefined },
});
});
});
describe("listMessages", () => {
it("calls SDK messages.list with resolved grant", async () => {
const client = new NylasClient({ config: testConfig });
const mockData = { data: [], nextCursor: null };
(client.sdk.messages.list as any).mockResolvedValue(mockData);
await client.listMessages({ grant: "work", limit: 10 });
expect(client.sdk.messages.list).toHaveBeenCalledWith({
identifier: "work-grant",
queryParams: expect.objectContaining({ limit: 10 }),
});
});
});
describe("error handling", () => {
it("wraps SDK errors", async () => {
const client = new NylasClient({ config: testConfig });
const sdkError = { message: "Unauthorized", statusCode: 401, requestId: "req-123" };
(client.sdk.grants.list as any).mockRejectedValue(sdkError);
await expect(client.listGrants()).rejects.toThrow(NylasApiError);
try {
await client.listGrants();
} catch (err) {
expect(err).toBeInstanceOf(NylasApiError);
expect((err as NylasApiError).statusCode).toBe(401);
expect((err as NylasApiError).requestId).toBe("req-123");
}
});
it("handles generic errors", async () => {
const client = new NylasClient({ config: testConfig });
(client.sdk.grants.list as any).mockRejectedValue(new Error("Network error"));
await expect(client.listGrants()).rejects.toThrow(NylasApiError);
});
});
});
// ===========================================================================
// NylasApiError Tests
// ===========================================================================
describe("NylasApiError", () => {
it("creates error with all properties", () => {
const error = new NylasApiError("Test error", 401, "req-123", "unauthorized");
expect(error.message).toBe("Test error");
expect(error.statusCode).toBe(401);
expect(error.requestId).toBe("req-123");
expect(error.errorType).toBe("unauthorized");
expect(error.name).toBe("NylasApiError");
});
it("creates error with minimal properties", () => {
const error = new NylasApiError("Test error");
expect(error.message).toBe("Test error");
expect(error.statusCode).toBeUndefined();
expect(error.requestId).toBeUndefined();
});
it("is instanceof Error", () => {
const error = new NylasApiError("Test");
expect(error).toBeInstanceOf(Error);
});
});
// ===========================================================================
// Tools Tests
// ===========================================================================
describe("Tools", () => {
it("has correct number of tools", () => {
expect(nylasTools.length).toBe(16);
});
it("all tools have required properties", () => {
for (const tool of nylasTools) {
expect(tool.name).toBeDefined();
expect(tool.name.startsWith("nylas_")).toBe(true);
expect(tool.label).toBeDefined();
expect(tool.description).toBeDefined();
expect(tool.parameters).toBeDefined();
expect(typeof tool.execute).toBe("function");
}
});
it("all tool names are unique", () => {
const names = nylasTools.map((t) => t.name);
const uniqueNames = new Set(names);
expect(uniqueNames.size).toBe(names.length);
});
describe("discover grants tool", () => {
it("exists and has correct metadata", () => {
const tool = nylasTools.find((t) => t.name === "nylas_discover_grants");
expect(tool).toBeDefined();
expect(tool?.description).toContain("authenticated");
expect(tool?.label).toBe("Discover Grants");
});
});
describe("email tools", () => {
const emailTools = ["nylas_list_emails", "nylas_get_email", "nylas_send_email", "nylas_create_draft", "nylas_list_threads", "nylas_list_folders"];
it("includes all email tools", () => {
for (const name of emailTools) {
const tool = nylasTools.find((t) => t.name === name);
expect(tool).toBeDefined();
}
});
it("list_emails has correct parameters", () => {
const tool = nylasTools.find((t) => t.name === "nylas_list_emails");
expect(tool?.parameters).toBeDefined();
const props = (tool?.parameters as any).properties;
expect(props.grant).toBeDefined();
expect(props.folder).toBeDefined();
expect(props.subject).toBeDefined();
expect(props.from).toBeDefined();
expect(props.unread).toBeDefined();
expect(props.limit).toBeDefined();
});
it("send_email has required parameters", () => {
const tool = nylasTools.find((t) => t.name === "nylas_send_email");
const required = (tool?.parameters as any).required ?? [];
expect(required).toContain("to");
expect(required).toContain("subject");
expect(required).toContain("body");
});
});
describe("calendar tools", () => {
const calendarTools = ["nylas_list_calendars", "nylas_list_events", "nylas_get_event", "nylas_create_event", "nylas_update_event", "nylas_delete_event", "nylas_check_availability"];
it("includes all calendar tools", () => {
for (const name of calendarTools) {
const tool = nylasTools.find((t) => t.name === name);
expect(tool).toBeDefined();
}
});
it("create_event has required parameters", () => {
const tool = nylasTools.find((t) => t.name === "nylas_create_event");
const required = (tool?.parameters as any).required ?? [];
expect(required).toContain("calendar_id");
expect(required).toContain("title");
expect(required).toContain("start");
});
it("check_availability has correct parameters", () => {
const tool = nylasTools.find((t) => t.name === "nylas_check_availability");
const props = (tool?.parameters as any).properties;
expect(props.emails).toBeDefined();
expect(props.start).toBeDefined();
expect(props.end).toBeDefined();
expect(props.duration_minutes).toBeDefined();
});
});
describe("contact tools", () => {
it("includes list_contacts tool", () => {
const tool = nylasTools.find((t) => t.name === "nylas_list_contacts");
expect(tool).toBeDefined();
expect(tool?.description).toContain("contact");
});
it("includes get_contact tool", () => {
const tool = nylasTools.find((t) => t.name === "nylas_get_contact");
expect(tool).toBeDefined();
const required = (tool?.parameters as any).required ?? [];
expect(required).toContain("contact_id");
});
});
});
// ===========================================================================
// Plugin Definition Tests
// ===========================================================================
describe("Plugin Definition", () => {
it("has correct metadata", () => {
expect(nylasPlugin.id).toBe("nylas");
expect(nylasPlugin.name).toBe("Nylas");
expect(nylasPlugin.description.toLowerCase()).toContain("email");
expect(nylasPlugin.description.toLowerCase()).toContain("calendar");
});
it("has config schema with parse function", () => {
expect(nylasPlugin.configSchema).toBeDefined();
expect(typeof nylasPlugin.configSchema.parse).toBe("function");
});
it("has config schema with uiHints", () => {
expect(nylasPlugin.configSchema.uiHints).toBeDefined();
expect(nylasPlugin.configSchema.uiHints.apiKey).toBeDefined();
expect(nylasPlugin.configSchema.uiHints.apiKey.sensitive).toBe(true);
});
it("has register function", () => {
expect(typeof nylasPlugin.register).toBe("function");
});
describe("register", () => {
it("registers all tools", () => {
const registeredTools: string[] = [];
const mockApi = {
id: "nylas",
name: "Nylas",
source: "test",
config: {},
pluginConfig: { apiKey: "test", defaultGrantId: "grant" },
runtime: {},
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
registerTool: (tool: any) => registeredTools.push(tool.name),
registerCli: vi.fn(),
registerGatewayMethod: vi.fn(),
};
nylasPlugin.register(mockApi as any);
expect(registeredTools).toHaveLength(16);
expect(registeredTools).toContain("nylas_discover_grants");
expect(registeredTools).toContain("nylas_list_emails");
expect(registeredTools).toContain("nylas_send_email");
});
it("registers CLI commands", () => {
const mockRegisterCli = vi.fn();
const mockApi = {
id: "nylas",
name: "Nylas",
source: "test",
config: {},
pluginConfig: { apiKey: "test", defaultGrantId: "grant" },
runtime: {},
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
registerTool: vi.fn(),
registerCli: mockRegisterCli,
registerGatewayMethod: vi.fn(),
};
nylasPlugin.register(mockApi as any);
expect(mockRegisterCli).toHaveBeenCalledWith(
expect.any(Function),
{ commands: ["nylas"] },
);
});
it("registers gateway methods", () => {
const registeredMethods: string[] = [];
const mockApi = {
id: "nylas",
name: "Nylas",
source: "test",
config: {},
pluginConfig: { apiKey: "test", defaultGrantId: "grant" },
runtime: {},
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
registerTool: vi.fn(),
registerCli: vi.fn(),
registerGatewayMethod: (method: string) => registeredMethods.push(method),
};
nylasPlugin.register(mockApi as any);
expect(registeredMethods).toContain("nylas.discoverGrants");
expect(registeredMethods).toContain("nylas.listEmails");
expect(registeredMethods).toContain("nylas.getMessage");
expect(registeredMethods).toContain("nylas.sendMessage");
expect(registeredMethods).toContain("nylas.listCalendars");
expect(registeredMethods).toContain("nylas.listEvents");
expect(registeredMethods).toContain("nylas.createEvent");
expect(registeredMethods).toContain("nylas.listContacts");
});
});
});
});
// ===========================================================================
// Email Tools Implementation Tests
// ===========================================================================
describe("Email Tools", () => {
const createMockClient = () => {
return {
listMessages: vi.fn(),
getMessage: vi.fn(),
sendMessage: vi.fn(),
createDraft: vi.fn(),
listThreads: vi.fn(),
listFolders: vi.fn(),
} as any;
};
describe("listEmails", () => {
it("returns formatted email list", async () => {
const client = createMockClient();
client.listMessages.mockResolvedValue({
data: [
{
id: "msg-1",
threadId: "thread-1",
subject: "Test Subject",
from: [{ email: "sender@example.com", name: "Sender" }],
to: [{ email: "recipient@example.com" }],
date: 1700000000,
unread: true,
starred: false,
snippet: "Preview text...",
attachments: [],
folders: ["INBOX"],
},
],
nextCursor: "next-token",
});
const result = await listEmails(client, { limit: 10 });
expect(result.emails).toHaveLength(1);
expect(result.emails[0].id).toBe("msg-1");
expect(result.emails[0].subject).toBe("Test Subject");
expect(result.emails[0].from).toBe("Sender <sender@example.com>");
expect(result.emails[0].unread).toBe(true);
expect(result.has_more).toBe(true);
expect(result.next_page_token).toBe("next-token");
});
it("limits results to max 50", async () => {
const client = createMockClient();
client.listMessages.mockResolvedValue({ data: [], nextCursor: null });
await listEmails(client, { limit: 100 });
expect(client.listMessages).toHaveBeenCalledWith(
expect.objectContaining({ limit: 50 }),
);
});
it("converts date filters to timestamps", async () => {
const client = createMockClient();
client.listMessages.mockResolvedValue({ data: [], nextCursor: null });
await listEmails(client, {
received_after: "2024-01-01T00:00:00Z",
received_before: "2024-12-31T23:59:59Z",
});
expect(client.listMessages).toHaveBeenCalledWith(
expect.objectContaining({
receivedAfter: expect.any(Number),
receivedBefore: expect.any(Number),
}),
);
});
});
describe("getEmail", () => {
it("returns formatted email with full body", async () => {
const client = createMockClient();
client.getMessage.mockResolvedValue({
data: {
id: "msg-1",
threadId: "thread-1",
subject: "Test",
from: [{ email: "sender@example.com" }],
to: [{ email: "recipient@example.com" }],
cc: [{ email: "cc@example.com" }],
bcc: [],
replyTo: [],
date: 1700000000,
body: "<p>Email body</p>",
unread: false,
starred: true,
folders: ["INBOX"],
attachments: [
{ id: "att-1", filename: "file.pdf", contentType: "application/pdf", size: 1024 },
],
},
});
const result = await getEmail(client, { message_id: "msg-1" });
expect(result.id).toBe("msg-1");
expect(result.body).toBe("<p>Email body</p>");
expect(result.starred).toBe(true);
expect(result.attachments).toHaveLength(1);
expect(result.attachments![0].filename).toBe("file.pdf");
});
});
describe("sendEmail", () => {
it("sends email and returns message id", async () => {
const client = createMockClient();
client.sendMessage.mockResolvedValue({
data: { id: "sent-msg-1", threadId: "thread-1" },
});
const result = await sendEmail(client, {
to: "recipient@example.com",
subject: "Test",
body: "Hello",
});
expect(result.success).toBe(true);
expect(result.message_id).toBe("sent-msg-1");
expect(client.sendMessage).toHaveBeenCalledWith(
expect.objectContaining({
to: [{ email: "recipient@example.com" }],
subject: "Test",
body: "Hello",
}),
);
});
it("parses multiple recipients", async () => {
const client = createMockClient();
client.sendMessage.mockResolvedValue({
data: { id: "msg-1", threadId: "thread-1" },
});
await sendEmail(client, {
to: "a@example.com, John <b@example.com>",
cc: "c@example.com",
subject: "Test",
body: "Hello",
});
expect(client.sendMessage).toHaveBeenCalledWith(
expect.objectContaining({
to: [
{ email: "a@example.com" },
{ email: "b@example.com", name: "John" },
],
cc: [{ email: "c@example.com" }],
}),
);
});
});
describe("createDraft", () => {
it("creates draft and returns draft id", async () => {
const client = createMockClient();
client.createDraft.mockResolvedValue({
data: { id: "draft-1", threadId: "thread-1" },
});
const result = await createDraft(client, {
to: "recipient@example.com",
subject: "Draft subject",
body: "Draft body",
});
expect(result.success).toBe(true);
expect(result.draft_id).toBe("draft-1");
});
});
describe("listThreads", () => {
it("returns formatted thread list", async () => {
const client = createMockClient();
client.listThreads.mockResolvedValue({
data: [
{
id: "thread-1",
subject: "Thread Subject",
participants: [
{ email: "a@example.com", name: "Alice" },
{ email: "b@example.com" },
],
messageIds: ["msg-1", "msg-2", "msg-3"],
snippet: "Latest message...",
unread: true,
starred: false,
latestMessageReceivedDate: 1700000000,
folders: ["INBOX"],
},
],
nextCursor: null,
});
const result = await listThreads(client, {});
expect(result.threads).toHaveLength(1);
expect(result.threads[0].subject).toBe("Thread Subject");
expect(result.threads[0].message_count).toBe(3);
expect(result.threads[0].participants).toContain("Alice <a@example.com>");
expect(result.has_more).toBe(false);
});
});
describe("listFolders", () => {
it("returns formatted folder list", async () => {
const client = createMockClient();
client.listFolders.mockResolvedValue({
data: [
{ id: "folder-1", name: "INBOX", systemFolder: true, unreadCount: 5, totalCount: 100 },
{ id: "folder-2", name: "Custom", systemFolder: false, totalCount: 10 },
],
});
const result = await listFolders(client, {});
expect(result.folders).toHaveLength(2);
expect(result.folders[0].name).toBe("INBOX");
expect(result.folders[0].system_folder).toBe(true);
expect(result.folders[0].unread_count).toBe(5);
});
});
});
// ===========================================================================
// Calendar Tools Implementation Tests
// ===========================================================================
describe("Calendar Tools", () => {
const createMockClient = () => {
return {
listCalendars: vi.fn(),
listEvents: vi.fn(),
getEvent: vi.fn(),
createEvent: vi.fn(),
updateEvent: vi.fn(),
deleteEvent: vi.fn(),
checkAvailability: vi.fn(),
} as any;
};
describe("listCalendars", () => {
it("returns formatted calendar list", async () => {
const client = createMockClient();
client.listCalendars.mockResolvedValue({
data: [
{ id: "cal-1", name: "Primary", isPrimary: true, timezone: "America/New_York", readOnly: false },
{ id: "cal-2", name: "Work", isPrimary: false, description: "Work calendar" },
],
});
const result = await listCalendars(client, {});
expect(result.calendars).toHaveLength(2);
expect(result.calendars[0].name).toBe("Primary");
expect(result.calendars[0].is_primary).toBe(true);
});
});
describe("listEvents", () => {
it("returns formatted event list with timespan", async () => {
const client = createMockClient();
client.listEvents.mockResolvedValue({
data: [
{
id: "event-1",
calendarId: "cal-1",
title: "Meeting",
description: "Team sync",
location: "Room A",
when: { startTime: 1700000000, endTime: 1700003600 },
participants: [{ email: "attendee@example.com", status: "yes" }],
status: "confirmed",
busy: true,
conferencing: { details: { url: "https://meet.example.com/123" } },
},
],
nextCursor: null,
});
const result = await listEvents(client, {});
expect(result.events).toHaveLength(1);
expect(result.events[0].title).toBe("Meeting");
expect(result.events[0].start).toBeDefined();
expect(result.events[0].end).toBeDefined();
expect(result.events[0].conferencing_url).toBe("https://meet.example.com/123");
});
it("handles all-day events", async () => {
const client = createMockClient();
client.listEvents.mockResolvedValue({
data: [
{
id: "event-1",
calendarId: "cal-1",
title: "Holiday",
when: { date: "2024-12-25" },
},
],
nextCursor: null,
});
const result = await listEvents(client, {});
expect(result.events[0].all_day).toBe(true);
expect(result.events[0].start).toBe("2024-12-25");
});
});
describe("createEvent", () => {
it("creates timespan event", async () => {
const client = createMockClient();
client.createEvent.mockResolvedValue({
data: {
id: "new-event",
calendarId: "cal-1",
title: "New Meeting",
when: { startTime: 1700000000, endTime: 1700003600 },
},
});
const result = await createEvent(client, {
calendar_id: "cal-1",
title: "New Meeting",
start: "2024-03-20T10:00:00Z",
end: "2024-03-20T11:00:00Z",
});
expect(result.success).toBe(true);
expect(result.event_id).toBe("new-event");
expect(client.createEvent).toHaveBeenCalledWith(
expect.objectContaining({
calendarId: "cal-1",
title: "New Meeting",
when: expect.objectContaining({
startTime: expect.any(Number),
endTime: expect.any(Number),
}),
}),
);
});
it("creates all-day event", async () => {
const client = createMockClient();
client.createEvent.mockResolvedValue({
data: {
id: "new-event",
calendarId: "cal-1",
title: "Holiday",
when: { date: "2024-12-25" },
},
});
const result = await createEvent(client, {
calendar_id: "cal-1",
title: "Holiday",
start: "2024-12-25",
all_day: true,
});
expect(result.success).toBe(true);
expect(client.createEvent).toHaveBeenCalledWith(
expect.objectContaining({
when: { date: "2024-12-25" },
}),
);
});
it("parses participants", async () => {
const client = createMockClient();
client.createEvent.mockResolvedValue({
data: { id: "event-1", calendarId: "cal-1", when: {} },
});
await createEvent(client, {
calendar_id: "cal-1",
title: "Meeting",
start: "2024-03-20T10:00:00Z",
participants: "alice@example.com, bob@example.com",
});
expect(client.createEvent).toHaveBeenCalledWith(
expect.objectContaining({
participants: [
{ email: "alice@example.com" },
{ email: "bob@example.com" },
],
}),
);
});
});
describe("updateEvent", () => {
it("updates event fields", async () => {
const client = createMockClient();
client.updateEvent.mockResolvedValue({
data: { id: "event-1", title: "Updated Title", when: {} },
});
const result = await updateEvent(client, {
event_id: "event-1",
calendar_id: "cal-1",
title: "Updated Title",
location: "New Room",
});
expect(result.success).toBe(true);
expect(client.updateEvent).toHaveBeenCalledWith(
"event-1",
"cal-1",
expect.objectContaining({
title: "Updated Title",
location: "New Room",
}),
);
});
});
describe("deleteEvent", () => {
it("deletes event", async () => {
const client = createMockClient();
client.deleteEvent.mockResolvedValue(undefined);
const result = await deleteEvent(client, {
event_id: "event-1",
calendar_id: "cal-1",
});
expect(result.success).toBe(true);
expect(result.event_id).toBe("event-1");
expect(client.deleteEvent).toHaveBeenCalledWith("event-1", "cal-1", undefined);
});
});
describe("checkAvailability", () => {
it("returns available time slots", async () => {
const client = createMockClient();
client.checkAvailability.mockResolvedValue({
data: {
timeSlots: [
{ startTime: 1700000000, endTime: 1700001800, emails: ["alice@example.com"] },
{ startTime: 1700003600, endTime: 1700005400, emails: ["alice@example.com", "bob@example.com"] },
],
order: ["alice@example.com", "bob@example.com"],
},
});
const result = await checkAvailability(client, {
emails: "alice@example.com, bob@example.com",
start: "2024-03-20T09:00:00Z",
end: "2024-03-20T17:00:00Z",
duration_minutes: 30,
});
expect(result.available_slots).toHaveLength(2);
expect(result.slot_count).toBe(2);
expect(result.checked_emails).toContain("alice@example.com");
});
});
});
// ===========================================================================
// Contact Tools Implementation Tests
// ===========================================================================
describe("Contact Tools", () => {
const createMockClient = () => {
return {
listContacts: vi.fn(),
getContact: vi.fn(),
} as any;
};
describe("listContacts", () => {
it("returns formatted contact list", async () => {
const client = createMockClient();
client.listContacts.mockResolvedValue({
data: [
{
id: "contact-1",
givenName: "John",
surname: "Doe",
companyName: "Acme Inc",
jobTitle: "Engineer",
emails: [{ email: "john@example.com", type: "work" }],
phoneNumbers: [{ number: "+1234567890", type: "mobile" }],
source: "address_book",
},
],
nextCursor: "next-token",
});
const result = await listContacts(client, {});
expect(result.contacts).toHaveLength(1);
expect(result.contacts[0].name).toBe("John Doe");
expect(result.contacts[0].company).toBe("Acme Inc");
expect(result.contacts[0].emails).toHaveLength(1);
expect(result.has_more).toBe(true);
});
it("handles contact with nickname only", async () => {
const client = createMockClient();
client.listContacts.mockResolvedValue({
data: [
{ id: "contact-1", nickname: "Johnny" },
],
nextCursor: null,
});
const result = await listContacts(client, {});
expect(result.contacts[0].name).toBe("Johnny");
});
it("handles contact with no name", async () => {
const client = createMockClient();
client.listContacts.mockResolvedValue({
data: [
{ id: "contact-1", emails: [{ email: "unknown@example.com" }] },
],
nextCursor: null,
});
const result = await listContacts(client, {});
expect(result.contacts[0].name).toBe("(no name)");
});
});
describe("getContact", () => {
it("returns full contact details", async () => {
const client = createMockClient();
client.getContact.mockResolvedValue({
data: {
id: "contact-1",
givenName: "Jane",
middleName: "Marie",
surname: "Smith",
nickname: "Janie",
companyName: "Tech Corp",
jobTitle: "Manager",
emails: [
{ email: "jane.work@example.com", type: "work" },
{ email: "jane.personal@example.com", type: "home" },
],
phoneNumbers: [{ number: "+1234567890", type: "work" }],
physicalAddresses: [
{
streetAddress: "123 Main St",
city: "New York",
state: "NY",
postalCode: "10001",
country: "USA",
type: "work",
},
],
notes: "Important contact",
birthday: "1990-01-15",
pictureUrl: "https://example.com/photo.jpg",
source: "address_book",
groups: [{ id: "group-1" }],
},
});
const result = await getContact(client, { contact_id: "contact-1" });
expect(result.name).toBe("Jane Marie Smith");
expect(result.given_name).toBe("Jane");
expect(result.middle_name).toBe("Marie");
expect(result.surname).toBe("Smith");
expect(result.emails).toHaveLength(2);
expect(result.addresses).toHaveLength(1);
expect(result.addresses![0].city).toBe("New York");
expect(result.notes).toBe("Important contact");
expect(result.birthday).toBe("1990-01-15");
});
});
});
// ===========================================================================
// Discover Grants Tool Tests
// ===========================================================================
describe("Discover Grants Tool", () => {
it("returns formatted grants list", async () => {
const mockClient = {
listGrants: vi.fn().mockResolvedValue({
data: [
{ id: "grant-1", email: "user@gmail.com", provider: "google", grantStatus: "valid", scope: ["email", "calendar"] },
{ id: "grant-2", email: "user@outlook.com", provider: "microsoft", grantStatus: "valid" },
],
}),
};
const result = await discoverGrants(mockClient as any, {});
expect(result.grants).toHaveLength(2);
expect(result.grants[0].email).toBe("user@gmail.com");
expect(result.grants[0].provider).toBe("google");
expect(result.grants[0].status).toBe("valid");
expect(result.count).toBe(2);
expect(result.hint).toContain("grant");
});
it("returns hint when no grants found", async () => {
const mockClient = {
listGrants: vi.fn().mockResolvedValue({ data: [] }),
};
const result = await discoverGrants(mockClient as any, {});
expect(result.grants).toHaveLength(0);
expect(result.count).toBe(0);
expect(result.hint).toContain("dashboard.nylas.com");
});
it("filters by provider", async () => {
const mockClient = {
listGrants: vi.fn().mockResolvedValue({ data: [] }),
};
await discoverGrants(mockClient as any, { provider: "google" });
expect(mockClient.listGrants).toHaveBeenCalledWith({ provider: "google", email: undefined });
});
});
// ===========================================================================
// Additional Client Method Tests
// ===========================================================================
describe("Client Methods", () => {
const createMockSdk = () => ({
grants: { list: vi.fn(), find: vi.fn() },
messages: { list: vi.fn(), find: vi.fn(), send: vi.fn() },
threads: { list: vi.fn() },
folders: { list: vi.fn() },
drafts: { create: vi.fn() },
calendars: { list: vi.fn(), getAvailability: vi.fn() },
events: { list: vi.fn(), find: vi.fn(), create: vi.fn(), update: vi.fn(), destroy: vi.fn() },
contacts: { list: vi.fn(), find: vi.fn() },
});
describe("getMessage", () => {
it("fetches a specific message by ID", async () => {
const mockSdk = createMockSdk();
mockSdk.messages.find.mockResolvedValue({
data: {
id: "msg-123",
subject: "Test Email",
body: "<p>Hello</p>",
},
});
const client = new NylasClient({
config: parseNylasConfig({ apiKey: "test-key", defaultGrantId: "grant-1" }),
});
(client as any).sdk = mockSdk;
const result = await client.getMessage("msg-123");
expect(mockSdk.messages.find).toHaveBeenCalledWith({
identifier: "grant-1",
messageId: "msg-123",
});
expect(result.data.id).toBe("msg-123");
});
});
describe("sendMessage", () => {
it("sends an email with all fields", async () => {
const mockSdk = createMockSdk();
mockSdk.messages.send.mockResolvedValue({
data: { id: "sent-msg-123" },
});
const client = new NylasClient({
config: parseNylasConfig({ apiKey: "test-key", defaultGrantId: "grant-1" }),
});
(client as any).sdk = mockSdk;
const result = await client.sendMessage({
to: [{ email: "to@example.com", name: "To" }],
cc: [{ email: "cc@example.com" }],
bcc: [{ email: "bcc@example.com" }],
subject: "Test Subject",
body: "<p>Test body</p>",
replyToMessageId: "reply-to-123",
});
expect(mockSdk.messages.send).toHaveBeenCalledWith({
identifier: "grant-1",
requestBody: {
to: [{ email: "to@example.com", name: "To" }],
cc: [{ email: "cc@example.com" }],
bcc: [{ email: "bcc@example.com" }],
replyTo: undefined,
subject: "Test Subject",
body: "<p>Test body</p>",
replyToMessageId: "reply-to-123",
},
});
expect(result.data.id).toBe("sent-msg-123");
});
});
describe("createDraft", () => {
it("creates a draft email", async () => {
const mockSdk = createMockSdk();
mockSdk.drafts.create.mockResolvedValue({
data: { id: "draft-123" },
});
const client = new NylasClient({
config: parseNylasConfig({ apiKey: "test-key", defaultGrantId: "grant-1" }),
});
(client as any).sdk = mockSdk;
const result = await client.createDraft({
to: [{ email: "to@example.com" }],
subject: "Draft Subject",
body: "<p>Draft body</p>",
});
expect(mockSdk.drafts.create).toHaveBeenCalledWith({
identifier: "grant-1",
requestBody: {
to: [{ email: "to@example.com" }],
cc: undefined,
bcc: undefined,
subject: "Draft Subject",
body: "<p>Draft body</p>",
replyToMessageId: undefined,
},
});
expect(result.data.id).toBe("draft-123");
});
});
describe("listCalendars", () => {
it("lists calendars for a grant", async () => {
const mockSdk = createMockSdk();
mockSdk.calendars.list.mockResolvedValue({
data: [
{ id: "cal-1", name: "Primary", isPrimary: true },
{ id: "cal-2", name: "Work" },
],
});
const client = new NylasClient({
config: parseNylasConfig({ apiKey: "test-key", defaultGrantId: "grant-1" }),
});
(client as any).sdk = mockSdk;
const result = await client.listCalendars();
expect(mockSdk.calendars.list).toHaveBeenCalledWith({ identifier: "grant-1" });
expect(result.data).toHaveLength(2);
});
});
describe("getEvent", () => {
it("fetches a specific event by ID", async () => {
const mockSdk = createMockSdk();
mockSdk.events.find.mockResolvedValue({
data: {
id: "event-123",
title: "Meeting",
when: { startTime: 1700000000, endTime: 1700003600 },
},
});
const client = new NylasClient({
config: parseNylasConfig({ apiKey: "test-key", defaultGrantId: "grant-1" }),
});
(client as any).sdk = mockSdk;
const result = await client.getEvent("event-123", "cal-1");
expect(mockSdk.events.find).toHaveBeenCalledWith({
identifier: "grant-1",
eventId: "event-123",
queryParams: { calendarId: "cal-1" },
});
expect(result.data.title).toBe("Meeting");
});
});
describe("createEvent", () => {
it("creates a new event", async () => {
const mockSdk = createMockSdk();
mockSdk.events.create.mockResolvedValue({
data: { id: "new-event-123" },
});
const client = new NylasClient({
config: parseNylasConfig({ apiKey: "test-key", defaultGrantId: "grant-1" }),
});
(client as any).sdk = mockSdk;
const result = await client.createEvent({
calendarId: "cal-1",
title: "New Meeting",
description: "A test meeting",
location: "Room A",
when: { startTime: 1700000000, endTime: 1700003600 },
participants: [{ email: "attendee@example.com" }],
busy: true,
});
expect(mockSdk.events.create).toHaveBeenCalledWith({
identifier: "grant-1",
queryParams: { calendarId: "cal-1" },
requestBody: {
title: "New Meeting",
description: "A test meeting",
location: "Room A",
when: { startTime: 1700000000, endTime: 1700003600 },
participants: [{ email: "attendee@example.com" }],
busy: true,
},
});
expect(result.data.id).toBe("new-event-123");
});
});
describe("updateEvent", () => {
it("updates an existing event", async () => {
const mockSdk = createMockSdk();
mockSdk.events.update.mockResolvedValue({
data: { id: "event-123", title: "Updated Meeting" },
});
const client = new NylasClient({
config: parseNylasConfig({ apiKey: "test-key", defaultGrantId: "grant-1" }),
});
(client as any).sdk = mockSdk;
const result = await client.updateEvent("event-123", "cal-1", {
title: "Updated Meeting",
location: "Room B",
});
expect(mockSdk.events.update).toHaveBeenCalledWith({
identifier: "grant-1",
eventId: "event-123",
queryParams: { calendarId: "cal-1" },
requestBody: {
title: "Updated Meeting",
description: undefined,
location: "Room B",
when: undefined,
participants: undefined,
busy: undefined,
},
});
expect(result.data.title).toBe("Updated Meeting");
});
});
describe("deleteEvent", () => {
it("deletes an event", async () => {
const mockSdk = createMockSdk();
mockSdk.events.destroy.mockResolvedValue(undefined);
const client = new NylasClient({
config: parseNylasConfig({ apiKey: "test-key", defaultGrantId: "grant-1" }),
});
(client as any).sdk = mockSdk;
await client.deleteEvent("event-123", "cal-1");
expect(mockSdk.events.destroy).toHaveBeenCalledWith({
identifier: "grant-1",
eventId: "event-123",
queryParams: { calendarId: "cal-1" },
});
});
});
describe("checkAvailability", () => {
it("checks availability for participants", async () => {
const mockSdk = createMockSdk();
mockSdk.calendars.getAvailability.mockResolvedValue({
data: {
timeSlots: [
{ startTime: 1700000000, endTime: 1700001800 },
],
},
});
const client = new NylasClient({
config: parseNylasConfig({ apiKey: "test-key", defaultGrantId: "grant-1" }),
});
(client as any).sdk = mockSdk;
const result = await client.checkAvailability({
startTime: 1700000000,
endTime: 1700010000,
participants: [{ email: "person@example.com" }],
durationMinutes: 30,
intervalMinutes: 15,
});
expect(mockSdk.calendars.getAvailability).toHaveBeenCalledWith({
requestBody: {
startTime: 1700000000,
endTime: 1700010000,
participants: [{ email: "person@example.com" }],
durationMinutes: 30,
intervalMinutes: 15,
},
});
expect(result.data.timeSlots).toHaveLength(1);
});
});
describe("getContact", () => {
it("fetches a specific contact by ID", async () => {
const mockSdk = createMockSdk();
mockSdk.contacts.find.mockResolvedValue({
data: {
id: "contact-123",
givenName: "John",
surname: "Doe",
},
});
const client = new NylasClient({
config: parseNylasConfig({ apiKey: "test-key", defaultGrantId: "grant-1" }),
});
(client as any).sdk = mockSdk;
const result = await client.getContact("contact-123");
expect(mockSdk.contacts.find).toHaveBeenCalledWith({
identifier: "grant-1",
contactId: "contact-123",
});
expect(result.data.givenName).toBe("John");
});
});
describe("listFolders", () => {
it("lists email folders", async () => {
const mockSdk = createMockSdk();
mockSdk.folders.list.mockResolvedValue({
data: [
{ id: "folder-1", name: "INBOX" },
{ id: "folder-2", name: "SENT" },
],
});
const client = new NylasClient({
config: parseNylasConfig({ apiKey: "test-key", defaultGrantId: "grant-1" }),
});
(client as any).sdk = mockSdk;
const result = await client.listFolders();
expect(mockSdk.folders.list).toHaveBeenCalledWith({ identifier: "grant-1" });
expect(result.data).toHaveLength(2);
});
});
describe("listThreads", () => {
it("lists email threads with filters", async () => {
const mockSdk = createMockSdk();
mockSdk.threads.list.mockResolvedValue({
data: [
{ id: "thread-1", subject: "Thread 1" },
{ id: "thread-2", subject: "Thread 2" },
],
});
const client = new NylasClient({
config: parseNylasConfig({ apiKey: "test-key", defaultGrantId: "grant-1" }),
});
(client as any).sdk = mockSdk;
const result = await client.listThreads({ limit: 10, unread: true });
expect(mockSdk.threads.list).toHaveBeenCalledWith({
identifier: "grant-1",
queryParams: { limit: 10, unread: true },
});
expect(result.data).toHaveLength(2);
});
it("lists threads without params", async () => {
const mockSdk = createMockSdk();
mockSdk.threads.list.mockResolvedValue({ data: [] });
const client = new NylasClient({
config: parseNylasConfig({ apiKey: "test-key", defaultGrantId: "grant-1" }),
});
(client as any).sdk = mockSdk;
await client.listThreads();
expect(mockSdk.threads.list).toHaveBeenCalledWith({
identifier: "grant-1",
queryParams: undefined,
});
});
});
describe("listEvents", () => {
it("lists events with calendar and date range", async () => {
const mockSdk = createMockSdk();
mockSdk.events.list.mockResolvedValue({
data: [
{ id: "event-1", title: "Meeting" },
],
});
const client = new NylasClient({
config: parseNylasConfig({ apiKey: "test-key", defaultGrantId: "grant-1" }),
});
(client as any).sdk = mockSdk;
const result = await client.listEvents({
calendarId: "cal-1",
start: 1700000000,
end: 1700010000,
limit: 10,
});
expect(mockSdk.events.list).toHaveBeenCalledWith({
identifier: "grant-1",
queryParams: { calendarId: "cal-1", start: 1700000000, end: 1700010000, limit: 10 },
});
expect(result.data).toHaveLength(1);
});
it("uses primary calendar by default", async () => {
const mockSdk = createMockSdk();
mockSdk.events.list.mockResolvedValue({ data: [] });
const client = new NylasClient({
config: parseNylasConfig({ apiKey: "test-key", defaultGrantId: "grant-1" }),
});
(client as any).sdk = mockSdk;
await client.listEvents();
expect(mockSdk.events.list).toHaveBeenCalledWith({
identifier: "grant-1",
queryParams: { calendarId: "primary" },
});
});
});
describe("listContacts", () => {
it("lists contacts with filters", async () => {
const mockSdk = createMockSdk();
mockSdk.contacts.list.mockResolvedValue({
data: [
{ id: "contact-1", givenName: "John" },
],
});
const client = new NylasClient({
config: parseNylasConfig({ apiKey: "test-key", defaultGrantId: "grant-1" }),
});
(client as any).sdk = mockSdk;
const result = await client.listContacts({ limit: 10, email: "john@example.com" });
expect(mockSdk.contacts.list).toHaveBeenCalledWith({
identifier: "grant-1",
queryParams: { limit: 10, email: "john@example.com" },
});
expect(result.data).toHaveLength(1);
});
it("lists contacts without params", async () => {
const mockSdk = createMockSdk();
mockSdk.contacts.list.mockResolvedValue({ data: [] });
const client = new NylasClient({
config: parseNylasConfig({ apiKey: "test-key", defaultGrantId: "grant-1" }),
});
(client as any).sdk = mockSdk;
await client.listContacts();
expect(mockSdk.contacts.list).toHaveBeenCalledWith({
identifier: "grant-1",
queryParams: undefined,
});
});
});
describe("error handling in methods", () => {
it("wraps errors from listThreads", async () => {
const mockSdk = createMockSdk();
mockSdk.threads.list.mockRejectedValue(new Error("Network error"));
const client = new NylasClient({
config: parseNylasConfig({ apiKey: "test-key", defaultGrantId: "grant-1" }),
});
(client as any).sdk = mockSdk;
await expect(client.listThreads()).rejects.toThrow(NylasApiError);
});
it("wraps errors from listEvents", async () => {
const mockSdk = createMockSdk();
mockSdk.events.list.mockRejectedValue({ statusCode: 404, message: "Not found" });
const client = new NylasClient({
config: parseNylasConfig({ apiKey: "test-key", defaultGrantId: "grant-1" }),
});
(client as any).sdk = mockSdk;
await expect(client.listEvents()).rejects.toThrow(NylasApiError);
});
it("wraps errors from listContacts", async () => {
const mockSdk = createMockSdk();
mockSdk.contacts.list.mockRejectedValue({ statusCode: 400, message: "Bad request" });
const client = new NylasClient({
config: parseNylasConfig({ apiKey: "test-key", defaultGrantId: "grant-1" }),
});
(client as any).sdk = mockSdk;
await expect(client.listContacts()).rejects.toThrow(NylasApiError);
});
it("wraps errors from listGrants", async () => {
const mockSdk = createMockSdk();
mockSdk.grants.list.mockRejectedValue({ statusCode: 401, message: "Unauthorized" });
const client = new NylasClient({
config: parseNylasConfig({ apiKey: "test-key", defaultGrantId: "grant-1" }),
});
(client as any).sdk = mockSdk;
await expect(client.listGrants()).rejects.toThrow(NylasApiError);
});
it("wraps errors from getGrant", async () => {
const mockSdk = createMockSdk();
mockSdk.grants.find.mockRejectedValue({ statusCode: 404, message: "Not found" });
const client = new NylasClient({
config: parseNylasConfig({ apiKey: "test-key", defaultGrantId: "grant-1" }),
});
(client as any).sdk = mockSdk;
await expect(client.getGrant("invalid-id")).rejects.toThrow(NylasApiError);
});
});
});
// ===========================================================================
// Calendar Tools - Additional Tests
// ===========================================================================
describe("Calendar Tools - Additional", () => {
describe("listCalendars", () => {
it("handles calendars without isPrimary field", async () => {
const mockClient = {
listCalendars: vi.fn().mockResolvedValue({
data: [
{ id: "cal-1", name: "Calendar 1" },
{ id: "cal-2", name: "Calendar 2", isPrimary: true },
],
}),
};
const result = await listCalendars(mockClient as any, {});
expect(result.calendars).toHaveLength(2);
expect(result.calendars[0].is_primary).toBeUndefined();
expect(result.calendars[1].is_primary).toBe(true);
});
});
describe("listEvents with edge cases", () => {
it("handles events with different when types", async () => {
const mockClient = {
listEvents: vi.fn().mockResolvedValue({
data: [
// Date range (all-day multi-day event)
{ id: "event-1", title: "Vacation", when: { startDate: "2024-03-20", endDate: "2024-03-25" } },
// Single date (all-day)
{ id: "event-2", title: "Holiday", when: { date: "2024-03-20" } },
// Single time (point-in-time)
{ id: "event-3", title: "Reminder", when: { time: 1700000000 } },
// Time range (normal event)
{ id: "event-4", title: "Meeting", when: { startTime: 1700000000, endTime: 1700003600 } },
// Unknown format
{ id: "event-5", title: "Unknown", when: { custom: "value" } },
// Missing when
{ id: "event-6", title: "No Time" },
],
nextCursor: null,
}),
};
const result = await listEvents(mockClient as any, { calendar_id: "cal-1" });
expect(result.events).toHaveLength(6);
// Date range (all-day)
expect(result.events[0].start).toBe("2024-03-20");
expect(result.events[0].end).toBe("2024-03-25");
expect(result.events[0].all_day).toBe(true);
// Single date (all-day)
expect(result.events[1].start).toBe("2024-03-20");
expect(result.events[1].all_day).toBe(true);
// Single time
expect(result.events[2].start).toContain("2023-11");
// Time range
expect(result.events[3].start).toContain("2023-11");
expect(result.events[3].end).toContain("2023-11");
// Unknown format - should stringify
expect(result.events[4].start).toContain("custom");
// Missing when
expect(result.events[5].start).toBe("unknown");
});
});
describe("checkAvailability", () => {
it("checks availability for multiple participants", async () => {
const mockClient = {
checkAvailability: vi.fn().mockResolvedValue({
data: {
timeSlots: [
{ startTime: 1700000000, endTime: 1700001800 },
{ startTime: 1700003600, endTime: 1700005400 },
],
order: ["alice@example.com", "bob@example.com"],
},
}),
};
const result = await checkAvailability(mockClient as any, {
emails: "alice@example.com, bob@example.com",
start: "2024-03-20T09:00:00Z",
end: "2024-03-20T17:00:00Z",
duration_minutes: 30,
});
expect(result.available_slots).toHaveLength(2);
expect(result.slot_count).toBe(2);
expect(result.checked_emails).toContain("alice@example.com");
expect(result.checked_emails).toContain("bob@example.com");
expect(mockClient.checkAvailability).toHaveBeenCalledWith({
startTime: expect.any(Number),
endTime: expect.any(Number),
participants: [{ email: "alice@example.com" }, { email: "bob@example.com" }],
durationMinutes: 30,
intervalMinutes: 15,
});
});
});
describe("getEvent", () => {
it("fetches and formats event details", async () => {
const mockClient = {
getEvent: vi.fn().mockResolvedValue({
data: {
id: "event-123",
calendarId: "cal-1",
title: "Team Standup",
description: "Daily standup meeting",
location: "Zoom",
when: { startTime: 1700000000, endTime: 1700001800 },
participants: [
{ email: "alice@example.com", name: "Alice", status: "yes" },
{ email: "bob@example.com", status: "noreply" },
],
busy: true,
status: "confirmed",
htmlLink: "https://calendar.google.com/event/123",
},
}),
};
const result = await getEvent(mockClient as any, {
event_id: "event-123",
calendar_id: "cal-1",
});
expect(result.id).toBe("event-123");
expect(result.title).toBe("Team Standup");
expect(result.description).toBe("Daily standup meeting");
expect(result.location).toBe("Zoom");
expect(result.participants).toContain("Alice alice@example.com (yes)");
expect(result.participants).toContain("bob@example.com");
expect(result.busy).toBe(true);
});
});
describe("createEvent", () => {
it("creates event with participants", async () => {
const mockClient = {
createEvent: vi.fn().mockResolvedValue({
data: { id: "new-event-123", calendarId: "cal-1", title: "New Meeting", when: { startTime: 1700000000, endTime: 1700003600 } },
}),
};
const result = await createEvent(mockClient as any, {
calendar_id: "cal-1",
title: "New Meeting",
start: "2024-03-20T14:00:00Z",
end: "2024-03-20T15:00:00Z",
participants: "alice@example.com, bob@example.com",
location: "Room A",
description: "Important meeting",
});
expect(result.event_id).toBe("new-event-123");
expect(result.success).toBe(true);
expect(mockClient.createEvent).toHaveBeenCalledWith(expect.objectContaining({
calendarId: "cal-1",
title: "New Meeting",
location: "Room A",
description: "Important meeting",
}));
});
it("creates all-day event", async () => {
const mockClient = {
createEvent: vi.fn().mockResolvedValue({
data: { id: "allday-123", calendarId: "cal-1", title: "All Day Event", when: { date: "2024-03-20" } },
}),
};
const result = await createEvent(mockClient as any, {
calendar_id: "cal-1",
title: "All Day Event",
start: "2024-03-20",
all_day: true,
});
expect(result.event_id).toBe("allday-123");
expect(result.success).toBe(true);
expect(mockClient.createEvent).toHaveBeenCalled();
});
});
describe("updateEvent", () => {
it("updates event fields", async () => {
const mockClient = {
updateEvent: vi.fn().mockResolvedValue({
data: { id: "event-123", title: "Updated Title", when: { startTime: 1700000000, endTime: 1700003600 } },
}),
};
const result = await updateEvent(mockClient as any, {
event_id: "event-123",
calendar_id: "cal-1",
title: "Updated Title",
location: "New Room",
});
expect(result.event_id).toBe("event-123");
expect(result.success).toBe(true);
expect(mockClient.updateEvent).toHaveBeenCalledWith("event-123", "cal-1", expect.objectContaining({
title: "Updated Title",
location: "New Room",
}));
});
});
describe("deleteEvent", () => {
it("deletes an event", async () => {
const mockClient = {
deleteEvent: vi.fn().mockResolvedValue(undefined),
};
const result = await deleteEvent(mockClient as any, {
event_id: "event-123",
calendar_id: "cal-1",
});
expect(result.success).toBe(true);
expect(result.event_id).toBe("event-123");
expect(mockClient.deleteEvent).toHaveBeenCalledWith("event-123", "cal-1", undefined);
});
});
});
// ===========================================================================
// Email Tools - Additional Tests
// ===========================================================================
describe("Email Tools - Additional", () => {
describe("sendEmail", () => {
it("sends email with CC and BCC", async () => {
const mockClient = {
sendMessage: vi.fn().mockResolvedValue({
data: {
id: "sent-123",
threadId: "thread-456",
},
}),
};
const result = await sendEmail(mockClient as any, {
to: "recipient@example.com",
cc: "cc@example.com",
bcc: "bcc@example.com",
subject: "Test Subject",
body: "<p>Test body</p>",
});
expect(result.message_id).toBe("sent-123");
expect(result.success).toBe(true);
expect(result.thread_id).toBe("thread-456");
expect(mockClient.sendMessage).toHaveBeenCalledWith(expect.objectContaining({
to: [{ email: "recipient@example.com" }],
cc: [{ email: "cc@example.com" }],
bcc: [{ email: "bcc@example.com" }],
subject: "Test Subject",
body: "<p>Test body</p>",
}));
});
it("parses named email addresses", async () => {
const mockClient = {
sendMessage: vi.fn().mockResolvedValue({
data: { id: "sent-123" },
}),
};
await sendEmail(mockClient as any, {
to: "John Doe <john@example.com>, jane@example.com",
subject: "Test",
body: "Body",
});
expect(mockClient.sendMessage).toHaveBeenCalledWith(expect.objectContaining({
to: [
{ name: "John Doe", email: "john@example.com" },
{ email: "jane@example.com" },
],
}));
});
});
describe("createDraft", () => {
it("creates a draft email", async () => {
const mockClient = {
createDraft: vi.fn().mockResolvedValue({
data: { id: "draft-123", threadId: "thread-789" },
}),
};
const result = await createDraft(mockClient as any, {
to: "recipient@example.com",
subject: "Draft Subject",
body: "<p>Draft body</p>",
});
expect(result.draft_id).toBe("draft-123");
expect(result.success).toBe(true);
expect(result.thread_id).toBe("thread-789");
});
});
});
// ===========================================================================
// CLI Tests
// ===========================================================================
describe("CLI Commands", () => {
it("registers all CLI commands", () => {
const registeredCommands: string[] = [];
const registeredOptions: string[] = [];
const mockSubCommand = {
description: vi.fn().mockReturnThis(),
option: vi.fn().mockImplementation((opt) => {
registeredOptions.push(opt);
return mockSubCommand;
}),
action: vi.fn().mockReturnThis(),
};
const mockNylasCommand = {
description: vi.fn().mockReturnThis(),
command: vi.fn().mockImplementation((name) => {
registeredCommands.push(name);
return mockSubCommand;
}),
};
const mockProgram = {
command: vi.fn().mockReturnValue(mockNylasCommand),
};
const config = parseNylasConfig({ apiKey: "test-key", defaultGrantId: "grant-1" });
const logger = { info: vi.fn(), warn: vi.fn(), error: vi.fn() };
registerNylasCli({ program: mockProgram as any, config, logger });
// Main nylas command should be registered
expect(mockProgram.command).toHaveBeenCalledWith("nylas");
expect(mockNylasCommand.description).toHaveBeenCalledWith(expect.stringContaining("Nylas"));
// Sub-commands should be registered
expect(registeredCommands).toContain("status");
expect(registeredCommands).toContain("discover");
expect(registeredCommands).toContain("test");
expect(registeredCommands).toContain("grants");
// Options should be registered
expect(registeredOptions.some((o) => o.includes("--json"))).toBe(true);
expect(registeredOptions.some((o) => o.includes("--grant"))).toBe(true);
expect(registeredOptions.some((o) => o.includes("--configured"))).toBe(true);
});
it("registerNylasCli is exported and callable", () => {
expect(typeof registerNylasCli).toBe("function");
});
});