Skip to main content
Glama
calendar-edge-cases.test.js12.7 kB
/** * Edge case tests for calendar indexing * Tests recurring events, all-day events, and date range filtering */ import { describe, it, expect, beforeEach, vi } from 'vitest' import { createEmbeddingMock, MAC_ABSOLUTE_EPOCH } from '../helpers/indexing-mocks.js' describe('Calendar Edge Cases', () => { let mockEmbedder beforeEach(() => { vi.clearAllMocks() const mock = createEmbeddingMock() mockEmbedder = mock.mockEmbedder }) describe('recurring events via OccurrenceCache', () => { it('should identify recurring events by recurrence join', () => { // Simulated SQL result with recurrence data const event = { ROWID: 123, summary: 'Weekly Meeting', recurrence_id: 456 // Non-null means recurring } const isRecurring = event.recurrence_id !== null expect(isRecurring).toBe(true) }) it('should handle non-recurring events', () => { const event = { ROWID: 123, summary: 'One-time Meeting', recurrence_id: null } const isRecurring = event.recurrence_id !== null expect(isRecurring).toBe(false) }) it('should use OccurrenceCache for calculated dates', () => { // OccurrenceCache contains actual occurrence dates for recurring events const occurrences = [ { occurrence_date: 725760000, owner_id: 123 }, // Jan 1, 2024 { occurrence_date: 726364800, owner_id: 123 }, // Jan 8, 2024 { occurrence_date: 726969600, owner_id: 123 } // Jan 15, 2024 ] expect(occurrences.length).toBe(3) // Each occurrence should have same owner_id (parent event) const uniqueOwners = new Set(occurrences.map(o => o.owner_id)) expect(uniqueOwners.size).toBe(1) }) it('should calculate occurrence count', () => { const event = { ROWID: 123, summary: 'Weekly Meeting', occurrence_count: 52 // Weekly for 1 year } expect(event.occurrence_count).toBe(52) }) }) describe('all-day events', () => { it('should detect all_day=1', () => { const event = { summary: 'Holiday', all_day: 1 } const isAllDay = event.all_day === 1 expect(isAllDay).toBe(true) }) it('should detect all_day=0', () => { const event = { summary: 'Meeting', all_day: 0 } const isAllDay = event.all_day === 1 expect(isAllDay).toBe(false) }) it('should handle NULL all_day', () => { const event = { summary: 'Event', all_day: null } const isAllDay = event.all_day === 1 expect(isAllDay).toBe(false) }) it('should have 24-hour duration for all-day events', () => { const event = { start_date: 725760000, // Jan 1, 2024 00:00:00 end_date: 725846400, // Jan 2, 2024 00:00:00 all_day: 1 } const durationSeconds = event.end_date - event.start_date const durationHours = durationSeconds / 3600 expect(durationHours).toBe(24) }) }) describe('multi-day events', () => { it('should handle events spanning multiple days', () => { const event = { summary: 'Conference', start_date: 725760000, // Jan 1, 2024 end_date: 726019200, // Jan 4, 2024 all_day: 1 } const durationDays = (event.end_date - event.start_date) / (24 * 3600) expect(durationDays).toBe(3) }) it('should handle overnight non-all-day events', () => { const event = { summary: 'Night Shift', start_date: 725796000, // Jan 1, 2024 10:00 end_date: 725824800, // Jan 2, 2024 02:00 all_day: 0 } const durationHours = (event.end_date - event.start_date) / 3600 expect(durationHours).toBeGreaterThan(0) }) }) describe('location extraction', () => { it('should extract location from event', () => { const event = { summary: 'Meeting', location: 'Conference Room A' } expect(event.location).toBe('Conference Room A') }) it('should handle NULL location', () => { const event = { summary: 'Call', location: null } const location = event.location || '' expect(location).toBe('') }) it('should handle location with address', () => { const event = { summary: 'Lunch', location: '123 Main St, City, State 12345' } expect(event.location).toContain('Main St') }) it('should handle virtual meeting locations', () => { const event = { summary: 'Video Call', location: 'https://zoom.us/j/123456789' } expect(event.location).toContain('zoom.us') }) }) describe('notes truncation', () => { it('should truncate notes to 200 chars', () => { const longNotes = 'x'.repeat(500) const maxLength = 200 const truncated = longNotes.substring(0, maxLength) expect(truncated.length).toBe(200) }) it('should preserve short notes', () => { const shortNotes = 'Brief meeting notes' const maxLength = 200 const result = shortNotes.length > maxLength ? shortNotes.substring(0, maxLength) : shortNotes expect(result).toBe(shortNotes) }) it('should handle NULL notes', () => { const event = { notes: null } const notes = event.notes || '' expect(notes).toBe('') }) }) describe('participant status codes', () => { it('should map status 0 to unknown', () => { const statusMap = { 0: 'unknown', 1: 'pending', 2: 'accepted', 3: 'declined', 4: 'tentative', 5: 'delegated', 6: 'completed', 7: 'in process' } expect(statusMap[0]).toBe('unknown') }) it('should map status 2 to accepted', () => { const statusMap = { 0: 'unknown', 1: 'pending', 2: 'accepted', 3: 'declined', 4: 'tentative' } expect(statusMap[2]).toBe('accepted') }) it('should handle unknown status codes', () => { const statusMap = { 0: 'unknown', 1: 'pending', 2: 'accepted', 3: 'declined', 4: 'tentative' } const getStatus = (code) => statusMap[code] || 'unknown' expect(getStatus(99)).toBe('unknown') }) it('should format participant with status', () => { const participant = { email: 'john@example.com', status: 2 } const statusMap = { 2: 'accepted' } const formatted = `${participant.email} (${statusMap[participant.status]})` expect(formatted).toBe('john@example.com (accepted)') }) }) describe('time zone handling', () => { it('should handle UTC times', () => { // Mac Absolute Time is always UTC const macTime = 725760000 // Jan 1, 2024 00:00:00 UTC const unixTime = macTime + MAC_ABSOLUTE_EPOCH const date = new Date(unixTime * 1000) expect(date.getUTCFullYear()).toBe(2024) expect(date.getUTCMonth()).toBe(0) // January expect(date.getUTCDate()).toBe(1) }) it('should handle timezone offset events', () => { // Event in PST (UTC-8) const event = { start_date: 725760000, // UTC timezone: 'America/Los_Angeles' } // Conversion would be done during display, not storage expect(event.timezone).toBe('America/Los_Angeles') }) it('should handle floating timezones', () => { // Floating = same local time regardless of timezone const event = { start_date: 725760000, timezone: 'floating' } expect(event.timezone).toBe('floating') }) }) describe('date range filtering', () => { it('should filter events within 90 days back', () => { const now = Date.now() const ninetyDaysAgo = now - (90 * 24 * 60 * 60 * 1000) const events = [ { start: now - (30 * 24 * 60 * 60 * 1000), title: 'Recent' }, { start: now - (100 * 24 * 60 * 60 * 1000), title: 'Old' } ] const filtered = events.filter(e => e.start >= ninetyDaysAgo) expect(filtered.length).toBe(1) expect(filtered[0].title).toBe('Recent') }) it('should filter events within 365 days ahead', () => { const now = Date.now() const oneYearAhead = now + (365 * 24 * 60 * 60 * 1000) const events = [ { start: now + (30 * 24 * 60 * 60 * 1000), title: 'Soon' }, { start: now + (400 * 24 * 60 * 60 * 1000), title: 'Far' } ] const filtered = events.filter(e => e.start <= oneYearAhead) expect(filtered.length).toBe(1) expect(filtered[0].title).toBe('Soon') }) it('should include today in range', () => { const now = Date.now() const ninetyDaysAgo = now - (90 * 24 * 60 * 60 * 1000) const oneYearAhead = now + (365 * 24 * 60 * 60 * 1000) const todayEvent = { start: now, title: 'Today' } const inRange = todayEvent.start >= ninetyDaysAgo && todayEvent.start <= oneYearAhead expect(inRange).toBe(true) }) }) describe('composite ID generation', () => { it('should create ID from title and start', () => { const event = { summary: 'Team Meeting', start_date: 725760000 } const id = `${event.summary}-${event.start_date}` expect(id).toBe('Team Meeting-725760000') }) it('should handle events with same title different times', () => { const event1 = { summary: 'Standup', start_date: 725760000 } const event2 = { summary: 'Standup', start_date: 725846400 } const id1 = `${event1.summary}-${event1.start_date}` const id2 = `${event2.summary}-${event2.start_date}` expect(id1).not.toBe(id2) }) it('should handle special characters in title', () => { const event = { summary: 'Meeting: Q1 Review (Important!)', start_date: 725760000 } const id = `${event.summary}-${event.start_date}` expect(id).toContain('Meeting:') expect(id).toContain('(Important!)') }) }) describe('attendee serialization', () => { it('should serialize attendees to JSON', () => { const attendees = [ { email: 'john@example.com', status: 'accepted' }, { email: 'jane@example.com', status: 'pending' } ] const serialized = JSON.stringify(attendees) const parsed = JSON.parse(serialized) expect(parsed.length).toBe(2) expect(parsed[0].email).toBe('john@example.com') }) it('should handle empty attendees', () => { const attendees = [] const serialized = JSON.stringify(attendees) expect(serialized).toBe('[]') }) it('should count attendees', () => { const attendees = [ { email: 'a@example.com' }, { email: 'b@example.com' }, { email: 'c@example.com' } ] expect(attendees.length).toBe(3) }) }) describe('stale event detection', () => { it('should identify events not in source', () => { const indexedIds = ['event-1', 'event-2', 'event-3'] const sourceIds = ['event-1', 'event-3'] // event-2 deleted const staleIds = indexedIds.filter(id => !sourceIds.includes(id)) expect(staleIds).toEqual(['event-2']) }) it('should handle no stale events', () => { const indexedIds = ['event-1', 'event-2'] const sourceIds = ['event-1', 'event-2', 'event-3'] const staleIds = indexedIds.filter(id => !sourceIds.includes(id)) expect(staleIds.length).toBe(0) }) it('should handle all events stale', () => { const indexedIds = ['old-1', 'old-2'] const sourceIds = ['new-1', 'new-2'] const staleIds = indexedIds.filter(id => !sourceIds.includes(id)) expect(staleIds).toEqual(['old-1', 'old-2']) }) }) describe('search text generation', () => { it('should combine title, location, and notes', () => { const event = { summary: 'Team Meeting', location: 'Room 101', notes: 'Discuss Q1 goals' } const searchText = `${event.summary} ${event.location || ''} ${event.notes || ''}` .trim() .substring(0, 500) expect(searchText).toContain('Team Meeting') expect(searchText).toContain('Room 101') expect(searchText).toContain('Q1 goals') }) it('should handle missing fields gracefully', () => { const event = { summary: 'Call', location: null, notes: null } const searchText = `${event.summary} ${event.location || ''} ${event.notes || ''}` .trim() expect(searchText).toBe('Call') }) }) })

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/sfls1397/Apple-Tools-MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server