#
tokens: 36598/50000 4/104 files (page 4/5)
lines: off (toggle) GitHub
raw markdown copy
This is page 4 of 5. Use http://codebase.md/nspady/google-calendar-mcp?page={x} to view the full context.

# Directory Structure

```
├── .cursorignore
├── .dockerignore
├── .env.example
├── .github
│   └── workflows
│       ├── ci.yml
│       ├── publish.yml
│       └── README.md
├── .gitignore
├── .release-please-manifest.json
├── AGENTS.md
├── CHANGELOG.md
├── CLAUDE.md
├── docker-compose.yml
├── Dockerfile
├── docs
│   ├── advanced-usage.md
│   ├── architecture.md
│   ├── authentication.md
│   ├── deployment.md
│   ├── development.md
│   ├── docker.md
│   ├── README.md
│   └── testing.md
├── examples
│   ├── http-client.js
│   └── http-with-curl.sh
├── future_features
│   └── ARCHITECTURE_REDESIGN.md
├── gcp-oauth.keys.example.json
├── instructions
│   └── file_structure.md
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── scripts
│   ├── account-manager.js
│   ├── build.js
│   ├── dev.js
│   └── test-docker.sh
├── src
│   ├── auth
│   │   ├── client.ts
│   │   ├── paths.d.ts
│   │   ├── paths.js
│   │   ├── server.ts
│   │   ├── tokenManager.ts
│   │   └── utils.ts
│   ├── auth-server.ts
│   ├── config
│   │   └── TransportConfig.ts
│   ├── handlers
│   │   ├── core
│   │   │   ├── BaseToolHandler.ts
│   │   │   ├── BatchRequestHandler.ts
│   │   │   ├── CreateEventHandler.ts
│   │   │   ├── DeleteEventHandler.ts
│   │   │   ├── FreeBusyEventHandler.ts
│   │   │   ├── GetCurrentTimeHandler.ts
│   │   │   ├── GetEventHandler.ts
│   │   │   ├── ListCalendarsHandler.ts
│   │   │   ├── ListColorsHandler.ts
│   │   │   ├── ListEventsHandler.ts
│   │   │   ├── RecurringEventHelpers.ts
│   │   │   ├── SearchEventsHandler.ts
│   │   │   └── UpdateEventHandler.ts
│   │   ├── utils
│   │   │   └── datetime.ts
│   │   └── utils.ts
│   ├── index.ts
│   ├── schemas
│   │   └── types.ts
│   ├── server.ts
│   ├── services
│   │   └── conflict-detection
│   │       ├── config.ts
│   │       ├── ConflictAnalyzer.ts
│   │       ├── ConflictDetectionService.ts
│   │       ├── EventSimilarityChecker.ts
│   │       ├── index.ts
│   │       └── types.ts
│   ├── tests
│   │   ├── integration
│   │   │   ├── claude-mcp-integration.test.ts
│   │   │   ├── direct-integration.test.ts
│   │   │   ├── docker-integration.test.ts
│   │   │   ├── openai-mcp-integration.test.ts
│   │   │   └── test-data-factory.ts
│   │   └── unit
│   │       ├── console-statements.test.ts
│   │       ├── handlers
│   │       │   ├── BatchListEvents.test.ts
│   │       │   ├── BatchRequestHandler.test.ts
│   │       │   ├── CalendarNameResolution.test.ts
│   │       │   ├── create-event-blocking.test.ts
│   │       │   ├── CreateEventHandler.test.ts
│   │       │   ├── datetime-utils.test.ts
│   │       │   ├── duplicate-event-display.test.ts
│   │       │   ├── GetCurrentTimeHandler.test.ts
│   │       │   ├── GetEventHandler.test.ts
│   │       │   ├── list-events-registry.test.ts
│   │       │   ├── ListEventsHandler.test.ts
│   │       │   ├── RecurringEventHelpers.test.ts
│   │       │   ├── UpdateEventHandler.recurring.test.ts
│   │       │   ├── UpdateEventHandler.test.ts
│   │       │   ├── utils-conflict-format.test.ts
│   │       │   └── utils.test.ts
│   │       ├── index.test.ts
│   │       ├── schemas
│   │       │   ├── enhanced-properties.test.ts
│   │       │   ├── no-refs.test.ts
│   │       │   ├── schema-compatibility.test.ts
│   │       │   ├── tool-registration.test.ts
│   │       │   └── validators.test.ts
│   │       ├── services
│   │       │   └── conflict-detection
│   │       │       ├── ConflictAnalyzer.test.ts
│   │       │       └── EventSimilarityChecker.test.ts
│   │       └── utils
│   │           ├── event-id-validator.test.ts
│   │           └── field-mask-builder.test.ts
│   ├── tools
│   │   └── registry.ts
│   ├── transports
│   │   ├── http.ts
│   │   └── stdio.ts
│   ├── types
│   │   └── structured-responses.ts
│   └── utils
│       ├── event-id-validator.ts
│       ├── field-mask-builder.ts
│       └── response-builder.ts
├── tsconfig.json
├── tsconfig.lint.json
└── vitest.config.ts
```

# Files

--------------------------------------------------------------------------------
/src/tests/unit/handlers/UpdateEventHandler.recurring.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { OAuth2Client } from 'google-auth-library';
import { calendar_v3 } from 'googleapis';

// Enhanced UpdateEventHandler class that will be implemented
class EnhancedUpdateEventHandler {
  private calendar: calendar_v3.Calendar;

  constructor(calendar: calendar_v3.Calendar) {
    this.calendar = calendar;
  }

  async runTool(args: any, oauth2Client: OAuth2Client): Promise<any> {
    // This would use the enhanced schema for validation
    const event = await this.updateEventWithScope(args);
    return {
      content: [{
        type: "text",
        text: `Event updated: ${event.summary} (${event.id})`,
      }],
    };
  }

  async updateEventWithScope(args: any): Promise<calendar_v3.Schema$Event> {
    const eventType = await this.detectEventType(args.eventId, args.calendarId);
    
    // Validate scope usage
    if (args.modificationScope !== 'all' && eventType !== 'recurring') {
      throw new Error('Scope other than "all" only applies to recurring events');
    }
    
    switch (args.modificationScope || 'all') {
      case 'single':
        return this.updateSingleInstance(args);
      case 'all':
        return this.updateAllInstances(args);
      case 'future':
        return this.updateFutureInstances(args);
      default:
        throw new Error(`Invalid modification scope: ${args.modificationScope}`);
    }
  }

  private async detectEventType(eventId: string, calendarId: string): Promise<'recurring' | 'single'> {
    const response = await this.calendar.events.get({
      calendarId,
      eventId
    });

    const event = response.data;
    return event.recurrence && event.recurrence.length > 0 ? 'recurring' : 'single';
  }

  async updateSingleInstance(args: any): Promise<calendar_v3.Schema$Event> {
    // Format instance ID: eventId_basicTimeFormat (convert to UTC first)
    const utcDate = new Date(args.originalStartTime);
    const basicTimeFormat = utcDate.toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z';
    const instanceId = `${args.eventId}_${basicTimeFormat}`;
    
    const response = await this.calendar.events.patch({
      calendarId: args.calendarId,
      eventId: instanceId,
      requestBody: this.buildUpdateRequestBody(args)
    });

    if (!response.data) throw new Error('Failed to update event instance');
    return response.data;
  }

  async updateAllInstances(args: any): Promise<calendar_v3.Schema$Event> {
    const response = await this.calendar.events.patch({
      calendarId: args.calendarId,
      eventId: args.eventId,
      requestBody: this.buildUpdateRequestBody(args)
    });

    if (!response.data) throw new Error('Failed to update event');
    return response.data;
  }

  async updateFutureInstances(args: any): Promise<calendar_v3.Schema$Event> {
    // 1. Get original event
    const originalResponse = await this.calendar.events.get({
      calendarId: args.calendarId,
      eventId: args.eventId
    });
    const originalEvent = originalResponse.data;

    if (!originalEvent.recurrence) {
      throw new Error('Event does not have recurrence rules');
    }

    // 2. Calculate UNTIL date (one day before future start date)
    const futureDate = new Date(args.futureStartDate);
    const untilDate = new Date(futureDate.getTime() - 86400000); // -1 day
    const untilString = untilDate.toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z';

    // 3. Update original event with UNTIL clause
    const updatedRRule = originalEvent.recurrence[0]
      .replace(/;UNTIL=\d{8}T\d{6}Z/g, '')
      .replace(/;COUNT=\d+/g, '') + `;UNTIL=${untilString}`;

    await this.calendar.events.patch({
      calendarId: args.calendarId,
      eventId: args.eventId,
      requestBody: { recurrence: [updatedRRule] }
    });

    // 4. Create new recurring event starting from future date
    const newEvent = {
      ...originalEvent,
      ...this.buildUpdateRequestBody(args),
      start: { 
        dateTime: args.futureStartDate, 
        timeZone: args.timeZone 
      },
      end: { 
        dateTime: this.calculateEndTime(args.futureStartDate, originalEvent), 
        timeZone: args.timeZone 
      }
    };

    // Clean fields that shouldn't be duplicated
    delete newEvent.id;
    delete newEvent.etag;
    delete newEvent.iCalUID;
    delete newEvent.created;
    delete newEvent.updated;
    delete newEvent.htmlLink;
    delete newEvent.hangoutLink;

    const response = await this.calendar.events.insert({
      calendarId: args.calendarId,
      requestBody: newEvent
    });

    if (!response.data) throw new Error('Failed to create new recurring event');
    return response.data;
  }

  private calculateEndTime(newStartTime: string, originalEvent: calendar_v3.Schema$Event): string {
    const newStart = new Date(newStartTime);
    const originalStart = new Date(originalEvent.start!.dateTime!);
    const originalEnd = new Date(originalEvent.end!.dateTime!);
    const duration = originalEnd.getTime() - originalStart.getTime();
    
    return new Date(newStart.getTime() + duration).toISOString();
  }

  private buildUpdateRequestBody(args: any): calendar_v3.Schema$Event {
    const requestBody: calendar_v3.Schema$Event = {};

    if (args.summary !== undefined && args.summary !== null) requestBody.summary = args.summary;
    if (args.description !== undefined && args.description !== null) requestBody.description = args.description;
    if (args.location !== undefined && args.location !== null) requestBody.location = args.location;
    if (args.colorId !== undefined && args.colorId !== null) requestBody.colorId = args.colorId;
    if (args.attendees !== undefined && args.attendees !== null) requestBody.attendees = args.attendees;
    if (args.reminders !== undefined && args.reminders !== null) requestBody.reminders = args.reminders;
    if (args.recurrence !== undefined && args.recurrence !== null) requestBody.recurrence = args.recurrence;

    // Handle time changes
    let timeChanged = false;
    if (args.start !== undefined && args.start !== null) {
      requestBody.start = { dateTime: args.start, timeZone: args.timeZone };
      timeChanged = true;
    }
    if (args.end !== undefined && args.end !== null) {
      requestBody.end = { dateTime: args.end, timeZone: args.timeZone };
      timeChanged = true;
    }

    // Only add timezone objects if there were actual time changes, OR if neither start/end provided but timezone is given
    if (timeChanged || (!args.start && !args.end && args.timeZone)) {
      if (!requestBody.start) requestBody.start = {};
      if (!requestBody.end) requestBody.end = {};
      if (!requestBody.start.timeZone) requestBody.start.timeZone = args.timeZone;
      if (!requestBody.end.timeZone) requestBody.end.timeZone = args.timeZone;
    }

    return requestBody;
  }
}

// Custom error class for recurring event errors
class RecurringEventError extends Error {
  public code: string;

  constructor(message: string, code: string) {
    super(message);
    this.name = 'RecurringEventError';
    this.code = code;
  }
}

const ERRORS = {
  INVALID_SCOPE: 'INVALID_MODIFICATION_SCOPE',
  MISSING_ORIGINAL_TIME: 'MISSING_ORIGINAL_START_TIME',
  MISSING_FUTURE_DATE: 'MISSING_FUTURE_START_DATE',
  PAST_FUTURE_DATE: 'FUTURE_DATE_IN_PAST',
  NON_RECURRING_SCOPE: 'SCOPE_NOT_APPLICABLE_TO_SINGLE_EVENT'
};

describe('UpdateEventHandler - Recurring Events', () => {
  let handler: EnhancedUpdateEventHandler;
  let mockCalendar: any;
  let mockOAuth2Client: OAuth2Client;

  beforeEach(() => {
    mockCalendar = {
      events: {
        get: vi.fn(),
        patch: vi.fn(),
        insert: vi.fn()
      }
    };
    handler = new EnhancedUpdateEventHandler(mockCalendar);
    mockOAuth2Client = {} as OAuth2Client;
  });

  describe('updateEventWithScope', () => {
    it('should detect event type and route to appropriate method', async () => {
      const recurringEvent = {
        data: {
          id: 'recurring123',
          summary: 'Weekly Meeting',
          recurrence: ['RRULE:FREQ=WEEKLY;BYDAY=MO']
        }
      };
      mockCalendar.events.get.mockResolvedValue(recurringEvent);
      mockCalendar.events.patch.mockResolvedValue({ data: recurringEvent.data });

      const args = {
        calendarId: 'primary',
        eventId: 'recurring123',
        timeZone: 'America/Los_Angeles',
        modificationScope: 'all',
        summary: 'Updated Meeting'
      };

      await handler.updateEventWithScope(args);

      expect(mockCalendar.events.get).toHaveBeenCalledWith({
        calendarId: 'primary',
        eventId: 'recurring123'
      });
      expect(mockCalendar.events.patch).toHaveBeenCalledWith({
        calendarId: 'primary',
        eventId: 'recurring123',
        requestBody: expect.objectContaining({
          summary: 'Updated Meeting'
        })
      });
    });

    it('should throw error when using non-"all" scope on single events', async () => {
      const singleEvent = {
        data: {
          id: 'single123',
          summary: 'One-time Meeting'
          // no recurrence
        }
      };
      mockCalendar.events.get.mockResolvedValue(singleEvent);

      const args = {
        calendarId: 'primary',
        eventId: 'single123',
        timeZone: 'America/Los_Angeles',
        modificationScope: 'single',
        originalStartTime: '2024-06-15T10:00:00-07:00'
      };

      await expect(handler.updateEventWithScope(args))
        .rejects.toThrow('Scope other than "all" only applies to recurring events');
    });

    it('should default to "all" scope when not specified', async () => {
      const recurringEvent = {
        data: {
          id: 'recurring123',
          recurrence: ['RRULE:FREQ=WEEKLY']
        }
      };
      mockCalendar.events.get.mockResolvedValue(recurringEvent);
      mockCalendar.events.patch.mockResolvedValue({ data: recurringEvent.data });

      const args = {
        calendarId: 'primary',
        eventId: 'recurring123',
        timeZone: 'UTC',
        summary: 'Updated Meeting'
        // no modificationScope specified
      };

      await handler.updateEventWithScope(args);

      // Should call updateAllInstances (patch with master event ID)
      expect(mockCalendar.events.patch).toHaveBeenCalledWith({
        calendarId: 'primary',
        eventId: 'recurring123',
        requestBody: expect.any(Object)
      });
    });
  });

  describe('updateSingleInstance', () => {
    it('should format instance ID correctly and patch specific instance', async () => {
      const mockInstanceEvent = {
        data: {
          id: 'recurring123_20240615T170000Z',
          summary: 'Updated Instance'
        }
      };
      mockCalendar.events.patch.mockResolvedValue(mockInstanceEvent);

      const args = {
        calendarId: 'primary',
        eventId: 'recurring123',
        timeZone: 'America/Los_Angeles',
        modificationScope: 'single',
        originalStartTime: '2024-06-15T10:00:00-07:00',
        summary: 'Updated Instance'
      };

      const result = await handler.updateSingleInstance(args);

      expect(mockCalendar.events.patch).toHaveBeenCalledWith({
        calendarId: 'primary',
        eventId: 'recurring123_20240615T170000Z',
        requestBody: expect.objectContaining({
          summary: 'Updated Instance'
        })
      });
      expect(result.summary).toBe('Updated Instance');
    });

    it('should handle different timezone formats in originalStartTime', async () => {
      const testCases = [
        {
          originalStartTime: '2024-06-15T10:00:00Z',
          expectedInstanceId: 'event123_20240615T100000Z'
        },
        {
          originalStartTime: '2024-06-15T10:00:00+05:30',
          expectedInstanceId: 'event123_20240615T043000Z'
        },
        {
          originalStartTime: '2024-06-15T10:00:00.000-08:00',
          expectedInstanceId: 'event123_20240615T180000Z'
        }
      ];

      for (const testCase of testCases) {
        mockCalendar.events.patch.mockClear();
        mockCalendar.events.patch.mockResolvedValue({ data: { id: testCase.expectedInstanceId } });

        const args = {
          calendarId: 'primary',
          eventId: 'event123',
          timeZone: 'UTC',
          originalStartTime: testCase.originalStartTime,
          summary: 'Test'
        };

        await handler.updateSingleInstance(args);

        expect(mockCalendar.events.patch).toHaveBeenCalledWith({
          calendarId: 'primary',
          eventId: testCase.expectedInstanceId,
          requestBody: expect.any(Object)
        });
      }
    });

    it('should throw error if patch fails', async () => {
      mockCalendar.events.patch.mockResolvedValue({ data: null });

      const args = {
        calendarId: 'primary',
        eventId: 'recurring123',
        originalStartTime: '2024-06-15T10:00:00Z',
        timeZone: 'UTC'
      };

      await expect(handler.updateSingleInstance(args))
        .rejects.toThrow('Failed to update event instance');
    });
  });

  describe('updateAllInstances', () => {
    it('should patch master event with all modifications', async () => {
      const mockUpdatedEvent = {
        data: {
          id: 'recurring123',
          summary: 'Updated Weekly Meeting',
          location: 'New Conference Room'
        }
      };
      mockCalendar.events.patch.mockResolvedValue(mockUpdatedEvent);

      const args = {
        calendarId: 'primary',
        eventId: 'recurring123',
        timeZone: 'America/Los_Angeles',
        modificationScope: 'all',
        summary: 'Updated Weekly Meeting',
        location: 'New Conference Room',
        colorId: '9'
      };

      const result = await handler.updateAllInstances(args);

      expect(mockCalendar.events.patch).toHaveBeenCalledWith({
        calendarId: 'primary',
        eventId: 'recurring123',
        requestBody: expect.objectContaining({
          summary: 'Updated Weekly Meeting',
          location: 'New Conference Room',
          colorId: '9'
        })
      });
      expect(result.summary).toBe('Updated Weekly Meeting');
    });

    it('should handle timezone changes for recurring events', async () => {
      const mockEvent = { data: { id: 'recurring123' } };
      mockCalendar.events.patch.mockResolvedValue(mockEvent);

      const args = {
        calendarId: 'primary',
        eventId: 'recurring123',
        timeZone: 'Europe/London',
        start: '2024-06-15T09:00:00+01:00',
        end: '2024-06-15T10:00:00+01:00'
      };

      await handler.updateAllInstances(args);

      expect(mockCalendar.events.patch).toHaveBeenCalledWith({
        calendarId: 'primary',
        eventId: 'recurring123',
        requestBody: expect.objectContaining({
          start: {
            dateTime: '2024-06-15T09:00:00+01:00',
            timeZone: 'Europe/London'
          },
          end: {
            dateTime: '2024-06-15T10:00:00+01:00',
            timeZone: 'Europe/London'
          }
        })
      });
    });
  });

  describe('updateFutureInstances', () => {
    it('should split recurring series correctly', async () => {
      const originalEvent = {
        data: {
          id: 'recurring123',
          summary: 'Weekly Meeting',
          start: { dateTime: '2024-06-01T10:00:00-07:00' },
          end: { dateTime: '2024-06-01T11:00:00-07:00' },
          recurrence: ['RRULE:FREQ=WEEKLY;BYDAY=MO;COUNT=20'],
          attendees: [{ email: '[email protected]' }]
        }
      };
      
      mockCalendar.events.get.mockResolvedValue(originalEvent);
      mockCalendar.events.patch.mockResolvedValue({ data: {} });
      
      const newEvent = {
        data: {
          id: 'new_recurring456',
          summary: 'Updated Future Meeting'
        }
      };
      mockCalendar.events.insert.mockResolvedValue(newEvent);

      const args = {
        calendarId: 'primary',
        eventId: 'recurring123',
        timeZone: 'America/Los_Angeles',
        modificationScope: 'future',
        futureStartDate: '2024-06-15T10:00:00-07:00',
        summary: 'Updated Future Meeting',
        location: 'New Location'
      };

      const result = await handler.updateFutureInstances(args);

      // Should update original event with UNTIL clause
      expect(mockCalendar.events.patch).toHaveBeenCalledWith({
        calendarId: 'primary',
        eventId: 'recurring123',
        requestBody: {
          recurrence: ['RRULE:FREQ=WEEKLY;BYDAY=MO;UNTIL=20240614T170000Z']
        }
      });

      // Should create new recurring event
      expect(mockCalendar.events.insert).toHaveBeenCalledWith({
        calendarId: 'primary',
        requestBody: expect.objectContaining({
          summary: 'Updated Future Meeting',
          location: 'New Location',
          start: {
            dateTime: '2024-06-15T10:00:00-07:00',
            timeZone: 'America/Los_Angeles'
          },
          end: {
            dateTime: expect.any(String),
            timeZone: 'America/Los_Angeles'
          },
          attendees: [{ email: '[email protected]' }]
        })
      });

      // Should not include system fields
      const insertCall = mockCalendar.events.insert.mock.calls[0][0];
      expect(insertCall.requestBody.id).toBeUndefined();
      expect(insertCall.requestBody.etag).toBeUndefined();
      expect(insertCall.requestBody.iCalUID).toBeUndefined();

      expect(result.summary).toBe('Updated Future Meeting');
    });

    it('should calculate end time correctly based on original duration', async () => {
      const originalEvent = {
        data: {
          id: 'recurring123',
          start: { dateTime: '2024-06-01T10:00:00-07:00' },
          end: { dateTime: '2024-06-01T12:30:00-07:00' }, // 2.5 hour duration
          recurrence: ['RRULE:FREQ=WEEKLY;BYDAY=MO']
        }
      };
      
      mockCalendar.events.get.mockResolvedValue(originalEvent);
      mockCalendar.events.patch.mockResolvedValue({ data: {} });
      mockCalendar.events.insert.mockResolvedValue({ data: {} });

      const args = {
        calendarId: 'primary',
        eventId: 'recurring123',
        timeZone: 'America/Los_Angeles',
        futureStartDate: '2024-06-15T14:00:00-07:00'
      };

      await handler.updateFutureInstances(args);

      const insertCall = mockCalendar.events.insert.mock.calls[0][0];
      const endDateTime = new Date(insertCall.requestBody.end.dateTime);
      const startDateTime = new Date(insertCall.requestBody.start.dateTime);
      const duration = endDateTime.getTime() - startDateTime.getTime();
      
      // Should maintain 2.5 hour duration (9000000 ms)
      expect(duration).toBe(2.5 * 60 * 60 * 1000);
    });

    it('should handle events without recurrence', async () => {
      const singleEvent = {
        data: {
          id: 'single123',
          summary: 'One-time Meeting'
          // no recurrence
        }
      };
      
      mockCalendar.events.get.mockResolvedValue(singleEvent);

      const args = {
        calendarId: 'primary',
        eventId: 'single123',
        futureStartDate: '2024-06-15T10:00:00-07:00',
        timeZone: 'UTC'
      };

      await expect(handler.updateFutureInstances(args))
        .rejects.toThrow('Event does not have recurrence rules');
    });

    it('should handle existing UNTIL and COUNT clauses correctly', async () => {
      const testCases = [
        {
          original: 'RRULE:FREQ=WEEKLY;BYDAY=MO;UNTIL=20240531T170000Z',
          expected: 'RRULE:FREQ=WEEKLY;BYDAY=MO;UNTIL=20240614T170000Z'
        },
        {
          original: 'RRULE:FREQ=WEEKLY;BYDAY=MO;COUNT=10',
          expected: 'RRULE:FREQ=WEEKLY;BYDAY=MO;UNTIL=20240614T170000Z'
        },
        {
          original: 'RRULE:FREQ=DAILY;INTERVAL=2;COUNT=15;BYHOUR=10',
          expected: 'RRULE:FREQ=DAILY;INTERVAL=2;BYHOUR=10;UNTIL=20240614T170000Z'
        }
      ];

      for (const testCase of testCases) {
        const originalEvent = {
          data: {
            id: 'test',
            start: { dateTime: '2024-06-01T10:00:00-07:00' },
            end: { dateTime: '2024-06-01T11:00:00-07:00' },
            recurrence: [testCase.original]
          }
        };

        mockCalendar.events.get.mockResolvedValue(originalEvent);
        mockCalendar.events.patch.mockClear();
        mockCalendar.events.patch.mockResolvedValue({ data: {} });
        mockCalendar.events.insert.mockResolvedValue({ data: {} });

        const args = {
          calendarId: 'primary',
          eventId: 'test',
          futureStartDate: '2024-06-15T10:00:00-07:00',
          timeZone: 'America/Los_Angeles'
        };

        await handler.updateFutureInstances(args);

        expect(mockCalendar.events.patch).toHaveBeenCalledWith({
          calendarId: 'primary',
          eventId: 'test',
          requestBody: {
            recurrence: [testCase.expected]
          }
        });
      }
    });
  });

  describe('Error Handling', () => {
    it('should handle Google API errors gracefully', async () => {
      mockCalendar.events.get.mockRejectedValue(new Error('Event not found'));

      const args = {
        calendarId: 'primary',
        eventId: 'nonexistent',
        timeZone: 'UTC'
      };

      await expect(handler.updateEventWithScope(args))
        .rejects.toThrow('Event not found');
    });

    it('should handle patch failures for single instances', async () => {
      mockCalendar.events.patch.mockRejectedValue(new Error('Instance not found'));

      const args = {
        calendarId: 'primary',
        eventId: 'recurring123',
        originalStartTime: '2024-06-15T10:00:00Z',
        timeZone: 'UTC'
      };

      await expect(handler.updateSingleInstance(args))
        .rejects.toThrow('Instance not found');
    });

    it('should handle insert failures for future instances', async () => {
      const originalEvent = {
        data: {
          id: 'recurring123',
          start: { dateTime: '2024-06-01T10:00:00Z' },
          end: { dateTime: '2024-06-01T11:00:00Z' },
          recurrence: ['RRULE:FREQ=WEEKLY']
        }
      };
      
      mockCalendar.events.get.mockResolvedValue(originalEvent);
      mockCalendar.events.patch.mockResolvedValue({ data: {} });
      mockCalendar.events.insert.mockResolvedValue({ data: null });

      const args = {
        calendarId: 'primary',
        eventId: 'recurring123',
        futureStartDate: '2024-06-15T10:00:00Z',
        timeZone: 'UTC'
      };

      await expect(handler.updateFutureInstances(args))
        .rejects.toThrow('Failed to create new recurring event');
    });
  });

  describe('Integration with Tool Framework', () => {
    it('should return proper response format from runTool', async () => {
      const mockEvent = {
        data: {
          id: 'event123',
          summary: 'Updated Meeting',
          recurrence: ['RRULE:FREQ=WEEKLY']
        }
      };
      
      mockCalendar.events.get.mockResolvedValue(mockEvent);
      mockCalendar.events.patch.mockResolvedValue(mockEvent);

      const args = {
        calendarId: 'primary',
        eventId: 'event123',
        timeZone: 'UTC',
        summary: 'Updated Meeting'
      };

      const result = await handler.runTool(args, mockOAuth2Client);

      expect(result).toEqual({
        content: [{
          type: "text",
          text: "Event updated: Updated Meeting (event123)"
        }]
      });
    });
  });

  describe('Edge Cases and Additional Scenarios', () => {
    it('should handle events with complex recurrence patterns', async () => {
      const complexRecurringEvent = {
        data: {
          id: 'complex123',
          summary: 'Complex Meeting',
          start: { dateTime: '2024-06-01T10:00:00Z' },
          end: { dateTime: '2024-06-01T11:00:00Z' },
          recurrence: ['RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR;INTERVAL=2;BYHOUR=10;BYMINUTE=0']
        }
      };
      
      mockCalendar.events.get.mockResolvedValue(complexRecurringEvent);
      mockCalendar.events.patch.mockResolvedValue({ data: {} });
      mockCalendar.events.insert.mockResolvedValue({ data: { id: 'new_complex456' } });

      const args = {
        calendarId: 'primary',
        eventId: 'complex123',
        timeZone: 'UTC',
        modificationScope: 'future',
        futureStartDate: '2024-06-15T10:00:00Z',
        summary: 'Updated Complex Meeting'
      };

      const result = await handler.updateFutureInstances(args);

      // Should handle complex recurrence rules correctly
      expect(mockCalendar.events.patch).toHaveBeenCalledWith({
        calendarId: 'primary',
        eventId: 'complex123',
        requestBody: {
          recurrence: ['RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR;INTERVAL=2;BYHOUR=10;BYMINUTE=0;UNTIL=20240614T100000Z']
        }
      });
    });

    it('should handle timezone changes across DST boundaries', async () => {
      const mockEvent = { data: { id: 'dst123' } };
      mockCalendar.events.patch.mockResolvedValue(mockEvent);

      const args = {
        calendarId: 'primary',
        eventId: 'dst123',
        timeZone: 'America/New_York',
        modificationScope: 'all',
        start: '2024-03-10T07:00:00-05:00', // DST transition date
        end: '2024-03-10T08:00:00-05:00'
      };

      await handler.updateAllInstances(args);

      expect(mockCalendar.events.patch).toHaveBeenCalledWith({
        calendarId: 'primary',
        eventId: 'dst123',
        requestBody: expect.objectContaining({
          start: {
            dateTime: '2024-03-10T07:00:00-05:00',
            timeZone: 'America/New_York'
          },
          end: {
            dateTime: '2024-03-10T08:00:00-05:00',
            timeZone: 'America/New_York'
          }
        })
      });
    });

    it('should handle very long recurrence series', async () => {
      const longRecurringEvent = {
        data: {
          id: 'long123',
          start: { dateTime: '2024-01-01T10:00:00Z' },
          end: { dateTime: '2024-01-01T11:00:00Z' },
          recurrence: ['RRULE:FREQ=DAILY;COUNT=365'] // Daily for a year
        }
      };
      
      mockCalendar.events.get.mockResolvedValue(longRecurringEvent);
      mockCalendar.events.patch.mockResolvedValue({ data: {} });
      mockCalendar.events.insert.mockResolvedValue({ data: { id: 'new_long456' } });

      const args = {
        calendarId: 'primary',
        eventId: 'long123',
        timeZone: 'UTC',
        modificationScope: 'future',
        futureStartDate: '2024-06-01T10:00:00Z'
      };

      await handler.updateFutureInstances(args);

      expect(mockCalendar.events.patch).toHaveBeenCalledWith({
        calendarId: 'primary',
        eventId: 'long123',
        requestBody: {
          recurrence: ['RRULE:FREQ=DAILY;UNTIL=20240531T100000Z']
        }
      });
    });

    it('should handle events with multiple recurrence rules', async () => {
      const multiRuleEvent = {
        data: {
          id: 'multi123',
          start: { dateTime: '2024-06-01T10:00:00Z' },
          end: { dateTime: '2024-06-01T11:00:00Z' },
          recurrence: [
            'RRULE:FREQ=WEEKLY;BYDAY=MO',
            'EXDATE:20240610T100000Z' // Exception date
          ]
        }
      };
      
      mockCalendar.events.get.mockResolvedValue(multiRuleEvent);
      mockCalendar.events.patch.mockResolvedValue({ data: {} });
      mockCalendar.events.insert.mockResolvedValue({ data: { id: 'new_multi456' } });

      const args = {
        calendarId: 'primary',
        eventId: 'multi123',
        timeZone: 'UTC',
        modificationScope: 'future',
        futureStartDate: '2024-06-15T10:00:00Z'
      };

      await handler.updateFutureInstances(args);

      // Should preserve exception dates in new event
      const insertCall = mockCalendar.events.insert.mock.calls[0][0];
      expect(insertCall.requestBody.recurrence).toContain('EXDATE:20240610T100000Z');
    });

    it('should handle instance ID formatting with milliseconds and various timezones', async () => {
      const testCases = [
        {
          originalStartTime: '2024-06-15T10:00:00.123-07:00',
          expectedInstanceId: 'event123_20240615T170000Z'
        },
        {
          originalStartTime: '2024-12-31T23:59:59.999+14:00',
          expectedInstanceId: 'event123_20241231T095959Z'
        },
        {
          originalStartTime: '2024-06-15T00:00:00.000-12:00',
          expectedInstanceId: 'event123_20240615T120000Z'
        }
      ];

      for (const testCase of testCases) {
        mockCalendar.events.patch.mockClear();
        mockCalendar.events.patch.mockResolvedValue({ data: { id: testCase.expectedInstanceId } });

        const args = {
          calendarId: 'primary',
          eventId: 'event123',
          timeZone: 'UTC',
          originalStartTime: testCase.originalStartTime,
          summary: 'Test'
        };

        await handler.updateSingleInstance(args);

        expect(mockCalendar.events.patch).toHaveBeenCalledWith({
          calendarId: 'primary',
          eventId: testCase.expectedInstanceId,
          requestBody: expect.any(Object)
        });
      }
    });

    it('should handle empty or minimal event data gracefully', async () => {
      const minimalEvent = {
        data: {
          id: 'minimal123',
          start: { dateTime: '2024-06-01T10:00:00Z' },
          end: { dateTime: '2024-06-01T11:00:00Z' },
          recurrence: ['RRULE:FREQ=WEEKLY']
          // No summary, description, attendees, etc.
        }
      };
      
      mockCalendar.events.get.mockResolvedValue(minimalEvent);
      mockCalendar.events.patch.mockResolvedValue({ data: {} });
      mockCalendar.events.insert.mockResolvedValue({ data: { id: 'new_minimal456' } });

      const args = {
        calendarId: 'primary',
        eventId: 'minimal123',
        timeZone: 'UTC',
        modificationScope: 'future',
        futureStartDate: '2024-06-15T10:00:00Z',
        summary: 'Added Summary'
      };

      const result = await handler.updateFutureInstances(args);

      const insertCall = mockCalendar.events.insert.mock.calls[0][0];
      expect(insertCall.requestBody.summary).toBe('Added Summary');
      expect(insertCall.requestBody.id).toBeUndefined();
    });
  });

  describe('Validation and Error Edge Cases', () => {
    it('should handle malformed recurrence rules gracefully', async () => {
      const malformedEvent = {
        data: {
          id: 'malformed123',
          start: { dateTime: '2024-06-01T10:00:00Z' },
          end: { dateTime: '2024-06-01T11:00:00Z' },
          recurrence: ['INVALID_RRULE_FORMAT']
        }
      };
      
      mockCalendar.events.get.mockResolvedValue(malformedEvent);

      const args = {
        calendarId: 'primary',
        eventId: 'malformed123',
        timeZone: 'UTC',
        modificationScope: 'future',
        futureStartDate: '2024-06-15T10:00:00Z'
      };

      // Should still attempt to process, letting Google Calendar API handle validation
      mockCalendar.events.patch.mockResolvedValue({ data: {} });
      mockCalendar.events.insert.mockResolvedValue({ data: { id: 'new123' } });

      await handler.updateFutureInstances(args);

      expect(mockCalendar.events.patch).toHaveBeenCalled();
    });

    it('should handle network timeouts and retries', async () => {
      mockCalendar.events.get.mockRejectedValueOnce(new Error('Network timeout'))
                           .mockResolvedValue({
                             data: {
                               id: 'retry123',
                               recurrence: ['RRULE:FREQ=WEEKLY']
                             }
                           });

      const args = {
        calendarId: 'primary',
        eventId: 'retry123',
        timeZone: 'UTC'
      };

      // First call should fail, but we're testing that the error propagates correctly
      await expect(handler.updateEventWithScope(args))
        .rejects.toThrow('Network timeout');
    });

    it('should validate scope restrictions on single events', async () => {
      const singleEvent = {
        data: {
          id: 'single123',
          summary: 'One-time Meeting'
          // no recurrence
        }
      };
      mockCalendar.events.get.mockResolvedValue(singleEvent);

      const invalidScopes = ['single', 'future'];
      
      for (const scope of invalidScopes) {
        const args = {
          calendarId: 'primary',
          eventId: 'single123',
          timeZone: 'UTC',
          modificationScope: scope,
          originalStartTime: '2024-06-15T10:00:00Z',
          futureStartDate: '2024-06-20T10:00:00Z'
        };

        await expect(handler.updateEventWithScope(args))
          .rejects.toThrow('Scope other than "all" only applies to recurring events');
      }
    });
  });
}); 
```

--------------------------------------------------------------------------------
/src/tools/registry.ts:
--------------------------------------------------------------------------------

```typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";
import { BaseToolHandler } from "../handlers/core/BaseToolHandler.js";
import { ALLOWED_EVENT_FIELDS } from "../utils/field-mask-builder.js";

// Import all handlers
import { ListCalendarsHandler } from "../handlers/core/ListCalendarsHandler.js";
import { ListEventsHandler } from "../handlers/core/ListEventsHandler.js";
import { SearchEventsHandler } from "../handlers/core/SearchEventsHandler.js";
import { GetEventHandler } from "../handlers/core/GetEventHandler.js";
import { ListColorsHandler } from "../handlers/core/ListColorsHandler.js";
import { CreateEventHandler } from "../handlers/core/CreateEventHandler.js";
import { UpdateEventHandler } from "../handlers/core/UpdateEventHandler.js";
import { DeleteEventHandler } from "../handlers/core/DeleteEventHandler.js";
import { FreeBusyEventHandler } from "../handlers/core/FreeBusyEventHandler.js";
import { GetCurrentTimeHandler } from "../handlers/core/GetCurrentTimeHandler.js";

// Define shared schema fields for reuse
// Note: Event datetime fields (start/end) are NOT shared to avoid $ref generation
// Each tool defines its own inline schemas for these fields

const timeMinSchema = z.string()
  .refine((val) => {
    const withTimezone = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|[+-]\d{2}:\d{2})$/.test(val);
    const withoutTimezone = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/.test(val);
    return withTimezone || withoutTimezone;
  }, "Must be ISO 8601 format: '2026-01-01T00:00:00'")
  .describe("Start time boundary. Preferred: '2024-01-01T00:00:00' (uses timeZone parameter or calendar timezone). Also accepts: '2024-01-01T00:00:00Z' or '2024-01-01T00:00:00-08:00'.")
  .optional();

const timeMaxSchema = z.string()
  .refine((val) => {
    const withTimezone = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|[+-]\d{2}:\d{2})$/.test(val);
    const withoutTimezone = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/.test(val);
    return withTimezone || withoutTimezone;
  }, "Must be ISO 8601 format: '2026-01-01T00:00:00'")
  .describe("End time boundary. Preferred: '2024-01-01T23:59:59' (uses timeZone parameter or calendar timezone). Also accepts: '2024-01-01T23:59:59Z' or '2024-01-01T23:59:59-08:00'.")
  .optional();

const timeZoneSchema = z.string().optional().describe(
  "Timezone as IANA Time Zone Database name (e.g., America/Los_Angeles). Takes priority over calendar's default timezone. Only used for timezone-naive datetime strings."
);

const fieldsSchema = z.array(z.enum(ALLOWED_EVENT_FIELDS)).optional().describe(
  "Optional array of additional event fields to retrieve. Available fields are strictly validated. Default fields (id, summary, start, end, status, htmlLink, location, attendees) are always included."
);

const privateExtendedPropertySchema = z
  .array(z.string().regex(/^[^=]+=[^=]+$/, "Must be in key=value format"))
  .optional()
  .describe(
    "Filter by private extended properties (key=value). Matches events that have all specified properties."
  );

const sharedExtendedPropertySchema = z
  .array(z.string().regex(/^[^=]+=[^=]+$/, "Must be in key=value format"))
  .optional()
  .describe(
    "Filter by shared extended properties (key=value). Matches events that have all specified properties."
  );

// Define all tool schemas with TypeScript inference
export const ToolSchemas = {
  'list-calendars': z.object({}),

  'list-events': z.object({
    calendarId: z.union([
      z.string().describe(
        "Calendar identifier(s) to query. Accepts calendar IDs (e.g., 'primary', '[email protected]') OR calendar names (e.g., 'Work', 'Personal'). Single calendar: 'primary'. Multiple calendars: array ['Work', 'Personal'] or JSON string '[\"Work\", \"Personal\"]'"
      ),
      z.array(z.string().min(1))
        .min(1, "At least one calendar ID is required")
        .max(50, "Maximum 50 calendars allowed per request")
        .refine(
          (arr) => new Set(arr).size === arr.length,
          "Duplicate calendar IDs are not allowed"
        )
        .describe("Array of calendar IDs to query events from (max 50, no duplicates)")
    ]),
    timeMin: timeMinSchema,
    timeMax: timeMaxSchema,
    timeZone: timeZoneSchema,
    fields: fieldsSchema,
    privateExtendedProperty: privateExtendedPropertySchema,
    sharedExtendedProperty: sharedExtendedPropertySchema
  }),
  
  'search-events': z.object({
    calendarId: z.string().describe("ID of the calendar (use 'primary' for the main calendar)"),
    query: z.string().describe(
      "Free text search query (searches summary, description, location, attendees, etc.)"
    ),
    timeMin: z.string()
      .refine((val) => {
        const withTimezone = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|[+-]\d{2}:\d{2})$/.test(val);
        const withoutTimezone = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/.test(val);
        return withTimezone || withoutTimezone;
      }, "Must be ISO 8601 format: '2026-01-01T00:00:00'")
      .describe("Start time boundary. Preferred: '2024-01-01T00:00:00' (uses timeZone parameter or calendar timezone). Also accepts: '2024-01-01T00:00:00Z' or '2024-01-01T00:00:00-08:00'."),
    timeMax: z.string()
      .refine((val) => {
        const withTimezone = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|[+-]\d{2}:\d{2})$/.test(val);
        const withoutTimezone = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/.test(val);
        return withTimezone || withoutTimezone;
      }, "Must be ISO 8601 format: '2026-01-01T00:00:00'")
      .describe("End time boundary. Preferred: '2024-01-01T23:59:59' (uses timeZone parameter or calendar timezone). Also accepts: '2024-01-01T23:59:59Z' or '2024-01-01T23:59:59-08:00'."),
    timeZone: z.string().optional().describe(
      "Timezone as IANA Time Zone Database name (e.g., America/Los_Angeles). Takes priority over calendar's default timezone. Only used for timezone-naive datetime strings."
    ),
    fields: z.array(z.enum(ALLOWED_EVENT_FIELDS)).optional().describe(
      "Optional array of additional event fields to retrieve. Available fields are strictly validated. Default fields (id, summary, start, end, status, htmlLink, location, attendees) are always included."
    ),
    privateExtendedProperty: z
      .array(z.string().regex(/^[^=]+=[^=]+$/, "Must be in key=value format"))
      .optional()
      .describe(
        "Filter by private extended properties (key=value). Matches events that have all specified properties."
      ),
    sharedExtendedProperty: z
      .array(z.string().regex(/^[^=]+=[^=]+$/, "Must be in key=value format"))
      .optional()
      .describe(
        "Filter by shared extended properties (key=value). Matches events that have all specified properties."
      )
  }),
  
  'get-event': z.object({
    calendarId: z.string().describe("ID of the calendar (use 'primary' for the main calendar)"),
    eventId: z.string().describe("ID of the event to retrieve"),
    fields: z.array(z.enum(ALLOWED_EVENT_FIELDS)).optional().describe(
      "Optional array of additional event fields to retrieve. Available fields are strictly validated. Default fields (id, summary, start, end, status, htmlLink, location, attendees) are always included."
    )
  }),

  'list-colors': z.object({}),
  
  'create-event': z.object({
    calendarId: z.string().describe("ID of the calendar (use 'primary' for the main calendar)"),
    eventId: z.string().optional().describe("Optional custom event ID (5-1024 characters, base32hex encoding: lowercase letters a-v and digits 0-9 only). If not provided, Google Calendar will generate one."),
    summary: z.string().describe("Title of the event"),
    description: z.string().optional().describe("Description/notes for the event"),
    start: z.string()
      .refine((val) => {
        const dateOnly = /^\d{4}-\d{2}-\d{2}$/.test(val);
        const withTimezone = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|[+-]\d{2}:\d{2})$/.test(val);
        const withoutTimezone = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/.test(val);
        return dateOnly || withTimezone || withoutTimezone;
      }, "Must be ISO 8601 format: '2025-01-01T10:00:00' for timed events or '2025-01-01' for all-day events")
      .describe("Event start time: '2025-01-01T10:00:00' for timed events or '2025-01-01' for all-day events. Also accepts Google Calendar API object format: {date: '2025-01-01'} or {dateTime: '2025-01-01T10:00:00', timeZone: 'America/Los_Angeles'}"),
    end: z.string()
      .refine((val) => {
        const dateOnly = /^\d{4}-\d{2}-\d{2}$/.test(val);
        const withTimezone = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|[+-]\d{2}:\d{2})$/.test(val);
        const withoutTimezone = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/.test(val);
        return dateOnly || withTimezone || withoutTimezone;
      }, "Must be ISO 8601 format: '2025-01-01T11:00:00' for timed events or '2025-01-02' for all-day events")
      .describe("Event end time: '2025-01-01T11:00:00' for timed events or '2025-01-02' for all-day events (exclusive). Also accepts Google Calendar API object format: {date: '2025-01-02'} or {dateTime: '2025-01-01T11:00:00', timeZone: 'America/Los_Angeles'}"),
    timeZone: z.string().optional().describe(
      "Timezone as IANA Time Zone Database name (e.g., America/Los_Angeles). Takes priority over calendar's default timezone. Only used for timezone-naive datetime strings."
    ),
    location: z.string().optional().describe("Location of the event"),
    attendees: z.array(z.object({
      email: z.string().email().describe("Email address of the attendee"),
      displayName: z.string().optional().describe("Display name of the attendee"),
      optional: z.boolean().optional().describe("Whether this is an optional attendee"),
      responseStatus: z.enum(["needsAction", "declined", "tentative", "accepted"]).optional().describe("Attendee's response status"),
      comment: z.string().optional().describe("Attendee's response comment"),
      additionalGuests: z.number().int().min(0).optional().describe("Number of additional guests the attendee is bringing")
    })).optional().describe("List of event attendees with their details"),
    colorId: z.string().optional().describe(
      "Color ID for the event (use list-colors to see available IDs)"
    ),
    reminders: z.object({
      useDefault: z.boolean().describe("Whether to use the default reminders"),
      overrides: z.array(z.object({
        method: z.enum(["email", "popup"]).default("popup").describe("Reminder method"),
        minutes: z.number().describe("Minutes before the event to trigger the reminder")
      }).partial({ method: true })).optional().describe("Custom reminders")
    }).describe("Reminder settings for the event").optional(),
    recurrence: z.array(z.string()).optional().describe(
      "Recurrence rules in RFC5545 format (e.g., [\"RRULE:FREQ=WEEKLY;COUNT=5\"])"
    ),
    transparency: z.enum(["opaque", "transparent"]).optional().describe(
      "Whether the event blocks time on the calendar. 'opaque' means busy, 'transparent' means free."
    ),
    visibility: z.enum(["default", "public", "private", "confidential"]).optional().describe(
      "Visibility of the event. Use 'public' for public events, 'private' for private events visible to attendees."
    ),
    guestsCanInviteOthers: z.boolean().optional().describe(
      "Whether attendees can invite others to the event. Default is true."
    ),
    guestsCanModify: z.boolean().optional().describe(
      "Whether attendees can modify the event. Default is false."
    ),
    guestsCanSeeOtherGuests: z.boolean().optional().describe(
      "Whether attendees can see the list of other attendees. Default is true."
    ),
    anyoneCanAddSelf: z.boolean().optional().describe(
      "Whether anyone can add themselves to the event. Default is false."
    ),
    sendUpdates: z.enum(["all", "externalOnly", "none"]).optional().describe(
      "Whether to send notifications about the event creation. 'all' sends to all guests, 'externalOnly' to non-Google Calendar users only, 'none' sends no notifications."
    ),
    conferenceData: z.object({
      createRequest: z.object({
        requestId: z.string().describe("Client-generated unique ID for this request to ensure idempotency"),
        conferenceSolutionKey: z.object({
          type: z.enum(["hangoutsMeet", "eventHangout", "eventNamedHangout", "addOn"]).describe("Conference solution type")
        }).describe("Conference solution to create")
      }).describe("Request to generate a new conference")
    }).optional().describe(
      "Conference properties for the event. Use createRequest to add a new conference."
    ),
    extendedProperties: z.object({
      private: z.record(z.string()).optional().describe(
        "Properties private to the application. Keys can have max 44 chars, values max 1024 chars."
      ),
      shared: z.record(z.string()).optional().describe(
        "Properties visible to all attendees. Keys can have max 44 chars, values max 1024 chars."
      )
    }).optional().describe(
      "Extended properties for storing application-specific data. Max 300 properties totaling 32KB."
    ),
    attachments: z.array(z.object({
      fileUrl: z.string().describe("URL of the attached file"),
      title: z.string().optional().describe("Title of the attachment"),
      mimeType: z.string().optional().describe("MIME type of the attachment"),
      iconLink: z.string().optional().describe("URL of the icon for the attachment"),
      fileId: z.string().optional().describe("ID of the attached file in Google Drive")
    })).optional().describe(
      "File attachments for the event. Requires calendar to support attachments."
    ),
    source: z.object({
      url: z.string().describe("URL of the source"),
      title: z.string().describe("Title of the source")
    }).optional().describe(
      "Source of the event, such as a web page or email message."
    ),
    calendarsToCheck: z.array(z.string()).optional().describe(
      "List of calendar IDs to check for conflicts (defaults to just the target calendar)"
    ),
    duplicateSimilarityThreshold: z.number().min(0).max(1).optional().describe(
      "Threshold for duplicate detection (0-1, default: 0.7). Events with similarity above this are flagged as potential duplicates"
    ),
    allowDuplicates: z.boolean().optional().describe(
      "If true, allows creation even when exact duplicates are detected (similarity >= 0.95). Default is false which blocks duplicate creation"
    )
  }),
  
  'update-event': z.object({
    calendarId: z.string().describe("ID of the calendar (use 'primary' for the main calendar)"),
    eventId: z.string().describe("ID of the event to update"),
    summary: z.string().optional().describe("Updated title of the event"),
    description: z.string().optional().describe("Updated description/notes"),
    start: z.string()
      .refine((val) => {
        const dateOnly = /^\d{4}-\d{2}-\d{2}$/.test(val);
        const withTimezone = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|[+-]\d{2}:\d{2})$/.test(val);
        const withoutTimezone = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/.test(val);
        return dateOnly || withTimezone || withoutTimezone;
      }, "Must be ISO 8601 format: '2025-01-01T10:00:00' for timed events or '2025-01-01' for all-day events")
      .describe("Updated start time: '2025-01-01T10:00:00' for timed events or '2025-01-01' for all-day events. Also accepts Google Calendar API object format: {date: '2025-01-01'} or {dateTime: '2025-01-01T10:00:00', timeZone: 'America/Los_Angeles'}")
      .optional(),
    end: z.string()
      .refine((val) => {
        const dateOnly = /^\d{4}-\d{2}-\d{2}$/.test(val);
        const withTimezone = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|[+-]\d{2}:\d{2})$/.test(val);
        const withoutTimezone = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/.test(val);
        return dateOnly || withTimezone || withoutTimezone;
      }, "Must be ISO 8601 format: '2025-01-01T11:00:00' for timed events or '2025-01-02' for all-day events")
      .describe("Updated end time: '2025-01-01T11:00:00' for timed events or '2025-01-02' for all-day events (exclusive). Also accepts Google Calendar API object format: {date: '2025-01-02'} or {dateTime: '2025-01-01T11:00:00', timeZone: 'America/Los_Angeles'}")
      .optional(),
    timeZone: z.string().optional().describe("Updated timezone as IANA Time Zone Database name. If not provided, uses the calendar's default timezone."),
    location: z.string().optional().describe("Updated location"),
    attendees: z.array(z.object({
      email: z.string().email().describe("Email address of the attendee")
    })).optional().describe("Updated attendee list"),
    colorId: z.string().optional().describe("Updated color ID"),
    reminders: z.object({
      useDefault: z.boolean().describe("Whether to use the default reminders"),
      overrides: z.array(z.object({
        method: z.enum(["email", "popup"]).default("popup").describe("Reminder method"),
        minutes: z.number().describe("Minutes before the event to trigger the reminder")
      }).partial({ method: true })).optional().describe("Custom reminders")
    }).describe("Reminder settings for the event").optional(),
    recurrence: z.array(z.string()).optional().describe("Updated recurrence rules"),
    sendUpdates: z.enum(["all", "externalOnly", "none"]).default("all").describe(
      "Whether to send update notifications"
    ),
    modificationScope: z.enum(["thisAndFollowing", "all", "thisEventOnly"]).optional().describe(
      "Scope for recurring event modifications"
    ),
    originalStartTime: z.string()
      .refine((val) => {
        const withTimezone = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|[+-]\d{2}:\d{2})$/.test(val);
        const withoutTimezone = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/.test(val);
        return withTimezone || withoutTimezone;
      }, "Must be ISO 8601 format: '2026-01-01T00:00:00'")
      .describe("Original start time in the ISO 8601 format '2024-01-01T10:00:00'")
      .optional(),
    futureStartDate: z.string()
      .refine((val) => {
        const withTimezone = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|[+-]\d{2}:\d{2})$/.test(val);
        const withoutTimezone = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/.test(val);
        return withTimezone || withoutTimezone;
      }, "Must be ISO 8601 format: '2026-01-01T00:00:00'")
      .describe("Start date for future instances in the ISO 8601 format '2024-01-01T10:00:00'")
      .optional(),
    checkConflicts: z.boolean().optional().describe(
      "Whether to check for conflicts when updating (default: true when changing time)"
    ),
    calendarsToCheck: z.array(z.string()).optional().describe(
      "List of calendar IDs to check for conflicts (defaults to just the target calendar)"
    ),
    conferenceData: z.object({
      createRequest: z.object({
        requestId: z.string().describe("Client-generated unique ID for this request to ensure idempotency"),
        conferenceSolutionKey: z.object({
          type: z.enum(["hangoutsMeet", "eventHangout", "eventNamedHangout", "addOn"]).describe("Conference solution type")
        }).describe("Conference solution to create")
      }).describe("Request to generate a new conference for this event")
    }).optional().describe("Conference properties for the event. Used to add or update Google Meet links."),
    transparency: z.enum(["opaque", "transparent"]).optional().describe(
      "Whether the event blocks time on the calendar. 'opaque' means busy, 'transparent' means available"
    ),
    visibility: z.enum(["default", "public", "private", "confidential"]).optional().describe(
      "Visibility of the event"
    ),
    guestsCanInviteOthers: z.boolean().optional().describe(
      "Whether attendees other than the organizer can invite others"
    ),
    guestsCanModify: z.boolean().optional().describe(
      "Whether attendees other than the organizer can modify the event"
    ),
    guestsCanSeeOtherGuests: z.boolean().optional().describe(
      "Whether attendees other than the organizer can see who the event's attendees are"
    ),
    anyoneCanAddSelf: z.boolean().optional().describe(
      "Whether anyone can add themselves to the event"
    ),
    extendedProperties: z.object({
      private: z.record(z.string()).optional().describe("Properties that are private to the creator's app"),
      shared: z.record(z.string()).optional().describe("Properties that are shared between all apps")
    }).partial().optional().describe("Extended properties for the event"),
    attachments: z.array(z.object({
      fileUrl: z.string().url().describe("URL link to the attachment"),
      title: z.string().describe("Title of the attachment"),
      mimeType: z.string().optional().describe("MIME type of the attachment"),
      iconLink: z.string().optional().describe("URL link to the attachment's icon"),
      fileId: z.string().optional().describe("ID of the attached Google Drive file")
    })).optional().describe("File attachments for the event")
  }).refine(
    (data) => {
      // Require originalStartTime when modificationScope is 'thisEventOnly'
      if (data.modificationScope === 'thisEventOnly' && !data.originalStartTime) {
        return false;
      }
      return true;
    },
    {
      message: "originalStartTime is required when modificationScope is 'thisEventOnly'",
      path: ["originalStartTime"]
    }
  ).refine(
    (data) => {
      // Require futureStartDate when modificationScope is 'thisAndFollowing'
      if (data.modificationScope === 'thisAndFollowing' && !data.futureStartDate) {
        return false;
      }
      return true;
    },
    {
      message: "futureStartDate is required when modificationScope is 'thisAndFollowing'",
      path: ["futureStartDate"]
    }
  ).refine(
    (data) => {
      // Ensure futureStartDate is in the future when provided
      if (data.futureStartDate) {
        const futureDate = new Date(data.futureStartDate);
        const now = new Date();
        return futureDate > now;
      }
      return true;
    },
    {
      message: "futureStartDate must be in the future",
      path: ["futureStartDate"]
    }
  ),
  
  'delete-event': z.object({
    calendarId: z.string().describe("ID of the calendar (use 'primary' for the main calendar)"),
    eventId: z.string().describe("ID of the event to delete"),
    sendUpdates: z.enum(["all", "externalOnly", "none"]).default("all").describe(
      "Whether to send cancellation notifications"
    )
  }),
  
  'get-freebusy': z.object({
    calendars: z.array(z.object({
      id: z.string().describe("ID of the calendar (use 'primary' for the main calendar)")
    })).describe(
      "List of calendars and/or groups to query for free/busy information"
    ),
    timeMin: z.string()
      .refine((val) => {
        const withTimezone = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|[+-]\d{2}:\d{2})$/.test(val);
        const withoutTimezone = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/.test(val);
        return withTimezone || withoutTimezone;
      }, "Must be ISO 8601 format: '2026-01-01T00:00:00'")
      .describe("Start time boundary. Preferred: '2024-01-01T00:00:00' (uses timeZone parameter or calendar timezone). Also accepts: '2024-01-01T00:00:00Z' or '2024-01-01T00:00:00-08:00'."),
    timeMax: z.string()
      .refine((val) => {
        const withTimezone = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|[+-]\d{2}:\d{2})$/.test(val);
        const withoutTimezone = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/.test(val);
        return withTimezone || withoutTimezone;
      }, "Must be ISO 8601 format: '2026-01-01T00:00:00'")
      .describe("End time boundary. Preferred: '2024-01-01T23:59:59' (uses timeZone parameter or calendar timezone). Also accepts: '2024-01-01T23:59:59Z' or '2024-01-01T23:59:59-08:00'."),
    timeZone: z.string().optional().describe("Timezone for the query"),
    groupExpansionMax: z.number().int().max(100).optional().describe(
      "Maximum number of calendars to expand per group (max 100)"
    ),
    calendarExpansionMax: z.number().int().max(50).optional().describe(
      "Maximum number of calendars to expand (max 50)"
    )
  }),
  
  'get-current-time': z.object({
    timeZone: z.string().optional().describe(
      "Optional IANA timezone (e.g., 'America/Los_Angeles', 'Europe/London', 'UTC'). If not provided, uses the primary Google Calendar's default timezone."
    )
  })
} as const;

// Generate TypeScript types from schemas
export type ToolInputs = {
  [K in keyof typeof ToolSchemas]: z.infer<typeof ToolSchemas[K]>
};

// Export individual types for convenience
export type ListCalendarsInput = ToolInputs['list-calendars'];
export type ListEventsInput = ToolInputs['list-events'];
export type SearchEventsInput = ToolInputs['search-events'];
export type GetEventInput = ToolInputs['get-event'];
export type ListColorsInput = ToolInputs['list-colors'];
export type CreateEventInput = ToolInputs['create-event'];
export type UpdateEventInput = ToolInputs['update-event'];
export type DeleteEventInput = ToolInputs['delete-event'];
export type GetFreeBusyInput = ToolInputs['get-freebusy'];
export type GetCurrentTimeInput = ToolInputs['get-current-time'];

interface ToolDefinition {
  name: keyof typeof ToolSchemas;
  description: string;
  schema: z.ZodType<any>;
  handler: new () => BaseToolHandler;
  handlerFunction?: (args: any) => Promise<any>;
  customInputSchema?: any; // Custom schema shape for MCP registration (overrides extractSchemaShape)
}


export class ToolRegistry {
  private static extractSchemaShape(schema: z.ZodType<any>): any {
    const schemaAny = schema as any;
    
    // Handle ZodEffects (schemas with .refine())
    if (schemaAny._def && schemaAny._def.typeName === 'ZodEffects') {
      return this.extractSchemaShape(schemaAny._def.schema);
    }
    
    // Handle regular ZodObject
    if ('shape' in schemaAny) {
      return schemaAny.shape;
    }
    
    // Handle other nested structures
    if (schemaAny._def && schemaAny._def.schema) {
      return this.extractSchemaShape(schemaAny._def.schema);
    }
    
    // Fallback to the original approach
    return schemaAny._def?.schema?.shape || schemaAny.shape;
  }

  private static tools: ToolDefinition[] = [
    {
      name: "list-calendars",
      description: "List all available calendars",
      schema: ToolSchemas['list-calendars'],
      handler: ListCalendarsHandler
    },
    {
      name: "list-events",
      description: "List events from one or more calendars. Supports both calendar IDs and calendar names.",
      schema: ToolSchemas['list-events'],
      handler: ListEventsHandler,
      handlerFunction: async (args: ListEventsInput & { calendarId: string | string[] }) => {
        let processedCalendarId: string | string[] = args.calendarId;

        // If it's already an array (native array format), keep as-is (already validated by schema)
        if (Array.isArray(args.calendarId)) {
          processedCalendarId = args.calendarId;
        }
        // Handle JSON string format (double or single-quoted)
        else if (typeof args.calendarId === 'string' && args.calendarId.trim().startsWith('[') && args.calendarId.trim().endsWith(']')) {
          try {
            let jsonString = args.calendarId.trim();

            // Normalize single-quoted JSON-like strings to valid JSON (Python/shell style)
            // Only replace single quotes that are string delimiters (after '[', ',', or before ']', ',')
            // This avoids breaking calendar IDs with apostrophes like "John's Calendar"
            if (jsonString.includes("'")) {
              jsonString = jsonString
                .replace(/\[\s*'/g, '["')           // [' -> ["
                .replace(/'\s*,\s*'/g, '", "')      // ', ' -> ", "
                .replace(/'\s*\]/g, '"]');          // '] -> "]
            }

            const parsed = JSON.parse(jsonString);

            // Validate parsed result
            if (!Array.isArray(parsed)) {
              throw new Error('JSON string must contain an array');
            }
            if (!parsed.every(id => typeof id === 'string' && id.length > 0)) {
              throw new Error('Array must contain only non-empty strings');
            }
            if (parsed.length === 0) {
              throw new Error("At least one calendar ID is required");
            }
            if (parsed.length > 50) {
              throw new Error("Maximum 50 calendars allowed");
            }
            if (new Set(parsed).size !== parsed.length) {
              throw new Error("Duplicate calendar IDs are not allowed");
            }

            processedCalendarId = parsed;
          } catch (error) {
            throw new Error(
              `Invalid JSON format for calendarId: ${error instanceof Error ? error.message : 'Unknown parsing error'}`
            );
          }
        }
        // Otherwise it's a single string calendar ID - keep as-is

        return {
          calendarId: processedCalendarId,
          timeMin: args.timeMin,
          timeMax: args.timeMax,
          timeZone: args.timeZone,
          fields: args.fields,
          privateExtendedProperty: args.privateExtendedProperty,
          sharedExtendedProperty: args.sharedExtendedProperty
        };
      }
    },
    {
      name: "search-events",
      description: "Search for events in a calendar by text query.",
      schema: ToolSchemas['search-events'],
      handler: SearchEventsHandler
    },
    {
      name: "get-event",
      description: "Get details of a specific event by ID.",
      schema: ToolSchemas['get-event'],
      handler: GetEventHandler
    },
    {
      name: "list-colors",
      description: "List available color IDs and their meanings for calendar events",
      schema: ToolSchemas['list-colors'],
      handler: ListColorsHandler
    },
    {
      name: "create-event",
      description: "Create a new calendar event.",
      schema: ToolSchemas['create-event'],
      handler: CreateEventHandler
    },
    {
      name: "update-event",
      description: "Update an existing calendar event with recurring event modification scope support.",
      schema: ToolSchemas['update-event'],
      handler: UpdateEventHandler
    },
    {
      name: "delete-event",
      description: "Delete a calendar event.",
      schema: ToolSchemas['delete-event'],
      handler: DeleteEventHandler
    },
    {
      name: "get-freebusy",
      description: "Query free/busy information for calendars. Note: Time range is limited to a maximum of 3 months between timeMin and timeMax.",
      schema: ToolSchemas['get-freebusy'],
      handler: FreeBusyEventHandler
    },
    {
      name: "get-current-time",
      description: "Get current time in the primary Google Calendar's timezone (or a requested timezone).",
      schema: ToolSchemas['get-current-time'],
      handler: GetCurrentTimeHandler
    }
  ];

  static getToolsWithSchemas() {
    return this.tools.map(tool => {
      const jsonSchema = tool.customInputSchema
        ? zodToJsonSchema(z.object(tool.customInputSchema))
        : zodToJsonSchema(tool.schema);
      return {
        name: tool.name,
        description: tool.description,
        inputSchema: jsonSchema
      };
    });
  }

  /**
   * Normalizes datetime fields from object format to string format
   * Converts { date: "2025-01-01" } or { dateTime: "...", timeZone: "..." } to simple strings
   * This allows accepting both Google Calendar API format and our simplified format
   */
  private static normalizeDateTimeFields(toolName: string, args: any): any {
    // Only normalize for tools that have datetime fields
    const toolsWithDateTime = ['create-event', 'update-event'];
    if (!toolsWithDateTime.includes(toolName)) {
      return args;
    }

    const normalized = { ...args };
    const dateTimeFields = ['start', 'end', 'originalStartTime', 'futureStartDate'];

    for (const field of dateTimeFields) {
      if (normalized[field] && typeof normalized[field] === 'object') {
        const obj = normalized[field];
        // Convert object format to string format
        if (obj.date) {
          normalized[field] = obj.date;
        } else if (obj.dateTime) {
          normalized[field] = obj.dateTime;
        }
      }
    }

    return normalized;
  }

  static async registerAll(
    server: McpServer,
    executeWithHandler: (
      handler: any,
      args: any
    ) => Promise<{ content: Array<{ type: "text"; text: string }> }>
  ) {
    for (const tool of this.tools) {
      // Use the existing registerTool method which handles schema conversion properly
      server.registerTool(
        tool.name,
        {
          description: tool.description,
          inputSchema: tool.customInputSchema || this.extractSchemaShape(tool.schema)
        },
        async (args: any) => {
          // Preprocess: Normalize datetime fields (convert object format to string format)
          // This allows accepting both formats while keeping schemas simple
          const normalizedArgs = this.normalizeDateTimeFields(tool.name, args);

          // Validate input using our Zod schema
          const validatedArgs = tool.schema.parse(normalizedArgs);

          // Apply any custom handler function preprocessing
          const processedArgs = tool.handlerFunction ? await tool.handlerFunction(validatedArgs) : validatedArgs;

          // Create handler instance and execute
          const handler = new tool.handler();
          return executeWithHandler(handler, processedArgs);
        }
      );
    }
  }
}

```

--------------------------------------------------------------------------------
/src/tests/unit/handlers/UpdateEventHandler.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { UpdateEventHandler } from '../../../handlers/core/UpdateEventHandler.js';
import { OAuth2Client } from 'google-auth-library';
import type { UpdateEventInput } from '../../../tools/registry.js';
import type { RecurringEventHelpers } from '../../../handlers/core/RecurringEventHelpers.js';

// Mock the googleapis module
vi.mock('googleapis', () => ({
  google: {
    calendar: vi.fn(() => ({
      events: {
        patch: vi.fn(),
        get: vi.fn()
      },
      calendars: {
        get: vi.fn()
      }
    }))
  },
  calendar_v3: {}
}));

// Import createTimeObject for proper datetime handling in mocks
import { createTimeObject } from '../../../handlers/utils/datetime.js';

// Mock RecurringEventHelpers
vi.mock('../../../handlers/core/RecurringEventHelpers.js', () => ({
  RecurringEventHelpers: vi.fn().mockImplementation((calendar) => ({
    detectEventType: vi.fn().mockResolvedValue('single'),
    getCalendar: vi.fn(() => calendar),
    buildUpdateRequestBody: vi.fn((args, defaultTimeZone) => {
      const body: any = {};
      if (args.summary !== undefined && args.summary !== null) body.summary = args.summary;
      if (args.description !== undefined && args.description !== null) body.description = args.description;
      if (args.location !== undefined && args.location !== null) body.location = args.location;
      const tz = args.timeZone || defaultTimeZone;

      // Use createTimeObject to handle both timed and all-day events
      if (args.start !== undefined && args.start !== null) {
        const timeObj = createTimeObject(args.start, tz);
        // When converting formats, explicitly nullify the opposite field
        if (timeObj.date !== undefined) {
          body.start = { date: timeObj.date, dateTime: null };
        } else {
          body.start = { dateTime: timeObj.dateTime, timeZone: timeObj.timeZone, date: null };
        }
      }
      if (args.end !== undefined && args.end !== null) {
        const timeObj = createTimeObject(args.end, tz);
        // When converting formats, explicitly nullify the opposite field
        if (timeObj.date !== undefined) {
          body.end = { date: timeObj.date, dateTime: null };
        } else {
          body.end = { dateTime: timeObj.dateTime, timeZone: timeObj.timeZone, date: null };
        }
      }

      if (args.attendees !== undefined && args.attendees !== null) body.attendees = args.attendees;
      if (args.colorId !== undefined && args.colorId !== null) body.colorId = args.colorId;
      if (args.reminders !== undefined && args.reminders !== null) body.reminders = args.reminders;
      if (args.conferenceData !== undefined && args.conferenceData !== null) body.conferenceData = args.conferenceData;
      if (args.transparency !== undefined && args.transparency !== null) body.transparency = args.transparency;
      if (args.visibility !== undefined && args.visibility !== null) body.visibility = args.visibility;
      if (args.guestsCanInviteOthers !== undefined) body.guestsCanInviteOthers = args.guestsCanInviteOthers;
      if (args.guestsCanModify !== undefined) body.guestsCanModify = args.guestsCanModify;
      if (args.guestsCanSeeOtherGuests !== undefined) body.guestsCanSeeOtherGuests = args.guestsCanSeeOtherGuests;
      if (args.anyoneCanAddSelf !== undefined) body.anyoneCanAddSelf = args.anyoneCanAddSelf;
      if (args.extendedProperties !== undefined && args.extendedProperties !== null) body.extendedProperties = args.extendedProperties;
      if (args.attachments !== undefined && args.attachments !== null) body.attachments = args.attachments;
      return body;
    })
  })),
  RecurringEventError: class extends Error {
    code: string;
    constructor(message: string, code: string) {
      super(message);
      this.code = code;
    }
  },
  RECURRING_EVENT_ERRORS: {
    NON_RECURRING_SCOPE: 'NON_RECURRING_SCOPE'
  }
}));

describe('UpdateEventHandler', () => {
  let handler: UpdateEventHandler;
  let mockOAuth2Client: OAuth2Client;
  let mockCalendar: any;

  beforeEach(() => {
    handler = new UpdateEventHandler();
    mockOAuth2Client = new OAuth2Client();
    
    // Setup mock calendar
    mockCalendar = {
      events: {
        patch: vi.fn(),
        get: vi.fn(),
        insert: vi.fn()
      },
      calendars: {
        get: vi.fn()
      }
    };
    
    // Mock the getCalendar method
    vi.spyOn(handler as any, 'getCalendar').mockReturnValue(mockCalendar);
    
    // Mock getCalendarTimezone
    vi.spyOn(handler as any, 'getCalendarTimezone').mockResolvedValue('America/Los_Angeles');
  });

  describe('Basic Event Updates', () => {
    it('should update event summary', async () => {
      const mockUpdatedEvent = {
        id: 'event123',
        summary: 'Updated Meeting',
        start: { dateTime: '2025-01-15T10:00:00Z' },
        end: { dateTime: '2025-01-15T11:00:00Z' },
        htmlLink: 'https://calendar.google.com/event?eid=abc123'
      };

      mockCalendar.events.get.mockResolvedValue({ data: { recurrence: null } });
      mockCalendar.events.patch.mockResolvedValue({ data: mockUpdatedEvent });

      const args = {
        calendarId: 'primary',
        eventId: 'event123',
        summary: 'Updated Meeting'
      };

      const result = await handler.runTool(args, mockOAuth2Client);

      expect(mockCalendar.events.patch).toHaveBeenCalledWith({
        calendarId: 'primary',
        eventId: 'event123',
        requestBody: expect.objectContaining({
          summary: 'Updated Meeting'
        })
      });

      expect(result.content[0].type).toBe('text');
      const response = JSON.parse((result.content[0] as any).text);
      expect(response.event).toBeDefined();
      expect(response.event.summary).toBe('Updated Meeting');
    });

    it('should update event description and location', async () => {
      const mockUpdatedEvent = {
        id: 'event123',
        summary: 'Meeting',
        description: 'New description',
        location: 'Conference Room B',
        start: { dateTime: '2025-01-15T10:00:00Z' },
        end: { dateTime: '2025-01-15T11:00:00Z' }
      };

      mockCalendar.events.get.mockResolvedValue({ data: { recurrence: null } });
      mockCalendar.events.patch.mockResolvedValue({ data: mockUpdatedEvent });

      const args = {
        calendarId: 'primary',
        eventId: 'event123',
        description: 'New description',
        location: 'Conference Room B'
      };

      const result = await handler.runTool(args, mockOAuth2Client);

      expect(mockCalendar.events.patch).toHaveBeenCalledWith({
        calendarId: 'primary',
        eventId: 'event123',
        requestBody: expect.objectContaining({
          description: 'New description',
          location: 'Conference Room B'
        })
      });

      const response = JSON.parse((result.content[0] as any).text);
      expect(response.event).toBeDefined();
      expect(response.event.description).toBe('New description');
      expect(response.event.location).toBe('Conference Room B');
    });

    it('should update event times', async () => {
      const mockUpdatedEvent = {
        id: 'event123',
        summary: 'Meeting',
        start: { dateTime: '2025-01-16T14:00:00Z' },
        end: { dateTime: '2025-01-16T15:00:00Z' }
      };

      mockCalendar.events.get.mockResolvedValue({ data: { recurrence: null } });
      mockCalendar.events.patch.mockResolvedValue({ data: mockUpdatedEvent });

      const args = {
        calendarId: 'primary',
        eventId: 'event123',
        start: '2025-01-16T14:00:00',
        end: '2025-01-16T15:00:00',
        timeZone: 'America/Los_Angeles'
      };

      const result = await handler.runTool(args, mockOAuth2Client);

      expect(mockCalendar.events.patch).toHaveBeenCalledWith({
        calendarId: 'primary',
        eventId: 'event123',
        requestBody: expect.objectContaining({
          start: { dateTime: '2025-01-16T14:00:00', timeZone: 'America/Los_Angeles', date: null },
          end: { dateTime: '2025-01-16T15:00:00', timeZone: 'America/Los_Angeles', date: null }
        })
      });

      const response = JSON.parse((result.content[0] as any).text);
      expect(response.event).toBeDefined();
    });

    it('should update attendees', async () => {
      const mockUpdatedEvent = {
        id: 'event123',
        summary: 'Meeting',
        attendees: [
          { email: '[email protected]' },
          { email: '[email protected]' }
        ]
      };

      mockCalendar.events.get.mockResolvedValue({ data: { recurrence: null } });
      mockCalendar.events.patch.mockResolvedValue({ data: mockUpdatedEvent });

      const args = {
        calendarId: 'primary',
        eventId: 'event123',
        attendees: [
          { email: '[email protected]' },
          { email: '[email protected]' }
        ],
        sendUpdates: 'all' as const
      };

      const result = await handler.runTool(args, mockOAuth2Client);

      expect(mockCalendar.events.patch).toHaveBeenCalledWith({
        calendarId: 'primary',
        eventId: 'event123',
        requestBody: expect.objectContaining({
          attendees: [
            { email: '[email protected]' },
            { email: '[email protected]' }
          ]
        })
      });

      const response = JSON.parse((result.content[0] as any).text);
      expect(response.event).toBeDefined();
    });

    it('should update reminders', async () => {
      const mockUpdatedEvent = {
        id: 'event123',
        summary: 'Meeting',
        reminders: {
          useDefault: false,
          overrides: [
            { method: 'email', minutes: 30 },
            { method: 'popup', minutes: 10 }
          ]
        }
      };

      mockCalendar.events.get.mockResolvedValue({ data: { recurrence: null } });
      mockCalendar.events.patch.mockResolvedValue({ data: mockUpdatedEvent });

      const args = {
        calendarId: 'primary',
        eventId: 'event123',
        reminders: {
          useDefault: false,
          overrides: [
            { method: 'email' as const, minutes: 30 },
            { method: 'popup' as const, minutes: 10 }
          ]
        }
      };

      const result = await handler.runTool(args, mockOAuth2Client);

      expect(mockCalendar.events.patch).toHaveBeenCalledWith({
        calendarId: 'primary',
        eventId: 'event123',
        requestBody: expect.objectContaining({
          reminders: {
            useDefault: false,
            overrides: [
              { method: 'email', minutes: 30 },
              { method: 'popup', minutes: 10 }
            ]
          }
        })
      });

      const response = JSON.parse((result.content[0] as any).text);
      expect(response.event).toBeDefined();
    });

    it('should update guest permissions', async () => {
      const mockUpdatedEvent = {
        id: 'event123',
        summary: 'Team Meeting',
        guestsCanInviteOthers: false,
        guestsCanModify: true,
        guestsCanSeeOtherGuests: false
      };

      mockCalendar.events.get.mockResolvedValue({ data: { recurrence: null } });
      mockCalendar.events.patch.mockResolvedValue({ data: mockUpdatedEvent });

      const args = {
        calendarId: 'primary',
        eventId: 'event123',
        guestsCanInviteOthers: false,
        guestsCanModify: true,
        guestsCanSeeOtherGuests: false,
        anyoneCanAddSelf: true
      };

      const result = await handler.runTool(args, mockOAuth2Client);

      expect(mockCalendar.events.patch).toHaveBeenCalledWith({
        calendarId: 'primary',
        eventId: 'event123',
        requestBody: expect.objectContaining({
          guestsCanInviteOthers: false,
          guestsCanModify: true,
          guestsCanSeeOtherGuests: false,
          anyoneCanAddSelf: true
        })
      });

      const response = JSON.parse(result.content[0].text as string);
      expect(response).toHaveProperty('event');
      expect(response.event.id).toBe('event123');
      expect(response.event.guestsCanInviteOthers).toBe(false);
      expect(response.event.guestsCanModify).toBe(true);
      expect(response.event.guestsCanSeeOtherGuests).toBe(false);
    });

    it('should update event with conference data', async () => {
      const mockUpdatedEvent = {
        id: 'event123',
        summary: 'Video Meeting',
        conferenceData: {
          entryPoints: [{ uri: 'https://meet.google.com/abc-defg-hij' }]
        }
      };

      mockCalendar.events.get.mockResolvedValue({ data: { recurrence: null } });
      mockCalendar.events.patch.mockResolvedValue({ data: mockUpdatedEvent });

      const args = {
        calendarId: 'primary',
        eventId: 'event123',
        summary: 'Video Meeting',
        conferenceData: {
          createRequest: {
            requestId: 'unique-request-456',
            conferenceSolutionKey: {
              type: 'hangoutsMeet' as const
            }
          }
        }
      };

      const result = await handler.runTool(args, mockOAuth2Client);

      expect(mockCalendar.events.patch).toHaveBeenCalledWith({
        calendarId: 'primary',
        eventId: 'event123',
        requestBody: expect.objectContaining({
          summary: 'Video Meeting',
          conferenceData: {
            createRequest: {
              requestId: 'unique-request-456',
              conferenceSolutionKey: {
                type: 'hangoutsMeet'
              }
            }
          }
        }),
        conferenceDataVersion: 1
      });

      const response = JSON.parse(result.content[0].text as string);
      expect(response).toHaveProperty('event');
      expect(response.event.id).toBe('event123');
      expect(response.event.summary).toBe('Video Meeting');
      expect(response.event.conferenceData).toBeDefined();
      expect(response.event.conferenceData.entryPoints).toBeDefined();
    });

    it('should update color ID', async () => {
      const mockUpdatedEvent = {
        id: 'event123',
        summary: 'Meeting',
        colorId: '7'
      };

      mockCalendar.events.get.mockResolvedValue({ data: { recurrence: null } });
      mockCalendar.events.patch.mockResolvedValue({ data: mockUpdatedEvent });

      const args = {
        calendarId: 'primary',
        eventId: 'event123',
        colorId: '7'
      };

      const result = await handler.runTool(args, mockOAuth2Client);

      expect(mockCalendar.events.patch).toHaveBeenCalledWith({
        calendarId: 'primary',
        eventId: 'event123',
        requestBody: expect.objectContaining({
          colorId: '7'
        })
      });

      const response = JSON.parse((result.content[0] as any).text);
      expect(response.event).toBeDefined();
    });

    it('should update multiple fields at once', async () => {
      const mockUpdatedEvent = {
        id: 'event123',
        summary: 'Updated Meeting',
        description: 'Updated description',
        location: 'New Location',
        start: { dateTime: '2025-01-16T14:00:00Z' },
        end: { dateTime: '2025-01-16T15:00:00Z' },
        attendees: [{ email: '[email protected]' }],
        colorId: '5',
        reminders: { useDefault: true }
      };

      mockCalendar.events.get.mockResolvedValue({ data: { recurrence: null } });
      mockCalendar.events.patch.mockResolvedValue({ data: mockUpdatedEvent });

      const args = {
        calendarId: 'primary',
        eventId: 'event123',
        summary: 'Updated Meeting',
        description: 'Updated description',
        location: 'New Location',
        start: '2025-01-16T14:00:00',
        end: '2025-01-16T15:00:00',
        attendees: [{ email: '[email protected]' }],
        colorId: '5',
        reminders: { useDefault: true },
        sendUpdates: 'externalOnly' as const
      };

      const result = await handler.runTool(args, mockOAuth2Client);

      expect(mockCalendar.events.patch).toHaveBeenCalledWith({
        calendarId: 'primary',
        eventId: 'event123',
        requestBody: expect.objectContaining({
          summary: 'Updated Meeting',
          description: 'Updated description',
          location: 'New Location',
          colorId: '5'
        })
      });

      const response = JSON.parse((result.content[0] as any).text);
      expect(response.event).toBeDefined();
    });
  });

  describe('Attachments and conference data handling', () => {
    it('should set supportsAttachments when clearing attachments', async () => {
      const mockUpdatedEvent = {
        id: 'event123',
        summary: 'Meeting'
      };

      mockCalendar.events.get.mockResolvedValue({ data: { recurrence: null } });
      mockCalendar.events.patch.mockResolvedValue({ data: mockUpdatedEvent });

      const args = {
        calendarId: 'primary',
        eventId: 'event123',
        attachments: []
      };

      await handler.runTool(args, mockOAuth2Client);

      const patchCall = mockCalendar.events.patch.mock.calls[0][0];
      expect(patchCall.requestBody.attachments).toEqual([]);
      expect(patchCall.supportsAttachments).toBe(true);
    });

    it('should set supportsAttachments when duplicating attachments for future instances', async () => {
      const originalEvent = {
        id: 'recurring123',
        recurrence: ['RRULE:FREQ=WEEKLY'],
        start: { dateTime: '2025-01-01T10:00:00Z' },
        end: { dateTime: '2025-01-01T11:00:00Z' }
      };

      mockCalendar.events.get.mockResolvedValue({ data: originalEvent });
      mockCalendar.events.patch.mockResolvedValue({ data: {} });
      mockCalendar.events.insert.mockResolvedValue({ data: { id: 'newEvent' } });

      const helpersStub = {
        getCalendar: () => mockCalendar,
        buildUpdateRequestBody: vi.fn().mockReturnValue({}),
        cleanEventForDuplication: vi.fn().mockReturnValue({
          attachments: [{ fileId: 'file1', fileUrl: 'https://drive.google.com/file1' }],
          recurrence: originalEvent.recurrence
        }),
        calculateEndTime: vi.fn().mockReturnValue('2025-02-01T11:00:00Z'),
        calculateUntilDate: vi.fn().mockReturnValue('20250131T100000Z'),
        updateRecurrenceWithUntil: vi.fn().mockReturnValue(['RRULE:FREQ=WEEKLY;UNTIL=20250131T100000Z'])
      } as unknown as RecurringEventHelpers;

      const args = {
        calendarId: 'primary',
        eventId: 'recurring123',
        futureStartDate: '2025-02-01T10:00:00-08:00',
        timeZone: 'America/Los_Angeles'
      } as UpdateEventInput;

      await (handler as any).updateFutureInstances(helpersStub, args, 'America/Los_Angeles');

      const insertCall = mockCalendar.events.insert.mock.calls[0][0];
      expect(insertCall.supportsAttachments).toBe(true);
      expect(insertCall.requestBody.attachments).toEqual([
        { fileId: 'file1', fileUrl: 'https://drive.google.com/file1' }
      ]);
    });

    it('should set conferenceDataVersion when duplicating conference data for future instances', async () => {
      const originalEvent = {
        id: 'recurring123',
        recurrence: ['RRULE:FREQ=WEEKLY'],
        start: { dateTime: '2025-01-01T10:00:00Z' },
        end: { dateTime: '2025-01-01T11:00:00Z' },
        conferenceData: {
          entryPoints: [{ entryPointType: 'video', uri: 'https://meet.google.com/abc-defg-hij' }],
          conferenceId: 'abc-defg-hij'
        }
      };

      mockCalendar.events.get.mockResolvedValue({ data: originalEvent });
      mockCalendar.events.patch.mockResolvedValue({ data: {} });
      mockCalendar.events.insert.mockResolvedValue({ data: { id: 'newEvent' } });

      const helpersStub = {
        getCalendar: () => mockCalendar,
        buildUpdateRequestBody: vi.fn().mockReturnValue({}),
        cleanEventForDuplication: vi.fn().mockReturnValue({
          conferenceData: originalEvent.conferenceData,
          recurrence: originalEvent.recurrence
        }),
        calculateEndTime: vi.fn().mockReturnValue('2025-02-01T11:00:00Z'),
        calculateUntilDate: vi.fn().mockReturnValue('20250131T100000Z'),
        updateRecurrenceWithUntil: vi.fn().mockReturnValue(['RRULE:FREQ=WEEKLY;UNTIL=20250131T100000Z'])
      } as unknown as RecurringEventHelpers;

      const args = {
        calendarId: 'primary',
        eventId: 'recurring123',
        futureStartDate: '2025-02-01T10:00:00-08:00',
        timeZone: 'America/Los_Angeles'
      } as UpdateEventInput;

      await (handler as any).updateFutureInstances(helpersStub, args, 'America/Los_Angeles');

      const insertCall = mockCalendar.events.insert.mock.calls[0][0];
      expect(insertCall.conferenceDataVersion).toBe(1);
      expect(insertCall.requestBody.conferenceData).toEqual(originalEvent.conferenceData);
    });
  });

  describe('Send Updates Options', () => {
    it('should send updates to all when specified', async () => {
      const mockUpdatedEvent = {
        id: 'event123',
        summary: 'Updated Meeting'
      };

      mockCalendar.events.get.mockResolvedValue({ data: { recurrence: null } });
      mockCalendar.events.patch.mockResolvedValue({ data: mockUpdatedEvent });

      const args = {
        calendarId: 'primary',
        eventId: 'event123',
        summary: 'Updated Meeting',
        sendUpdates: 'all' as const
      };

      await handler.runTool(args, mockOAuth2Client);

      expect(mockCalendar.events.patch).toHaveBeenCalledWith({
        calendarId: 'primary',
        eventId: 'event123',
        requestBody: expect.objectContaining({
          summary: 'Updated Meeting'
        })
      });
    });

    it('should send updates to external users only when specified', async () => {
      const mockUpdatedEvent = {
        id: 'event123',
        summary: 'Updated Meeting'
      };

      mockCalendar.events.get.mockResolvedValue({ data: { recurrence: null } });
      mockCalendar.events.patch.mockResolvedValue({ data: mockUpdatedEvent });

      const args = {
        calendarId: 'primary',
        eventId: 'event123',
        summary: 'Updated Meeting',
        sendUpdates: 'externalOnly' as const
      };

      await handler.runTool(args, mockOAuth2Client);

      expect(mockCalendar.events.patch).toHaveBeenCalledWith({
        calendarId: 'primary',
        eventId: 'event123',
        requestBody: expect.objectContaining({
          summary: 'Updated Meeting'
        })
      });
    });

    it('should not send updates when none specified', async () => {
      const mockUpdatedEvent = {
        id: 'event123',
        summary: 'Updated Meeting'
      };

      mockCalendar.events.get.mockResolvedValue({ data: { recurrence: null } });
      mockCalendar.events.patch.mockResolvedValue({ data: mockUpdatedEvent });

      const args = {
        calendarId: 'primary',
        eventId: 'event123',
        summary: 'Updated Meeting',
        sendUpdates: 'none' as const
      };

      await handler.runTool(args, mockOAuth2Client);

      expect(mockCalendar.events.patch).toHaveBeenCalledWith({
        calendarId: 'primary',
        eventId: 'event123',
        requestBody: expect.objectContaining({
          summary: 'Updated Meeting'
        })
      });
    });
  });

  describe('Error Handling', () => {
    it('should handle event not found error', async () => {
      const notFoundError = new Error('Not Found');
      (notFoundError as any).code = 404;
      mockCalendar.events.get.mockResolvedValue({ data: { recurrence: null } });
      mockCalendar.events.patch.mockRejectedValue(notFoundError);

      const args = {
        calendarId: 'primary',
        eventId: 'nonexistent',
        summary: 'Updated Meeting'
      };

      // The actual error will be "Not Found" since handleGoogleApiError is not being called
      await expect(handler.runTool(args, mockOAuth2Client)).rejects.toThrow('Not Found');
    });

    it('should handle permission denied error', async () => {
      const permissionError = new Error('Forbidden');
      (permissionError as any).code = 403;
      mockCalendar.events.get.mockResolvedValue({ data: { recurrence: null } });
      mockCalendar.events.patch.mockRejectedValue(permissionError);

      const args = {
        calendarId: 'primary',
        eventId: 'event123',
        summary: 'Updated Meeting'
      };

      // Don't mock handleGoogleApiError - let the actual error pass through
      await expect(handler.runTool(args, mockOAuth2Client)).rejects.toThrow('Forbidden');
    });

    it('should reject modification scope on non-recurring events', async () => {
      // Mock detectEventType to return 'single' for non-recurring event
      mockCalendar.events.get.mockResolvedValue({ data: { recurrence: null } });

      const args = {
        calendarId: 'primary',
        eventId: 'event123',
        summary: 'Updated Meeting',
        modificationScope: 'thisEventOnly' as const
      };

      await expect(handler.runTool(args, mockOAuth2Client)).rejects.toThrow(
        'Scope other than "all" only applies to recurring events'
      );
    });

    it('should handle API errors with response status', async () => {
      const apiError = new Error('Bad Request');
      (apiError as any).response = { status: 400 };
      mockCalendar.events.get.mockResolvedValue({ data: { recurrence: null } });
      mockCalendar.events.patch.mockRejectedValue(apiError);

      const args = {
        calendarId: 'primary',
        eventId: 'event123',
        summary: 'Updated Meeting'
      };

      // Mock handleGoogleApiError
      vi.spyOn(handler as any, 'handleGoogleApiError').mockImplementation(() => {
        throw new Error('Bad Request');
      });

      await expect(handler.runTool(args, mockOAuth2Client)).rejects.toThrow('Bad Request');
    });

    it('should handle missing response data', async () => {
      mockCalendar.events.get.mockResolvedValue({ data: { recurrence: null } });
      mockCalendar.events.patch.mockResolvedValue({ data: null });

      const args = {
        calendarId: 'primary',
        eventId: 'event123',
        summary: 'Updated Meeting'
      };

      await expect(handler.runTool(args, mockOAuth2Client)).rejects.toThrow(
        'Failed to update event'
      );
    });
  });

  describe('Timezone Handling', () => {
    it('should use calendar default timezone when not specified', async () => {
      const mockUpdatedEvent = {
        id: 'event123',
        summary: 'Meeting',
        start: { dateTime: '2025-01-16T14:00:00-08:00' },
        end: { dateTime: '2025-01-16T15:00:00-08:00' }
      };

      mockCalendar.events.get.mockResolvedValue({ data: { recurrence: null } });
      mockCalendar.events.patch.mockResolvedValue({ data: mockUpdatedEvent });

      const args = {
        calendarId: 'primary',
        eventId: 'event123',
        start: '2025-01-16T14:00:00',
        end: '2025-01-16T15:00:00'
        // No timeZone specified
      };

      await handler.runTool(args, mockOAuth2Client);

      // Should use the mocked default timezone 'America/Los_Angeles'
      expect(mockCalendar.events.patch).toHaveBeenCalledWith({
        calendarId: 'primary',
        eventId: 'event123',
        requestBody: expect.objectContaining({
          start: { dateTime: '2025-01-16T14:00:00', timeZone: 'America/Los_Angeles', date: null },
          end: { dateTime: '2025-01-16T15:00:00', timeZone: 'America/Los_Angeles', date: null }
        })
      });
    });

    it('should override calendar timezone when specified', async () => {
      const mockUpdatedEvent = {
        id: 'event123',
        summary: 'Meeting',
        start: { dateTime: '2025-01-16T14:00:00+00:00' },
        end: { dateTime: '2025-01-16T15:00:00+00:00' }
      };

      mockCalendar.events.get.mockResolvedValue({ data: { recurrence: null } });
      mockCalendar.events.patch.mockResolvedValue({ data: mockUpdatedEvent });

      const args = {
        calendarId: 'primary',
        eventId: 'event123',
        start: '2025-01-16T14:00:00',
        end: '2025-01-16T15:00:00',
        timeZone: 'UTC'
      };

      await handler.runTool(args, mockOAuth2Client);

      expect(mockCalendar.events.patch).toHaveBeenCalledWith({
        calendarId: 'primary',
        eventId: 'event123',
        requestBody: expect.objectContaining({
          start: { dateTime: '2025-01-16T14:00:00', timeZone: 'UTC', date: null },
          end: { dateTime: '2025-01-16T15:00:00', timeZone: 'UTC', date: null }
        })
      });
    });
  });

  describe('All-day Event Conversion (Issue #118)', () => {
    it('should convert timed event to all-day event', async () => {
      const existingTimedEvent = {
        id: 'event123',
        summary: 'Timed Meeting',
        start: { dateTime: '2025-10-18T10:00:00-07:00' },
        end: { dateTime: '2025-10-18T11:00:00-07:00' }
      };

      const mockUpdatedAllDayEvent = {
        id: 'event123',
        summary: 'Timed Meeting',
        start: { date: '2025-10-18' },
        end: { date: '2025-10-19' }
      };

      mockCalendar.events.get.mockResolvedValue({ data: existingTimedEvent });
      mockCalendar.events.patch.mockResolvedValue({ data: mockUpdatedAllDayEvent });

      const args = {
        calendarId: 'primary',
        eventId: 'event123',
        start: '2025-10-18',
        end: '2025-10-19'
      };

      const result = await handler.runTool(args, mockOAuth2Client);

      // Verify patch was called with correct all-day format
      expect(mockCalendar.events.patch).toHaveBeenCalledWith({
        calendarId: 'primary',
        eventId: 'event123',
        requestBody: expect.objectContaining({
          start: { date: '2025-10-18', dateTime: null },
          end: { date: '2025-10-19', dateTime: null }
        })
      });

      const response = JSON.parse((result.content[0] as any).text);
      expect(response.event).toBeDefined();
      expect(response.event.start.date).toBe('2025-10-18');
      expect(response.event.end.date).toBe('2025-10-19');
    });

    it('should convert all-day event to timed event', async () => {
      const existingAllDayEvent = {
        id: 'event456',
        summary: 'All Day Event',
        start: { date: '2025-10-18' },
        end: { date: '2025-10-19' }
      };

      const mockUpdatedTimedEvent = {
        id: 'event456',
        summary: 'All Day Event',
        start: { dateTime: '2025-10-18T10:00:00-07:00', timeZone: 'America/Los_Angeles' },
        end: { dateTime: '2025-10-18T11:00:00-07:00', timeZone: 'America/Los_Angeles' }
      };

      mockCalendar.events.get.mockResolvedValue({ data: existingAllDayEvent });
      mockCalendar.events.patch.mockResolvedValue({ data: mockUpdatedTimedEvent });

      const args = {
        calendarId: 'primary',
        eventId: 'event456',
        start: '2025-10-18T10:00:00',
        end: '2025-10-18T11:00:00',
        timeZone: 'America/Los_Angeles'
      };

      const result = await handler.runTool(args, mockOAuth2Client);

      // Verify patch was called with correct timed format
      expect(mockCalendar.events.patch).toHaveBeenCalledWith({
        calendarId: 'primary',
        eventId: 'event456',
        requestBody: expect.objectContaining({
          start: { dateTime: '2025-10-18T10:00:00', timeZone: 'America/Los_Angeles', date: null },
          end: { dateTime: '2025-10-18T11:00:00', timeZone: 'America/Los_Angeles', date: null }
        })
      });

      const response = JSON.parse((result.content[0] as any).text);
      expect(response.event).toBeDefined();
      expect(response.event.start.dateTime).toBeDefined();
      expect(response.event.end.dateTime).toBeDefined();
    });

    it('should keep all-day event as all-day when updating', async () => {
      const existingAllDayEvent = {
        id: 'event789',
        summary: 'All Day Event',
        start: { date: '2025-10-18' },
        end: { date: '2025-10-19' }
      };

      const mockUpdatedAllDayEvent = {
        id: 'event789',
        summary: 'All Day Event',
        start: { date: '2025-10-20' },
        end: { date: '2025-10-21' }
      };

      mockCalendar.events.get.mockResolvedValue({ data: existingAllDayEvent });
      mockCalendar.events.patch.mockResolvedValue({ data: mockUpdatedAllDayEvent });

      const args = {
        calendarId: 'primary',
        eventId: 'event789',
        start: '2025-10-20',
        end: '2025-10-21'
      };

      const result = await handler.runTool(args, mockOAuth2Client);

      // Verify patch was called with all-day format
      expect(mockCalendar.events.patch).toHaveBeenCalledWith({
        calendarId: 'primary',
        eventId: 'event789',
        requestBody: expect.objectContaining({
          start: { date: '2025-10-20', dateTime: null },
          end: { date: '2025-10-21', dateTime: null }
        })
      });

      const response = JSON.parse((result.content[0] as any).text);
      expect(response.event).toBeDefined();
      expect(response.event.start.date).toBe('2025-10-20');
      expect(response.event.end.date).toBe('2025-10-21');
    });

    it('should keep timed event as timed when updating', async () => {
      const existingTimedEvent = {
        id: 'event999',
        summary: 'Timed Meeting',
        start: { dateTime: '2025-10-18T10:00:00-07:00' },
        end: { dateTime: '2025-10-18T11:00:00-07:00' }
      };

      const mockUpdatedTimedEvent = {
        id: 'event999',
        summary: 'Timed Meeting',
        start: { dateTime: '2025-10-18T14:00:00-07:00', timeZone: 'America/Los_Angeles' },
        end: { dateTime: '2025-10-18T15:00:00-07:00', timeZone: 'America/Los_Angeles' }
      };

      mockCalendar.events.get.mockResolvedValue({ data: existingTimedEvent });
      mockCalendar.events.patch.mockResolvedValue({ data: mockUpdatedTimedEvent });

      const args = {
        calendarId: 'primary',
        eventId: 'event999',
        start: '2025-10-18T14:00:00',
        end: '2025-10-18T15:00:00'
      };

      const result = await handler.runTool(args, mockOAuth2Client);

      // Verify patch was called with timed format
      expect(mockCalendar.events.patch).toHaveBeenCalledWith({
        calendarId: 'primary',
        eventId: 'event999',
        requestBody: expect.objectContaining({
          start: { dateTime: '2025-10-18T14:00:00', timeZone: 'America/Los_Angeles', date: null },
          end: { dateTime: '2025-10-18T15:00:00', timeZone: 'America/Los_Angeles', date: null }
        })
      });

      const response = JSON.parse((result.content[0] as any).text);
      expect(response.event).toBeDefined();
      expect(response.event.start.dateTime).toBeDefined();
      expect(response.event.end.dateTime).toBeDefined();
    });
  });
});

```

--------------------------------------------------------------------------------
/src/tests/integration/openai-mcp-integration.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from 'vitest';
import OpenAI from 'openai';
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { spawn, ChildProcess } from 'child_process';
import { TestDataFactory } from './test-data-factory.js';

/**
 * Complete OpenAI GPT + MCP Integration Tests
 * 
 * REQUIREMENTS TO RUN THESE TESTS:
 * 1. Valid Google OAuth credentials file at path specified by GOOGLE_OAUTH_CREDENTIALS env var
 * 2. Authenticated test account: Run `npm run dev auth:test` first
 * 3. OPENAI_API_KEY environment variable set to valid OpenAI API key
 * 4. TEST_CALENDAR_ID, INVITEE_1, INVITEE_2 environment variables set
 * 5. Network access to both Google Calendar API and OpenAI API
 * 
 * These tests implement a full end-to-end integration where:
 * 1. OpenAI GPT receives natural language prompts
 * 2. GPT selects and calls MCP tools
 * 3. Tools are executed against your real MCP server
 * 4. Real Google Calendar operations are performed
 * 5. Results are returned to GPT for response generation
 * 
 * DEBUGGING:
 * - When tests fail, full LLM interaction context is automatically logged
 * - Set DEBUG_LLM_INTERACTIONS=true to log all interactions (not just failures)
 * - Context includes: prompt, model, tools, OpenAI request/response, tool calls, results
 * 
 * WARNING: These tests will create, modify, and delete real calendar events
 * and consume OpenAI API credits.
 */

interface ToolCall {
  name: string;
  arguments: Record<string, any>;
}

interface LLMInteractionContext {
  requestId: string;
  prompt: string;
  model: string;
  availableTools: string[];
  openaiRequest: any;
  openaiResponse: any;
  requestDuration: number;
  toolCalls: ToolCall[];
  executedResults: Array<{ toolCall: ToolCall; result: any; success: boolean }>;
  finalResponse: any;
  timestamp: number;
}

interface OpenAIMCPClient {
  sendMessage(prompt: string): Promise<{
    content: string;
    toolCalls: ToolCall[];
    executedResults: Array<{ toolCall: ToolCall; result: any; success: boolean }>;
    context?: LLMInteractionContext;
  }>;
  getLastInteractionContext(): LLMInteractionContext | null;
  logInteractionContext(context: LLMInteractionContext): void;
}

class RealOpenAIMCPClient implements OpenAIMCPClient {
  private openai: OpenAI;
  private mcpClient: Client;
  private testFactory: TestDataFactory;
  private currentSessionId: string | null = null;
  private lastInteractionContext: LLMInteractionContext | null = null;
  
  constructor(apiKey: string, mcpClient: Client) {
    this.openai = new OpenAI({ apiKey });
    this.mcpClient = mcpClient;
    this.testFactory = new TestDataFactory();
  }
  
  startTestSession(_testName: string): string {
    this.currentSessionId = `session-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
    return this.currentSessionId;
  }
  
  endTestSession(): void {
    if (this.currentSessionId) {
      this.currentSessionId = null;
    }
  }
  
  async sendMessage(prompt: string): Promise<{
    content: string;
    toolCalls: ToolCall[];
    executedResults: Array<{ toolCall: ToolCall; result: any; success: boolean }>;
    context?: LLMInteractionContext;
  }> {
    if (!this.currentSessionId) {
      throw new Error('No active test session. Call startTestSession() first.');
    }

    const requestId = `req-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
    const timestamp = Date.now();

    try {
      // Get available tools from MCP server
      const availableTools = await this.mcpClient.listTools();
      const model = process.env.OPENAI_MODEL ?? 'gpt-5-mini-2025-08-07';
      
      // Convert MCP tools to OpenAI format
      const openaiTools = availableTools.tools.map(tool => ({
        type: 'function' as const,
        function: {
          name: tool.name,
          description: tool.description,
          parameters: this.convertMCPSchemaToOpenAISchema(tool.inputSchema)
        }
      }));

      const messages = [{
        role: 'system' as const,
        content: 'You are a helpful assistant that uses calendar tools. Please default to using the Primary calendar unless otherwise specified. For datetime fields (start, end, timeMin, timeMax), you can provide timezone information in RFC3339 format (e.g., "2024-01-01T10:00:00-08:00" or "2024-01-01T10:00:00Z"). If no timezone is provided (e.g., "2024-01-01T10:00:00"), the user\'s default timezone will be assumed. When possible, prefer including the timezone for clarity.'
      }, {
        role: 'user' as const,
        content: prompt
      }];

      // Prepare request context
      const openaiRequest = {
        model: model,
        max_completion_tokens: 2500,
        tools: openaiTools,
        tool_choice: 'auto' as const,
        messages
      };

      // Send message to OpenAI with tools
      const requestStartTime = Date.now();
      const completion = await this.openai.chat.completions.create(openaiRequest);
      const requestDuration = Date.now() - requestStartTime;

      const message = completion.choices[0]?.message;
      if (!message) {
        throw new Error('No response from OpenAI');
      }

      // Extract text and tool calls
      let textContent = message.content || '';
      const toolCalls: ToolCall[] = [];

      // Debug logging when no tool calls are made
      if (!message.tool_calls || message.tool_calls.length === 0) {
        console.log('\n⚠️  OpenAI Response (No Tool Calls):');
        console.log('Model:', completion.model);
        console.log('Finish Reason:', completion.choices[0]?.finish_reason);
        console.log('Content:', textContent);
        console.log('Available tools:', openaiTools.length);
        console.log('\n');
      }

      if (message.tool_calls) {
        message.tool_calls.forEach((toolCall: OpenAI.Chat.Completions.ChatCompletionMessageToolCall) => {
          if (toolCall.type === 'function') {
            toolCalls.push({
              name: toolCall.function.name,
              arguments: JSON.parse(toolCall.function.arguments)
            });
          }
        });
      }
      
      // Execute tool calls against MCP server
      const executedResults: Array<{ toolCall: ToolCall; result: any; success: boolean }> = [];
      for (const toolCall of toolCalls) {
        try {
          const startTime = this.testFactory.startTimer(`mcp-${toolCall.name}`);
          
          console.log(`🔧 Executing ${toolCall.name} with:`, JSON.stringify(toolCall.arguments, null, 2));
          
          const result = await this.mcpClient.callTool({
            name: toolCall.name,
            arguments: toolCall.arguments
          });
          
          this.testFactory.endTimer(`mcp-${toolCall.name}`, startTime, true);
          
          executedResults.push({
            toolCall,
            result,
            success: true
          });
          
          console.log(`✅ ${toolCall.name} succeeded`);
          
          // Track created events for cleanup
          if (toolCall.name === 'create-event') {
            const eventId = TestDataFactory.extractEventIdFromResponse(result);
            if (eventId) {
              this.testFactory.addCreatedEventId(eventId);
              console.log(`📝 Tracked created event ID: ${eventId}`);
            }
          }
          
        } catch (error) {
          const startTime = this.testFactory.startTimer(`mcp-${toolCall.name}`);
          this.testFactory.endTimer(`mcp-${toolCall.name}`, startTime, false, String(error));
          
          executedResults.push({
            toolCall,
            result: null,
            success: false
          });
          
          console.log(`❌ ${toolCall.name} failed:`, error);
        }
      }
      
      // If we have tool results, send a follow-up to OpenAI for final response
      if (toolCalls.length > 0) {
        const toolMessages = message.tool_calls?.map((toolCall: OpenAI.Chat.Completions.ChatCompletionMessageToolCall, index: number) => {
          const executedResult = executedResults[index];
          return {
            role: 'tool' as const,
            tool_call_id: toolCall.id,
            content: JSON.stringify(executedResult.result)
          };
        }) || [];
        
        const followUpMessages = [
          ...messages,
          message,
          ...toolMessages
        ];
        
        const followUpCompletion = await this.openai.chat.completions.create({
          model: model,
          max_completion_tokens: 2500,
          messages: followUpMessages
        });
        
        const followUpMessage = followUpCompletion.choices[0]?.message;
        if (followUpMessage?.content) {
          textContent = followUpMessage.content;
        }
        
        // Store interaction context for potential debugging
        const interactionContext: LLMInteractionContext = {
          requestId,
          prompt,
          model,
          availableTools: openaiTools.map(t => t.function.name),
          openaiRequest,
          openaiResponse: completion,
          requestDuration,
          toolCalls,
          executedResults,
          finalResponse: followUpCompletion,
          timestamp
        };
        
        this.lastInteractionContext = interactionContext;
        
        // Log immediately if debug flag is set
        if (process.env.DEBUG_LLM_INTERACTIONS === 'true') {
          this.logInteractionContext(interactionContext);
        }
        
        return {
          content: textContent,
          toolCalls,
          executedResults,
          context: interactionContext
        };
      }
      
      // Store interaction context for potential debugging
      const interactionContext: LLMInteractionContext = {
        requestId,
        prompt,
        model,
        availableTools: openaiTools.map(t => t.function.name),
        openaiRequest,
        openaiResponse: completion,
        requestDuration,
        toolCalls,
        executedResults,
        finalResponse: null,
        timestamp
      };
      
      this.lastInteractionContext = interactionContext;
      
      // Log immediately if debug flag is set
      if (process.env.DEBUG_LLM_INTERACTIONS === 'true') {
        this.logInteractionContext(interactionContext);
      }
      
      return {
        content: textContent,
        toolCalls: [],
        executedResults: [],
        context: interactionContext
      };
      
    } catch (error) {
      console.error('❌ OpenAI MCP Client Error:', error);
      throw error;
    }
  }
  
  private convertMCPSchemaToOpenAISchema(mcpSchema: any): any {
    // Convert MCP tool schema to OpenAI function schema format
    if (!mcpSchema) {
      return {
        type: 'object' as const,
        properties: {},
        required: []
      };
    }

    // Note: OpenAI doesn't fully support anyOf/oneOf, so we simplify union types
    const enhancedSchema = {
      type: 'object' as const,
      properties: this.enhancePropertiesForOpenAI(mcpSchema.properties || {}),
      required: mcpSchema.required || []
    };

    return enhancedSchema;
  }
  
  private enhancePropertiesForOpenAI(properties: any): any {
    const enhanced: any = {};

    for (const [key, value] of Object.entries(properties)) {
      const prop = value as any;
      enhanced[key] = { ...prop };

      // Handle anyOf union types (OpenAI doesn't support these well)
      // For calendarId with string|array union, simplify to string with usage note
      if (prop.anyOf && Array.isArray(prop.anyOf)) {
        // Find the string type in the union
        const stringType = prop.anyOf.find((t: any) => t.type === 'string');
        if (stringType) {
          enhanced[key] = {
            type: 'string',
            description: `${stringType.description || prop.description || ''} Note: For multiple values, use JSON array string format: '["id1", "id2"]'`.trim()
          };
        } else {
          // Fallback: use the first type
          enhanced[key] = { ...prop.anyOf[0] };
        }
        // Remove anyOf as OpenAI doesn't support it
        delete enhanced[key].anyOf;
      }

      // Enhance datetime properties for better OpenAI compliance
      if (this.isDateTimeProperty(key, prop)) {
        enhanced[key] = {
          ...enhanced[key],
          type: 'string',
          format: 'date-time',
          pattern: '^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(Z|[+-]\\d{2}:\\d{2})$',
          description: `${enhanced[key].description || prop.description || ''} CRITICAL: MUST be in RFC3339 format with timezone. Examples: "2024-01-01T10:00:00Z" (UTC) or "2024-01-01T10:00:00-08:00" (Pacific). NEVER use "2024-01-01T10:00:00" without timezone.`.trim()
        };
      }

      // Recursively enhance nested objects
      if (enhanced[key].type === 'object' && enhanced[key].properties) {
        enhanced[key].properties = this.enhancePropertiesForOpenAI(enhanced[key].properties);
      }

      // Enhance array items if they contain objects
      if (enhanced[key].type === 'array' && enhanced[key].items && enhanced[key].items.properties) {
        enhanced[key].items = {
          ...enhanced[key].items,
          properties: this.enhancePropertiesForOpenAI(enhanced[key].items.properties)
        };
      }
    }

    return enhanced;
  }
  
  private isDateTimeProperty(key: string, prop: any): boolean {
    // Check if this is a datetime property based on key name or description
    const dateTimeKeys = ['start', 'end', 'timeMin', 'timeMax', 'originalStartTime', 'futureStartDate'];
    const hasDateTimeKey = dateTimeKeys.includes(key);
    const hasDateTimeDescription = prop.description && (
      prop.description.includes('RFC3339') ||
      prop.description.includes('datetime') ||
      prop.description.includes('timezone') ||
      prop.description.includes('time in') ||
      prop.description.includes('time boundary')
    );
    
    return hasDateTimeKey || hasDateTimeDescription;
  }
  
  getPerformanceMetrics() {
    return this.testFactory.getPerformanceMetrics();
  }
  
  getCreatedEventIds(): string[] {
    return this.testFactory.getCreatedEventIds();
  }
  
  clearCreatedEventIds(): void {
    this.testFactory.clearCreatedEventIds();
  }
  
  getLastInteractionContext(): LLMInteractionContext | null {
    return this.lastInteractionContext;
  }
  
  logInteractionContext(context: LLMInteractionContext): void {
    console.log(`\n🔍 [${context.requestId}] LLM INTERACTION CONTEXT:`);
    console.log(`⏰ Timestamp: ${new Date(context.timestamp).toISOString()}`);
    console.log(`📝 Prompt: ${context.prompt}`);
    console.log(`🤖 Model: ${context.model}`);
    console.log(`🔧 Available tools: ${context.availableTools.join(', ')}`);
    console.log(`⚡ Request duration: ${context.requestDuration}ms`);
    
    console.log(`\n📤 OPENAI REQUEST:`);
    console.log(JSON.stringify(context.openaiRequest, null, 2));
    
    console.log(`\n📥 OPENAI RESPONSE:`);
    console.log(JSON.stringify(context.openaiResponse, null, 2));
    
    if (context.toolCalls.length > 0) {
      console.log(`\n🛠️  TOOL CALLS (${context.toolCalls.length}):`);
      context.toolCalls.forEach((call, index) => {
        console.log(`  ${index + 1}. ${call.name}:`);
        console.log(`     Arguments: ${JSON.stringify(call.arguments, null, 4)}`);
      });
      
      console.log(`\n📊 TOOL EXECUTION RESULTS:`);
      context.executedResults.forEach((result, index) => {
        console.log(`  ${index + 1}. ${result.toolCall.name}: ${result.success ? '✅ SUCCESS' : '❌ FAILED'}`);
        if (!result.success) {
          console.log(`     Error: ${JSON.stringify(result.result, null, 4)}`);
        } else {
          console.log(`     Result: ${JSON.stringify(result.result, null, 4)}`);
        }
      });
    }
    
    if (context.finalResponse) {
      console.log(`\n🏁 FINAL RESPONSE:`);
      console.log(JSON.stringify(context.finalResponse, null, 2));
    }
    
    console.log(`\n🔚 [${context.requestId}] END INTERACTION CONTEXT\n`);
  }
}

describe('Complete OpenAI GPT + MCP Integration Tests', () => {
  let openaiMCPClient: RealOpenAIMCPClient;
  let mcpClient: Client;
  let serverProcess: ChildProcess;
  let createdEventIds: string[] = [];
  
  const TEST_CALENDAR_ID = process.env.TEST_CALENDAR_ID;
  const INVITEE_1 = process.env.INVITEE_1;
  const INVITEE_2 = process.env.INVITEE_2;

  beforeAll(async () => {
    console.log('🚀 Starting complete OpenAI GPT + MCP integration tests...');
    
    // Validate required environment variables
    if (!TEST_CALENDAR_ID) {
      throw new Error('TEST_CALENDAR_ID environment variable is required');
    }
    if (!INVITEE_1 || !INVITEE_2) {
      throw new Error('INVITEE_1 and INVITEE_2 environment variables are required for testing event invitations');
    }

    // Start the MCP server
    console.log('🔌 Starting MCP server...');
    
    // Filter out undefined values from process.env and set NODE_ENV=test
    const cleanEnv = Object.fromEntries(
      Object.entries(process.env).filter(([_, value]) => value !== undefined)
    ) as Record<string, string>;
    cleanEnv.NODE_ENV = 'test';
    
    serverProcess = spawn('node', ['build/index.js'], {
      stdio: ['pipe', 'pipe', 'pipe'],
      env: cleanEnv
    });

    // Wait for server to start
    await new Promise(resolve => setTimeout(resolve, 3000));

    // Create MCP client
    mcpClient = new Client({
      name: "openai-mcp-integration-client",
      version: "1.0.0"
    }, {
      capabilities: {
        tools: {}
      }
    });

    // Connect to MCP server
    const transport = new StdioClientTransport({
      command: 'node',
      args: ['build/index.js'],
      env: cleanEnv
    });
    
    await mcpClient.connect(transport);
    console.log('✅ Connected to MCP server');

    // Initialize OpenAI MCP client
    const apiKey = process.env.OPENAI_API_KEY;
    if (!apiKey || apiKey === 'your_api_key_here') {
      throw new Error('OpenAI API key not configured');
    }
    
    openaiMCPClient = new RealOpenAIMCPClient(apiKey, mcpClient);
    
    // Test the integration
    openaiMCPClient.startTestSession('Initial Connection Test');
    try {
      const testResponse = await openaiMCPClient.sendMessage('Hello, can you list my calendars?');
      console.log('✅ OpenAI GPT + MCP integration verified');
      console.log('Sample response:', testResponse.content.substring(0, 100) + '...');
      openaiMCPClient.endTestSession();
    } catch (error) {
      openaiMCPClient.endTestSession();
      throw error;
    }
    
  }, 60000);

  afterAll(async () => {
    // Final cleanup
    await cleanupAllCreatedEvents();
    
    // Close connections
    if (mcpClient) {
      await mcpClient.close();
    }
    
    if (serverProcess && !serverProcess.killed) {
      serverProcess.kill();
      await new Promise(resolve => setTimeout(resolve, 1000));
    }
    
    console.log('🧹 Complete OpenAI GPT + MCP integration test cleanup completed');
  }, 30000);

  beforeEach(() => {
    createdEventIds = [];
  });

  afterEach(async () => {
    // Cleanup events created in this test
    if (openaiMCPClient instanceof RealOpenAIMCPClient) {
      const newEventIds = openaiMCPClient.getCreatedEventIds();
      createdEventIds.push(...newEventIds);
      await cleanupEvents(createdEventIds);
      openaiMCPClient.clearCreatedEventIds();
    }
    createdEventIds = [];
  });

  describe('End-to-End Calendar Workflows', () => {
    it('should complete a full calendar management workflow', async () => {
      console.log('\n🔄 Testing complete calendar workflow...');
      
      openaiMCPClient.startTestSession('Full Calendar Workflow Test');
      
      let step1Context: LLMInteractionContext | null = null;
      
      try {
        // Step 1: Check calendars
        const calendarsResponse = await openaiMCPClient.sendMessage(
          "First, show me all my available calendars"
        );
        
        step1Context = calendarsResponse.context || null;
        
        expect(calendarsResponse.content).toBeDefined();
        expect(calendarsResponse.executedResults.length).toBeGreaterThan(0);
        expect(calendarsResponse.executedResults[0].success).toBe(true);
        
        console.log('✅ Step 1: Retrieved calendars');
      } catch (error) {
        if (step1Context && openaiMCPClient instanceof RealOpenAIMCPClient) {
          console.log('\n❌ STEP 1 FAILED - LOGGING INTERACTION CONTEXT:');
          openaiMCPClient.logInteractionContext(step1Context);
        }
        openaiMCPClient.endTestSession();
        throw error;
      }
      
      let step2Context: LLMInteractionContext | null = null;
      let createToolCall: any = null;
      
      try {
        // Step 2: Create an event (allow for multiple tool calls)
        const createResponse = await openaiMCPClient.sendMessage(
          `Create a test meeting called 'OpenAI GPT MCP Integration Test' for tomorrow at 3 PM for 1 hour in calendar ${TEST_CALENDAR_ID}`
        );
        
        step2Context = createResponse.context || null;
        
        expect(createResponse.content).toBeDefined();
        expect(createResponse.executedResults.length).toBeGreaterThan(0);
        
        // Check if GPT eventually called create-event (may be after get-current-time or other tools)
        createToolCall = createResponse.executedResults.find(r => r.toolCall.name === 'create-event');
        
        if (createToolCall) {
          expect(createToolCall.success).toBe(true);
          console.log('✅ Step 2: Created test event');
        } else {
          // If no create-event, at least verify GPT made progress toward the goal
          const timeToolCall = createResponse.executedResults.find(r => r.toolCall.name === 'get-current-time');
          if (timeToolCall) {
            console.log('✅ Step 2: GPT gathered time information (reasonable first step)');
            
            // Try a follow-up to complete the creation
            const followUpResponse = await openaiMCPClient.sendMessage(
              `Now please create that test meeting called 'OpenAI GPT MCP Integration Test' for tomorrow at 3 PM for 1 hour in calendar ${TEST_CALENDAR_ID}`
            );
            
            const followUpCreateResult = followUpResponse.executedResults.find(r => r.toolCall.name === 'create-event');
            
            if (followUpCreateResult && followUpCreateResult.success) {
              createToolCall = followUpCreateResult;
              console.log('✅ Step 2: Created test event in follow-up');
            } else {
              // GPT understood but didn't complete creation - still valid
              expect(createResponse.content.toLowerCase()).toMatch(/(meeting|event|created|tomorrow|test)/);
              console.log('✅ Step 2: GPT understood request but did not complete creation');
            }
          } else {
            console.log('⚠️ Step 2: GPT responded but did not call expected tools');
            // Still consider this valid - GPT understood the request
            expect(createResponse.content.toLowerCase()).toMatch(/(meeting|event|created|tomorrow|test)/);
          }
        }
      } catch (error) {
        if (step2Context && openaiMCPClient instanceof RealOpenAIMCPClient) {
          console.log('\n❌ STEP 2 FAILED - LOGGING INTERACTION CONTEXT:');
          openaiMCPClient.logInteractionContext(step2Context);
        }
        openaiMCPClient.endTestSession();
        throw error;
      }
      
      // Step 3: Search for the created event (only if one was actually created)
      if (createToolCall && createToolCall.success) {
        const searchResponse = await openaiMCPClient.sendMessage(
          "Find the meeting I just created with 'OpenAI GPT MCP Integration Test' in the title"
        );
        
        expect(searchResponse.content).toBeDefined();
        
        // Allow for multiple ways GPT might search
        const searchToolCall = searchResponse.executedResults.find(r => 
          r.toolCall.name === 'search-events' || r.toolCall.name === 'list-events'
        );
        
        if (searchToolCall) {
          expect(searchToolCall.success).toBe(true);
          console.log('✅ Step 3: Found created event');
        } else {
          // GPT might just respond about the search without calling tools
          console.log('✅ Step 3: GPT provided search response');
        }
      } else {
        console.log('⚠️ Step 3: Skipping search since no event was created');
      }
      
      console.log('🎉 Complete workflow successful!');
      openaiMCPClient.endTestSession();
    }, 120000);

    it('should handle event creation with complex details', async () => {
      openaiMCPClient.startTestSession('Complex Event Creation Test');
      
      await executeWithContextLogging('Complex Event Creation', async () => {
        const response = await openaiMCPClient.sendMessage(
          "Create a team meeting called 'Weekly Standup with GPT' for next Monday at 9 AM, lasting 30 minutes. " +
          `Add attendees ${INVITEE_1} and ${INVITEE_2}. Set it in Pacific timezone and add a reminder 15 minutes before.`
        );
        
        expect(response.content).toBeDefined();
        expect(response.executedResults.length).toBeGreaterThan(0);
        
        const createToolCall = response.executedResults.find(r => r.toolCall.name === 'create-event');
        const timeResult = response.executedResults.find(r => r.toolCall.name === 'get-current-time');
        
        if (createToolCall) {
          expect(createToolCall.success).toBe(true);
          
          // Verify GPT extracted the details correctly (only if the event was actually created)
          if (createToolCall?.toolCall.arguments.summary) {
            expect(createToolCall.toolCall.arguments.summary).toContain('Weekly Standup');
          }
          if (createToolCall?.toolCall.arguments.attendees) {
            expect(createToolCall.toolCall.arguments.attendees.length).toBe(2);
          }
          if (createToolCall?.toolCall.arguments.timeZone) {
            expect(createToolCall.toolCall.arguments.timeZone).toMatch(/Pacific|America\/Los_Angeles/);
          }
          
          console.log('✅ Complex event creation successful');
        } else if (timeResult && timeResult.success) {
          // GPT gathered time info first, try a follow-up with the complex details
          console.log('🔄 GPT gathered time info first, attempting follow-up for complex event...');
          
          const followUpResponse = await openaiMCPClient.sendMessage(
            `Now please create that team meeting with these specific details:
- Title: "Weekly Standup with GPT"
- Date: Next Monday  
- Time: 9:00 AM Pacific timezone
- Duration: 30 minutes
- Attendees: ${INVITEE_1}, ${INVITEE_2}
- Reminder: 15 minutes before
- Calendar: primary

Please use the create-event tool to create this event.`
          );
          
          const followUpCreateResult = followUpResponse.executedResults.find(r => r.toolCall.name === 'create-event');
          
          if (followUpCreateResult && followUpCreateResult.success) {
            // Verify the details in follow-up creation
            if (followUpCreateResult?.toolCall.arguments.summary) {
              expect(followUpCreateResult.toolCall.arguments.summary).toContain('Weekly Standup');
            }
            if (followUpCreateResult?.toolCall.arguments.attendees) {
              expect(followUpCreateResult.toolCall.arguments.attendees.length).toBe(2);
            }
            if (followUpCreateResult?.toolCall.arguments.timeZone) {
              expect(followUpCreateResult.toolCall.arguments.timeZone).toMatch(/Pacific|America\/Los_Angeles/);
            }
            
            console.log('✅ Complex event creation successful in follow-up');
          } else {
            // GPT understood but didn't complete creation - still valid
            expect(response.content.toLowerCase()).toMatch(/(meeting|standup|monday|team)/);
            console.log('✅ Complex event creation: GPT understood request');
          }
        } else {
          // GPT understood but didn't call expected tools - still valid if response shows understanding
          expect(response.content.toLowerCase()).toMatch(/(meeting|standup|monday|team)/);
          console.log('✅ Complex event creation: GPT provided reasonable response');
        }
        
        openaiMCPClient.endTestSession();
      });
    }, 120000); // Increased timeout for potential multi-step interaction

    it('should handle availability checking and smart scheduling', async () => {
      openaiMCPClient.startTestSession('Availability Checking Test');

      try {
        // Calculate next Thursday
        const now = new Date();
        const daysUntilThursday = (4 - now.getDay() + 7) % 7 || 7; // 4 = Thursday
        const nextThursday = new Date(now);
        nextThursday.setDate(now.getDate() + daysUntilThursday);
        const thursdayDate = nextThursday.toISOString().split('T')[0];

        const response = await openaiMCPClient.sendMessage(
          `Check my primary calendar availability on ${thursdayDate} between 2:00 PM and 6:00 PM, ` +
          `and suggest a good 2-hour time slot for a workshop.`
        );

        console.log('\n📊 Test Debug Info:');
        console.log('Response content:', response.content);
        console.log('Tool calls made:', response.toolCalls.map(tc => tc.name).join(', ') || 'NONE');
        console.log('Executed results count:', response.executedResults.length);
        console.log('\n');

        expect(response.content).toBeDefined();
        expect(response.executedResults.length).toBeGreaterThan(0);

        // Should check free/busy or list events
        const availabilityCheck = response.executedResults.find(r =>
          r.toolCall.name === 'get-freebusy' || r.toolCall.name === 'list-events' || r.toolCall.name === 'get-current-time'
        );
        expect(availabilityCheck).toBeDefined();
        expect(availabilityCheck?.success).toBe(true);

        console.log('✅ Availability checking successful');
        openaiMCPClient.endTestSession();

      } catch (error) {
        console.error('❌ Availability checking test failed:', error);
        openaiMCPClient.endTestSession();
        throw error;
      }
    }, 60000);

    it('should handle event modification requests', async () => {
      openaiMCPClient.startTestSession('Event Modification Test');
      
      await executeWithContextLogging('Event Modification', async () => {
        let eventId: string | null = null;
        
        // First create an event - use a specific date/time to avoid timezone issues
        const tomorrow = new Date();
        tomorrow.setDate(tomorrow.getDate() + 1);
        const tomorrowISO = tomorrow.toISOString().split('T')[0]; // Get YYYY-MM-DD format
        
        const createResponse = await openaiMCPClient.sendMessage(
          `Please use the create-event tool to create a calendar event with these exact parameters:
- calendarId: "primary"
- summary: "Test Event for Modification"
- start: "${tomorrowISO}T14:00:00-08:00"
- end: "${tomorrowISO}T15:00:00-08:00"
- timeZone: "America/Los_Angeles"

Call the create-event tool now with these exact values.`
        );
        
        expect(createResponse.content).toBeDefined();
        expect(createResponse.executedResults.length).toBeGreaterThan(0);
        
        // Look for create-event call in the response
        const createResult = createResponse.executedResults.find(r => r.toolCall.name === 'create-event');
        const timeResult = createResponse.executedResults.find(r => r.toolCall.name === 'get-current-time');
        
        if (createResult) {
          // GPT attempted creation but it may have failed
          if (!createResult.success) {
            console.log('❌ Event creation failed, skipping modification test');
            console.log('Error:', JSON.stringify(createResult.result, null, 2));
            return;
          }
          
          eventId = TestDataFactory.extractEventIdFromResponse(createResult.result);
          if (!eventId) {
            console.log('❌ Could not extract event ID from creation result, skipping modification test');
            return;
          }
          console.log('✅ Event created in single interaction');
        } else if (timeResult && timeResult.success) {
          // GPT gathered time info first, try a more explicit follow-up to complete creation
          console.log('🔄 GPT gathered time info first, attempting follow-up to complete creation...');
          
          const followUpResponse = await openaiMCPClient.sendMessage(
            `Based on the current time you just retrieved, please create a calendar event with these details:
- Title: "Test Event for Modification"  
- Date: Tomorrow
- Time: 2:00 PM
- Duration: 1 hour
- Calendar: primary

Please use the create-event tool to actually create this event now.`
          );
          
          const followUpCreateResult = followUpResponse.executedResults.find(r => r.toolCall.name === 'create-event');
          
          if (!followUpCreateResult) {
            console.log('GPT did not complete event creation in follow-up, trying one more approach...');
            
            // Try a third approach with even more explicit instructions
            const finalAttemptResponse = await openaiMCPClient.sendMessage(
              "Please call the create-event tool now to create a meeting titled 'Test Event for Modification' for tomorrow at 2 PM."
            );
            
            const finalCreateResult = finalAttemptResponse.executedResults.find(r => r.toolCall.name === 'create-event');
            
            if (!finalCreateResult) {
              console.log('GPT did not create event after multiple attempts, skipping modification test');
              return;
            }
            
            if (!finalCreateResult.success) {
              console.log('❌ Event creation failed in final attempt, skipping modification test');
              console.log('Error:', JSON.stringify(finalCreateResult.result, null, 2));
              return;
            }
            
            eventId = TestDataFactory.extractEventIdFromResponse(finalCreateResult.result);
            if (!eventId) {
              console.log('❌ Could not extract event ID from final creation result, skipping modification test');
              return;
            }
            console.log('✅ Event created in final attempt');
          } else {
            if (!followUpCreateResult.success) {
              console.log('❌ Event creation failed in follow-up, skipping modification test');
              console.log('Error:', JSON.stringify(followUpCreateResult.result, null, 2));
              return;
            }
            
            eventId = TestDataFactory.extractEventIdFromResponse(followUpCreateResult.result);
            if (!eventId) {
              console.log('❌ Could not extract event ID from follow-up creation result, skipping modification test');
              return;
            }
            console.log('✅ Event created in follow-up interaction');
          }
        } else {
          console.log('GPT did not call create-event or get-current-time, skipping modification test');
          return;
        }
        
        expect(eventId).toBeTruthy();
        
        // Now try to modify it - provide all the details GPT needs
        const modifyResponse = await openaiMCPClient.sendMessage(
          `Please use the update-event tool to modify the event with these parameters:
- calendarId: "primary"
- eventId: "${eventId}"
- summary: "Modified Test Event"
- start: "${tomorrowISO}T16:00:00-08:00"
- end: "${tomorrowISO}T17:00:00-08:00"
- timeZone: "America/Los_Angeles"

Call the update-event tool now with these exact values to update the event.`
        );
        
        expect(modifyResponse.content).toBeDefined();
        
        // Check if GPT called the update-event tool
        const updateResult = modifyResponse.executedResults.find(r => r.toolCall.name === 'update-event');
        
        if (updateResult) {
          expect(updateResult.success).toBe(true);
          console.log('✅ Event modification successful');
        } else if (modifyResponse.executedResults.length === 0) {
          // GPT responded with text - try a more direct follow-up
          console.log('🔄 GPT responded with guidance, trying more direct approach...');
          
          // Debug: Check what tools GPT sees
          if (modifyResponse.context) {
            console.log('🔧 Available tools:', modifyResponse.context.availableTools.join(', '));
          }
          
          const directUpdateResponse = await openaiMCPClient.sendMessage(
            `Please call the update-event function right now. Do not ask for more information. Use these exact parameters:
calendarId: "primary"
eventId: "${eventId}"  
summary: "Modified Test Event"
start: "${tomorrowISO}T16:00:00-08:00"
end: "${tomorrowISO}T17:00:00-08:00"
timeZone: "America/Los_Angeles"

Execute the update-event tool call immediately.`
          );
          
          const directUpdateResult = directUpdateResponse.executedResults.find(r => r.toolCall.name === 'update-event');
          
          if (directUpdateResult) {
            expect(directUpdateResult.success).toBe(true);
            console.log('✅ Event modification successful in follow-up');
          } else {
            // GPT understood but didn't use tools - still valid
            expect(modifyResponse.content.toLowerCase()).toMatch(/(update|modify|change|move|title|modified|event|calendar)/);
            console.log('✅ Event modification: GPT understood request but provided guidance instead of using tools');
          }
        } else {
          // GPT made other tool calls but not update-event
          expect(modifyResponse.content.toLowerCase()).toMatch(/(update|modify|change|move|title|modified)/);
          console.log('✅ Event modification: GPT understood request but did not call update-event tool');
        }
        
        openaiMCPClient.endTestSession();
      });
    }, 180000); // Increased timeout for multi-step interactions (up to 3 LLM calls)
  });

  describe('Natural Language Understanding with Real Execution', () => {
    it('should understand and execute various time expressions', async () => {
      openaiMCPClient.startTestSession('Time Expression Understanding Test');
      
      try {
        const timeExpressions = [
          "tomorrow at 10 AM",
          "next Friday at 2 PM",
          "in 3 days at noon"
        ];
        
        for (const timeExpr of timeExpressions) {
          await executeWithContextLogging(`Time Expression: ${timeExpr}`, async () => {
            const response = await openaiMCPClient.sendMessage(
              `Create a test meeting for ${timeExpr} called 'Time Expression Test - ${timeExpr}'`
            );
            
            expect(response.content).toBeDefined();
            expect(response.executedResults.length).toBeGreaterThan(0);
            
            // Look for create-event, but also accept get-current-time as a reasonable first step
            const createResult = response.executedResults.find(r => r.toolCall.name === 'create-event');
            const timeResult = response.executedResults.find(r => r.toolCall.name === 'get-current-time');
            
            if (createResult) {
              expect(createResult.success).toBe(true);
              
              // Verify GPT parsed the time correctly (if it provided these fields)
              if (createResult?.toolCall.arguments.start) {
                expect(createResult.toolCall.arguments.start).toBeDefined();
              }
              if (createResult?.toolCall.arguments.end) {
                expect(createResult.toolCall.arguments.end).toBeDefined();
              }
              
              console.log(`✅ Time expression "${timeExpr}" created successfully`);
            } else if (timeResult && timeResult.success) {
              // GPT gathered time info first, try a follow-up to complete creation
              console.log(`🔄 Time expression "${timeExpr}" - GPT gathered timing info first, attempting follow-up...`);
              
              const followUpResponse = await openaiMCPClient.sendMessage(
                `Now please create that test meeting for ${timeExpr} called 'Time Expression Test - ${timeExpr}'`
              );
              
              const followUpCreateResult = followUpResponse.executedResults.find(r => r.toolCall.name === 'create-event');
              
              if (followUpCreateResult) {
                expect(followUpCreateResult.success).toBe(true);
                console.log(`✅ Time expression "${timeExpr}" created successfully in follow-up`);
              } else {
                // GPT understood but didn't call expected tools - still valid if response is reasonable
                expect(followUpResponse.content.toLowerCase()).toMatch(/(meeting|event|time|tomorrow|friday|days)/);
                console.log(`✅ Time expression "${timeExpr}" - GPT provided reasonable response in follow-up`);
              }
            } else {
              // GPT understood but didn't call expected tools - still valid if response is reasonable
              expect(response.content.toLowerCase()).toMatch(/(meeting|event|time|tomorrow|friday|days)/);
              console.log(`✅ Time expression "${timeExpr}" - GPT provided reasonable response`);
            }
          });
        }
        
        openaiMCPClient.endTestSession();
        
      } catch (error) {
        console.error('❌ Time expression test failed:', error);
        openaiMCPClient.endTestSession();
        throw error;
      }
    }, 180000);

    it('should handle complex multi-step requests', async () => {
      openaiMCPClient.startTestSession('Multi-Step Request Test');
      
      try {
        // Calculate next Tuesday
        const now = new Date();
        const daysUntilTuesday = (2 - now.getDay() + 7) % 7 || 7; // 2 = Tuesday
        const nextTuesday = new Date(now);
        nextTuesday.setDate(now.getDate() + daysUntilTuesday);
        const tuesdayDate = nextTuesday.toISOString().split('T')[0];

        const response = await openaiMCPClient.sendMessage(
          `Check my primary calendar for ${tuesdayDate}, then find the first available time slot after 2:00 PM ` +
          `and create a 1-hour meeting called "Team Sync" at that time.`
        );

        console.log('\n📊 Test Debug Info:');
        console.log('Response content:', response.content);
        console.log('Tool calls made:', response.toolCalls.map(tc => tc.name).join(', ') || 'NONE');
        console.log('Executed results count:', response.executedResults.length);
        console.log('\n');

        expect(response.content).toBeDefined();
        expect(response.executedResults.length).toBeGreaterThan(0);

        // Should have at least one tool call - GPT may be conservative and only check calendar/time first
        // This tests that GPT can understand and start executing complex multi-step requests
        const listEventsCall = response.executedResults.find(r => r.toolCall.name === 'list-events');
        const createEventCall = response.executedResults.find(r => r.toolCall.name === 'create-event');
        const searchEventsCall = response.executedResults.find(r => r.toolCall.name === 'search-events');
        const listCalendarsCall = response.executedResults.find(r => r.toolCall.name === 'list-calendars');
        const getCurrentTimeCall = response.executedResults.find(r => r.toolCall.name === 'get-current-time');

        // Accept any calendar-related tool call as a valid first step
        expect(listEventsCall || createEventCall || searchEventsCall || listCalendarsCall || getCurrentTimeCall).toBeDefined();

        console.log('✅ Multi-step request executed successfully');
        openaiMCPClient.endTestSession();

      } catch (error) {
        console.error('❌ Multi-step request test failed:', error);
        openaiMCPClient.endTestSession();
        throw error;
      }
    }, 120000);
  });

  describe('Error Handling and Edge Cases', () => {
    it('should gracefully handle invalid requests', async () => {
      openaiMCPClient.startTestSession('Invalid Request Handling Test');
      
      try {
        const response = await openaiMCPClient.sendMessage(
          "Create a meeting for yesterday at 25 o'clock with invalid timezone"
        );
        
        expect(response.content).toBeDefined();
        // GPT should either refuse the request or handle it gracefully
        expect(response.content.toLowerCase()).toMatch(/(cannot|invalid|past|error|sorry|issue|valid)/);
        
        console.log('✅ Invalid request handled gracefully');
        openaiMCPClient.endTestSession();
        
      } catch (error) {
        console.error('❌ Invalid request handling test failed:', error);
        openaiMCPClient.endTestSession();
        throw error;
      }
    }, 30000);

    it('should handle calendar access issues', async () => {
      openaiMCPClient.startTestSession('Calendar Access Error Test');
      
      try {
        const response = await openaiMCPClient.sendMessage(
          "Create an event in calendar 'nonexistent_calendar_id_12345'"
        );
        
        expect(response.content).toBeDefined();
        
        if (response.executedResults.length > 0) {
          const createResult = response.executedResults.find(r => r.toolCall.name === 'create-event');
          if (createResult) {
            // If GPT tried to create the event, it should have failed
            expect(createResult.success).toBe(false);
          }
        }
        
        console.log('✅ Calendar access issue handled gracefully');
        openaiMCPClient.endTestSession();
        
      } catch (error) {
        console.error('❌ Calendar access error test failed:', error);
        openaiMCPClient.endTestSession();
        throw error;
      }
    }, 30000);
  });

  describe('Performance and Reliability', () => {
    it('should complete operations within reasonable time', async () => {
      openaiMCPClient.startTestSession('Performance Test');
      
      try {
        const startTime = Date.now();
        
        const response = await openaiMCPClient.sendMessage(
          "Quickly create a performance test meeting for tomorrow at 1 PM"
        );
        
        const totalTime = Date.now() - startTime;
        
        expect(response.content).toBeDefined();
        expect(totalTime).toBeLessThan(30000); // Should complete within 30 seconds
        
        if (openaiMCPClient instanceof RealOpenAIMCPClient) {
          const metrics = openaiMCPClient.getPerformanceMetrics();
          console.log('📊 Performance metrics:');
          metrics.forEach(metric => {
            console.log(`  ${metric.operation}: ${metric.duration}ms`);
          });
        }
        
        console.log(`✅ Operation completed in ${totalTime}ms`);
        openaiMCPClient.endTestSession();
        
      } catch (error) {
        console.error('❌ Performance test failed:', error);
        openaiMCPClient.endTestSession();
        throw error;
      }
    }, 60000);
  });

  // Helper Functions
  async function executeWithContextLogging<T>(
    testName: string,
    operation: () => Promise<T>
  ): Promise<T> {
    try {
      return await operation();
    } catch (error) {
      const lastContext = openaiMCPClient instanceof RealOpenAIMCPClient 
        ? openaiMCPClient.getLastInteractionContext() 
        : null;
      
      if (lastContext) {
        console.log(`\n❌ ${testName} FAILED - LOGGING LLM INTERACTION CONTEXT:`);
        (openaiMCPClient as RealOpenAIMCPClient).logInteractionContext(lastContext);
      }
      throw error;
    }
  }

  async function cleanupEvents(eventIds: string[]): Promise<void> {
    if (!openaiMCPClient || !(openaiMCPClient instanceof RealOpenAIMCPClient)) {
      return;
    }
    
    for (const eventId of eventIds) {
      try {
        await mcpClient.callTool({
          name: 'delete-event',
          arguments: {
            calendarId: TEST_CALENDAR_ID,
            eventId,
            sendUpdates: 'none'
          }
        });
        console.log(`🗑️ Cleaned up event: ${eventId}`);
      } catch (error) {
        console.warn(`Failed to cleanup event ${eventId}:`, String(error));
      }
    }
  }

  async function cleanupAllCreatedEvents(): Promise<void> {
    if (openaiMCPClient instanceof RealOpenAIMCPClient) {
      const allEventIds = openaiMCPClient.getCreatedEventIds();
      await cleanupEvents(allEventIds);
      openaiMCPClient.clearCreatedEventIds();
    }
  }
}); 
```
Page 4/5FirstPrevNextLast