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 vi.mock("nylas", () => { return { default: vi.fn().mockImplementation(() => ({ 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("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 "); 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: "

Email body

", 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("

Email body

"); 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 ", 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 "); 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: "

Hello

", }, }); 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: "

Test body

", 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: "

Test body

", 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: "

Draft body

", }); expect(mockSdk.drafts.create).toHaveBeenCalledWith({ identifier: "grant-1", requestBody: { to: [{ email: "to@example.com" }], cc: undefined, bcc: undefined, subject: "Draft Subject", body: "

Draft body

", 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: "

Test body

", }); 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: "

Test body

", })); }); it("parses named email addresses", async () => { const mockClient = { sendMessage: vi.fn().mockResolvedValue({ data: { id: "sent-123" }, }), }; await sendEmail(mockClient as any, { to: "John Doe , 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: "

Draft body

", }); 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"); }); });