#
tokens: 46105/50000 12/104 files (page 3/5)
lines: off (toggle) GitHub
raw markdown copy
This is page 3 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/BatchRequestHandler.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * @jest-environment node
 */
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { OAuth2Client } from 'google-auth-library';
import { BatchRequestHandler, BatchRequest, BatchResponse } from '../../../handlers/core/BatchRequestHandler.js';

describe('BatchRequestHandler', () => {
  let mockOAuth2Client: OAuth2Client;
  let batchHandler: BatchRequestHandler;

  beforeEach(() => {
    vi.clearAllMocks();
    mockOAuth2Client = {
      getAccessToken: vi.fn().mockResolvedValue({ token: 'mock_access_token' })
    } as any;
    batchHandler = new BatchRequestHandler(mockOAuth2Client);
  });

  describe('Batch Request Creation', () => {
    it('should create proper multipart request body with single request', () => {
      const requests: BatchRequest[] = [
        {
          method: 'GET',
          path: '/calendar/v3/calendars/primary/events?singleEvents=true&orderBy=startTime'
        }
      ];

      const result = (batchHandler as any).createBatchBody(requests);
      const boundary = (batchHandler as any).boundary;

      expect(result).toContain(`--${boundary}`);
      expect(result).toContain('Content-Type: application/http');
      expect(result).toContain('Content-ID: <item1>');
      expect(result).toContain('GET /calendar/v3/calendars/primary/events');
      expect(result).toContain('singleEvents=true');
      expect(result).toContain('orderBy=startTime');
      expect(result).toContain(`--${boundary}--`);
    });

    it('should create proper multipart request body with multiple requests', () => {
      const requests: BatchRequest[] = [
        {
          method: 'GET',
          path: '/calendar/v3/calendars/primary/events'
        },
        {
          method: 'GET',
          path: '/calendar/v3/calendars/work%40example.com/events'
        },
        {
          method: 'GET',
          path: '/calendar/v3/calendars/personal%40example.com/events'
        }
      ];

      const result = (batchHandler as any).createBatchBody(requests);
      const boundary = (batchHandler as any).boundary;

      expect(result).toContain('Content-ID: <item1>');
      expect(result).toContain('Content-ID: <item2>');
      expect(result).toContain('Content-ID: <item3>');
      expect(result).toContain('calendars/primary/events');
      expect(result).toContain('calendars/work%40example.com/events');
      expect(result).toContain('calendars/personal%40example.com/events');
      
      // Should have proper boundary structure
      const boundaryCount = (result.match(new RegExp(`--${boundary}`, 'g')) || []).length;
      expect(boundaryCount).toBe(4); // 3 request boundaries + 1 end boundary
    });

    it('should handle requests with custom headers', () => {
      const requests: BatchRequest[] = [
        {
          method: 'POST',
          path: '/calendar/v3/calendars/primary/events',
          headers: {
            'If-Match': '"etag123"',
            'X-Custom-Header': 'custom-value'
          }
        }
      ];

      const result = (batchHandler as any).createBatchBody(requests);

      expect(result).toContain('If-Match: "etag123"');
      expect(result).toContain('X-Custom-Header: custom-value');
    });

    it('should handle requests with JSON body', () => {
      const requestBody = {
        summary: 'Test Event',
        start: { dateTime: '2024-01-15T10:00:00Z' },
        end: { dateTime: '2024-01-15T11:00:00Z' }
      };

      const requests: BatchRequest[] = [
        {
          method: 'POST',
          path: '/calendar/v3/calendars/primary/events',
          body: requestBody
        }
      ];

      const result = (batchHandler as any).createBatchBody(requests);

      expect(result).toContain('Content-Type: application/json');
      expect(result).toContain(JSON.stringify(requestBody));
      expect(result).toContain('"summary":"Test Event"');
    });

    it('should encode URLs properly in batch requests', () => {
      const requests: BatchRequest[] = [
        {
          method: 'GET',
          path: '/calendar/v3/calendars/test%40example.com/events?timeMin=2024-01-01T00%3A00%3A00Z'
        }
      ];

      const result = (batchHandler as any).createBatchBody(requests);

      expect(result).toContain('calendars/test%40example.com/events');
      expect(result).toContain('timeMin=2024-01-01T00%3A00%3A00Z');
    });
  });

  describe('Batch Response Parsing', () => {
    it('should parse successful response correctly', () => {
      const mockResponseText = `HTTP/1.1 200 OK
Content-Length: response_total_content_length
Content-Type: multipart/mixed; boundary=batch_abc123

--batch_abc123
Content-Type: application/http
Content-ID: <response-item1>

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 123

{
  "items": [
    {
      "id": "event1",
      "summary": "Test Event",
      "start": {"dateTime": "2024-01-15T10:00:00Z"},
      "end": {"dateTime": "2024-01-15T11:00:00Z"}
    }
  ]
}

--batch_abc123--`;

      const responses = (batchHandler as any).parseBatchResponse(mockResponseText);

      expect(responses).toHaveLength(1);
      expect(responses[0].statusCode).toBe(200);
      expect(responses[0].body.items).toHaveLength(1);
      expect(responses[0].body.items[0].summary).toBe('Test Event');
    });

    it('should parse multiple responses correctly', () => {
      const mockResponseText = `HTTP/1.1 200 OK
Content-Type: multipart/mixed; boundary=batch_abc123

--batch_abc123
Content-Type: application/http
Content-ID: <response-item1>

HTTP/1.1 200 OK
Content-Type: application/json

{"items": [{"id": "event1", "summary": "Event 1"}]}

--batch_abc123
Content-Type: application/http
Content-ID: <response-item2>

HTTP/1.1 200 OK
Content-Type: application/json

{"items": [{"id": "event2", "summary": "Event 2"}]}

--batch_abc123--`;

      const responses = (batchHandler as any).parseBatchResponse(mockResponseText);

      expect(responses).toHaveLength(2);
      expect(responses[0].body.items[0].summary).toBe('Event 1');
      expect(responses[1].body.items[0].summary).toBe('Event 2');
    });

    it('should handle error responses in batch', () => {
      const mockResponseText = `HTTP/1.1 200 OK
Content-Type: multipart/mixed; boundary=batch_abc123

--batch_abc123
Content-Type: application/http
Content-ID: <response-item1>

HTTP/1.1 404 Not Found
Content-Type: application/json

{
  "error": {
    "code": 404,
    "message": "Calendar not found"
  }
}

--batch_abc123--`;

      const responses = (batchHandler as any).parseBatchResponse(mockResponseText);

      expect(responses).toHaveLength(1);
      expect(responses[0].statusCode).toBe(404);
      expect(responses[0].body.error.code).toBe(404);
      expect(responses[0].body.error.message).toBe('Calendar not found');
    });

    it('should handle mixed success and error responses', () => {
      const mockResponseText = `HTTP/1.1 200 OK
Content-Type: multipart/mixed; boundary=batch_abc123

--batch_abc123
Content-Type: application/http
Content-ID: <response-item1>

HTTP/1.1 200 OK
Content-Type: application/json

{"items": [{"id": "event1", "summary": "Success"}]}

--batch_abc123
Content-Type: application/http
Content-ID: <response-item2>

HTTP/1.1 403 Forbidden
Content-Type: application/json

{
  "error": {
    "code": 403,
    "message": "Access denied"
  }
}

--batch_abc123--`;

      const responses = (batchHandler as any).parseBatchResponse(mockResponseText);

      expect(responses).toHaveLength(2);
      expect(responses[0].statusCode).toBe(200);
      expect(responses[0].body.items[0].summary).toBe('Success');
      expect(responses[1].statusCode).toBe(403);
      expect(responses[1].body.error.message).toBe('Access denied');
    });

    it('should handle empty response parts gracefully', () => {
      const mockResponseText = `HTTP/1.1 200 OK
Content-Type: multipart/mixed; boundary=batch_abc123

--batch_abc123


--batch_abc123
Content-Type: application/http
Content-ID: <response-item1>

HTTP/1.1 200 OK
Content-Type: application/json

{"items": []}

--batch_abc123--`;

      const responses = (batchHandler as any).parseBatchResponse(mockResponseText);

      expect(responses).toHaveLength(1);
      expect(responses[0].statusCode).toBe(200);
      expect(responses[0].body.items).toEqual([]);
    });

    it('should handle malformed JSON gracefully', () => {
      const mockResponseText = `HTTP/1.1 200 OK
Content-Type: multipart/mixed; boundary=batch_abc123

--batch_abc123
Content-Type: application/http
Content-ID: <response-item1>

HTTP/1.1 200 OK
Content-Type: application/json

{invalid json here}

--batch_abc123--`;

      const responses = (batchHandler as any).parseBatchResponse(mockResponseText);

      expect(responses).toHaveLength(1);
      expect(responses[0].statusCode).toBe(200);
      expect(responses[0].body).toBe('{invalid json here}');
    });
  });

  describe('Integration Tests', () => {
    it('should execute batch request with mocked fetch', async () => {
      const mockResponseText = `HTTP/1.1 200 OK
Content-Type: multipart/mixed; boundary=batch_abc123

--batch_abc123
Content-Type: application/http

HTTP/1.1 200 OK
Content-Type: application/json

{"items": [{"id": "event1", "summary": "Test"}]}

--batch_abc123--`;

      global.fetch = vi.fn().mockResolvedValue({
        ok: true,
        status: 200,
        statusText: 'OK',
        text: () => Promise.resolve(mockResponseText)
      });

      const requests: BatchRequest[] = [
        {
          method: 'GET',
          path: '/calendar/v3/calendars/primary/events'
        }
      ];

      const responses = await batchHandler.executeBatch(requests);

      expect(global.fetch).toHaveBeenCalledWith(
        'https://www.googleapis.com/batch/calendar/v3',
        expect.objectContaining({
          method: 'POST',
          headers: expect.objectContaining({
            'Authorization': 'Bearer mock_access_token',
            'Content-Type': expect.stringContaining('multipart/mixed; boundary=')
          })
        })
      );

      expect(responses).toHaveLength(1);
      expect(responses[0].statusCode).toBe(200);
    });

    it('should handle network errors during batch execution', async () => {
      // Create a handler with no retries for this test
      const noRetryHandler = new BatchRequestHandler(mockOAuth2Client);
      (noRetryHandler as any).maxRetries = 0; // Override max retries
      
      global.fetch = vi.fn().mockRejectedValue(new Error('Network error'));

      const requests: BatchRequest[] = [
        {
          method: 'GET',
          path: '/calendar/v3/calendars/primary/events'
        }
      ];

      await expect(noRetryHandler.executeBatch(requests))
        .rejects.toThrow('Failed to execute batch request: Network error');
    });

    it('should handle authentication errors', async () => {
      mockOAuth2Client.getAccessToken = vi.fn().mockRejectedValue(
        new Error('Authentication failed')
      );

      const requests: BatchRequest[] = [
        {
          method: 'GET',
          path: '/calendar/v3/calendars/primary/events'
        }
      ];

      await expect(batchHandler.executeBatch(requests))
        .rejects.toThrow('Authentication failed');
    });
  });
}); 
```

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

```typescript
/**
 * Unit tests for calendar name resolution feature
 * Tests the resolveCalendarId and resolveCalendarIds methods in BaseToolHandler
 */
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ListEventsHandler } from '../../../handlers/core/ListEventsHandler.js';
import { OAuth2Client } from 'google-auth-library';
import { google } from 'googleapis';

// Mock googleapis globally
vi.mock('googleapis', () => ({
  google: {
    calendar: vi.fn(() => ({
      events: {
        list: vi.fn()
      },
      calendarList: {
        list: vi.fn(),
        get: vi.fn()
      }
    }))
  }
}));

describe('Calendar Name Resolution', () => {
  const mockOAuth2Client = {
    getAccessToken: vi.fn().mockResolvedValue({ token: 'mock-token' })
  } as unknown as OAuth2Client;

  let handler: ListEventsHandler;
  let mockCalendar: any;

  beforeEach(() => {
    handler = new ListEventsHandler();
    mockCalendar = {
      events: {
        list: vi.fn().mockResolvedValue({
          data: {
            items: []
          }
        })
      },
      calendarList: {
        list: vi.fn().mockResolvedValue({
          data: {
            items: [
              {
                id: 'primary',
                summary: 'Primary Calendar',
                summaryOverride: undefined
              },
              {
                id: '[email protected]',
                summary: 'Engineering Team - Project Alpha - Q4 2024',
                summaryOverride: 'Work Calendar'
              },
              {
                id: '[email protected]',
                summary: 'Personal Calendar',
                summaryOverride: undefined
              },
              {
                id: '[email protected]',
                summary: 'Team Events',
                summaryOverride: 'My Team'
              }
            ]
          }
        }),
        get: vi.fn().mockResolvedValue({
          data: { timeZone: 'UTC' }
        })
      }
    };
    vi.mocked(google.calendar).mockReturnValue(mockCalendar);
  });

  describe('summaryOverride matching priority', () => {
    it('should match summaryOverride before summary (exact match)', async () => {
      const args = {
        calendarId: 'Work Calendar',
        timeMin: '2025-06-02T00:00:00Z',
        timeMax: '2025-06-09T23:59:59Z'
      };

      await handler.runTool(args, mockOAuth2Client);

      // Should have called events.list with the resolved ID
      expect(mockCalendar.events.list).toHaveBeenCalledWith(
        expect.objectContaining({
          calendarId: '[email protected]'
        })
      );
    });

    it('should fall back to summary if summaryOverride does not match', async () => {
      const args = {
        calendarId: 'Personal Calendar',
        timeMin: '2025-06-02T00:00:00Z',
        timeMax: '2025-06-09T23:59:59Z'
      };

      await handler.runTool(args, mockOAuth2Client);

      expect(mockCalendar.events.list).toHaveBeenCalledWith(
        expect.objectContaining({
          calendarId: '[email protected]'
        })
      );
    });

    it('should match summaryOverride case-insensitively', async () => {
      const args = {
        calendarId: 'WORK CALENDAR',
        timeMin: '2025-06-02T00:00:00Z',
        timeMax: '2025-06-09T23:59:59Z'
      };

      await handler.runTool(args, mockOAuth2Client);

      expect(mockCalendar.events.list).toHaveBeenCalledWith(
        expect.objectContaining({
          calendarId: '[email protected]'
        })
      );
    });

    it('should match summary case-insensitively', async () => {
      const args = {
        calendarId: 'personal calendar',
        timeMin: '2025-06-02T00:00:00Z',
        timeMax: '2025-06-09T23:59:59Z'
      };

      await handler.runTool(args, mockOAuth2Client);

      expect(mockCalendar.events.list).toHaveBeenCalledWith(
        expect.objectContaining({
          calendarId: '[email protected]'
        })
      );
    });

    it('should prefer summaryOverride over similar summary name', async () => {
      // Even if there's a calendar with summary "My Team",
      // it should match the summaryOverride first
      const args = {
        calendarId: 'My Team',
        timeMin: '2025-06-02T00:00:00Z',
        timeMax: '2025-06-09T23:59:59Z'
      };

      await handler.runTool(args, mockOAuth2Client);

      expect(mockCalendar.events.list).toHaveBeenCalledWith(
        expect.objectContaining({
          calendarId: '[email protected]'
        })
      );
    });
  });

  describe('multiple calendar name resolution', () => {
    it('should resolve multiple calendar names including summaryOverride', async () => {
      const args = {
        calendarId: ['Work Calendar', 'Personal Calendar'],  // Pass as array, not JSON string
        timeMin: '2025-06-02T00:00:00Z',
        timeMax: '2025-06-09T23:59:59Z'
      };

      // Mock fetch for batch requests
      global.fetch = vi.fn().mockResolvedValue({
        ok: true,
        status: 200,
        headers: {
          get: vi.fn()
        },
        text: () => Promise.resolve(`--batch_boundary
Content-Type: application/http
Content-ID: <item1>

HTTP/1.1 200 OK
Content-Type: application/json

{"items": []}

--batch_boundary
Content-Type: application/http
Content-ID: <item2>

HTTP/1.1 200 OK
Content-Type: application/json

{"items": []}

--batch_boundary--`)
      });

      await handler.runTool(args, mockOAuth2Client);

      // Should have called fetch with both resolved calendar IDs
      expect(global.fetch).toHaveBeenCalled();
      const fetchCall = vi.mocked(global.fetch).mock.calls[0];
      const requestBody = fetchCall[1]?.body as string;

      // Calendar IDs may be URL-encoded in batch request
      expect(requestBody).toMatch(/work@example\.com|work%40example\.com/);
      expect(requestBody).toMatch(/personal@example\.com|personal%40example\.com/);
    });

    it('should resolve mix of IDs, summary names, and summaryOverride names', async () => {
      const args = {
        calendarId: ['primary', 'Work Calendar', 'Personal Calendar'],  // Pass as array
        timeMin: '2025-06-02T00:00:00Z',
        timeMax: '2025-06-09T23:59:59Z'
      };

      global.fetch = vi.fn().mockResolvedValue({
        ok: true,
        status: 200,
        headers: {
          get: vi.fn()
        },
        text: () => Promise.resolve(`--batch_boundary
Content-Type: application/http

HTTP/1.1 200 OK

{"items": []}
--batch_boundary--`)
      });

      await handler.runTool(args, mockOAuth2Client);

      const fetchCall = vi.mocked(global.fetch).mock.calls[0];
      const requestBody = fetchCall[1]?.body as string;

      // Should include all three calendar IDs (may be URL-encoded)
      expect(requestBody).toContain('primary');
      expect(requestBody).toMatch(/work@example\.com|work%40example\.com/);
      expect(requestBody).toMatch(/personal@example\.com|personal%40example\.com/);
    });
  });

  describe('error handling with summaryOverride', () => {
    it('should provide helpful error listing both summaryOverride and summary', async () => {
      const args = {
        calendarId: 'NonExistentCalendar',
        timeMin: '2025-06-02T00:00:00Z',
        timeMax: '2025-06-09T23:59:59Z'
      };

      await expect(handler.runTool(args, mockOAuth2Client)).rejects.toThrow(
        /Calendar\(s\) not found: "NonExistentCalendar"/
      );

      try {
        await handler.runTool(args, mockOAuth2Client);
      } catch (error: any) {
        // Error message should show both override and original name
        expect(error.message).toContain('Work Calendar');
        expect(error.message).toContain('Engineering Team - Project Alpha - Q4 2024');
        expect(error.message).toContain('My Team');
        expect(error.message).toContain('Team Events');
      }
    });

    it('should handle calendar with summaryOverride same as summary', async () => {
      // Update mock to have a calendar where override equals summary
      mockCalendar.calendarList.list.mockResolvedValueOnce({
        data: {
          items: [
            {
              id: '[email protected]',
              summary: 'Test Calendar',
              summaryOverride: 'Test Calendar'
            }
          ]
        }
      });

      const args = {
        calendarId: 'NonExistent',
        timeMin: '2025-06-02T00:00:00Z',
        timeMax: '2025-06-09T23:59:59Z'
      };

      try {
        await handler.runTool(args, mockOAuth2Client);
      } catch (error: any) {
        // Should not show duplicate when override equals summary
        const message = error.message;
        const matches = (message.match(/Test Calendar/g) || []).length;
        expect(matches).toBe(1);
      }
    });
  });

  describe('performance optimization', () => {
    it('should skip API call when all inputs are IDs', async () => {
      const args = {
        calendarId: ['primary', '[email protected]'],  // Pass as array
        timeMin: '2025-06-02T00:00:00Z',
        timeMax: '2025-06-09T23:59:59Z'
      };

      // Reset the mock to track calls
      mockCalendar.calendarList.list.mockClear();

      global.fetch = vi.fn().mockResolvedValue({
        ok: true,
        status: 200,
        headers: {
          get: vi.fn()
        },
        text: () => Promise.resolve(`--batch_boundary
Content-Type: application/http

HTTP/1.1 200 OK

{"items": []}
--batch_boundary--`)
      });

      await handler.runTool(args, mockOAuth2Client);

      // Should NOT have called calendarList.list since all inputs are IDs
      expect(mockCalendar.calendarList.list).not.toHaveBeenCalled();
    });

    it('should call API only once for multiple name resolutions', async () => {
      const args = {
        calendarId: ['Work Calendar', 'Personal Calendar', 'My Team'],  // Pass as array
        timeMin: '2025-06-02T00:00:00Z',
        timeMax: '2025-06-09T23:59:59Z'
      };

      mockCalendar.calendarList.list.mockClear();

      global.fetch = vi.fn().mockResolvedValue({
        ok: true,
        status: 200,
        headers: {
          get: vi.fn()
        },
        text: () => Promise.resolve(`--batch_boundary
Content-Type: application/http

HTTP/1.1 200 OK

{"items": []}
--batch_boundary--`)
      });

      await handler.runTool(args, mockOAuth2Client);

      // Should have called calendarList.list exactly once
      expect(mockCalendar.calendarList.list).toHaveBeenCalledTimes(1);
    });
  });

  describe('input validation', () => {
    it('should filter out empty strings', async () => {
      const args = {
        calendarId: ['primary', '', 'Work Calendar'],  // Pass as array
        timeMin: '2025-06-02T00:00:00Z',
        timeMax: '2025-06-09T23:59:59Z'
      };

      global.fetch = vi.fn().mockResolvedValue({
        ok: true,
        status: 200,
        headers: {
          get: vi.fn()
        },
        text: () => Promise.resolve(`--batch_boundary
Content-Type: application/http

HTTP/1.1 200 OK

{"items": []}
--batch_boundary--`)
      });

      // Should not throw - empty string should be filtered out
      await expect(handler.runTool(args, mockOAuth2Client)).resolves.toBeDefined();
    });

    it('should reject when all inputs are empty/whitespace', async () => {
      const args = {
        calendarId: ['', '  ', '\t'],  // Pass as array
        timeMin: '2025-06-02T00:00:00Z',
        timeMax: '2025-06-09T23:59:59Z'
      };

      await expect(handler.runTool(args, mockOAuth2Client)).rejects.toThrow(
        /At least one valid calendar identifier is required/
      );
    });
  });
});

```

--------------------------------------------------------------------------------
/src/tests/unit/index.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Tests for the Google Calendar MCP Server implementation
 */
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { OAuth2Client } from "google-auth-library";

// Import tool handlers to test them directly
import { ListCalendarsHandler } from "../../handlers/core/ListCalendarsHandler.js";
import { CreateEventHandler } from "../../handlers/core/CreateEventHandler.js";
import { ListEventsHandler } from "../../handlers/core/ListEventsHandler.js";

// Mock OAuth2Client
vi.mock('google-auth-library', () => ({
  OAuth2Client: vi.fn().mockImplementation(() => ({
    setCredentials: vi.fn(),
    refreshAccessToken: vi.fn().mockResolvedValue({ credentials: { access_token: 'mock_access_token' } }),
    on: vi.fn(),
  }))
}));

// Mock googleapis
vi.mock('googleapis', () => ({
  google: {
    calendar: vi.fn().mockReturnValue({
      calendarList: {
        list: vi.fn(),
        get: vi.fn()
      },
      events: {
        list: vi.fn(),
        insert: vi.fn(),
        patch: vi.fn(),
        delete: vi.fn()
      },
      colors: {
        get: vi.fn()
      },
      freebusy: {
        query: vi.fn()
      }
    })
  }
}));

// Mock TokenManager
vi.mock('./auth/tokenManager.js', () => ({
  TokenManager: vi.fn().mockImplementation(() => ({
    validateTokens: vi.fn().mockResolvedValue(true),
    loadSavedTokens: vi.fn().mockResolvedValue(true),
    clearTokens: vi.fn(),
  })),
}));

describe('Google Calendar MCP Server', () => {
  let mockOAuth2Client: OAuth2Client;

  beforeEach(() => {
    vi.clearAllMocks();
    mockOAuth2Client = new OAuth2Client();
  });

  describe('McpServer Configuration', () => {
    it('should create McpServer with correct configuration', () => {
      const server = new McpServer({
        name: "google-calendar",
        version: "1.2.0"
      });

      expect(server).toBeDefined();
      // McpServer doesn't expose internal configuration for testing,
      // but we can verify it doesn't throw during creation
    });
  });

  describe('Tool Handlers', () => {
    it('should handle list-calendars tool correctly', async () => {
      const handler = new ListCalendarsHandler();
      const { google } = await import('googleapis');
      const mockCalendarApi = google.calendar('v3');

      // Mock the API response
      (mockCalendarApi.calendarList.list as any).mockResolvedValue({
        data: {
          items: [
            { 
              id: 'cal1', 
              summary: 'Work Calendar',
              timeZone: 'America/New_York',
              kind: 'calendar#calendarListEntry',
              accessRole: 'owner',
              primary: true,
              selected: true,
              hidden: false,
              backgroundColor: '#0D7377',
              defaultReminders: [
                { method: 'popup', minutes: 15 },
                { method: 'email', minutes: 60 }
              ],
              description: 'Work-related events and meetings'
            },
            { 
              id: 'cal2', 
              summary: 'Personal',
              timeZone: 'America/Los_Angeles',
              kind: 'calendar#calendarListEntry',
              accessRole: 'reader',
              primary: false,
              selected: true,
              hidden: false,
              backgroundColor: '#D50000'
            },
          ]
        }
      });

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

      expect(mockCalendarApi.calendarList.list).toHaveBeenCalled();

      // Parse the JSON response
      const response = JSON.parse((result.content as any)[0].text);

      expect(response.totalCount).toBe(2);
      expect(response.calendars).toHaveLength(2);
      expect(response.calendars[0]).toMatchObject({
        id: 'cal1',
        summary: 'Work Calendar',
        description: 'Work-related events and meetings',
        timeZone: 'America/New_York',
        backgroundColor: '#0D7377',
        accessRole: 'owner',
        primary: true,
        selected: true,
        hidden: false
      });
      expect(response.calendars[0].defaultReminders).toHaveLength(2);
      expect(response.calendars[1]).toMatchObject({
        id: 'cal2',
        summary: 'Personal',
        timeZone: 'America/Los_Angeles',
        backgroundColor: '#D50000',
        accessRole: 'reader',
        primary: false
      });
    });

    it('should handle create-event tool with valid arguments', async () => {
      const handler = new CreateEventHandler();
      const { google } = await import('googleapis');
      const mockCalendarApi = google.calendar('v3');

      const mockEventArgs = {
        calendarId: 'primary',
        summary: 'Team Meeting',
        description: 'Discuss project progress',
        start: '2024-08-15T10:00:00',
        end: '2024-08-15T11:00:00',
        attendees: [{ email: '[email protected]' }],
        location: 'Conference Room 4',
      };

      const mockApiResponse = {
        id: 'eventId123',
        summary: mockEventArgs.summary,
      };

      // Mock calendar details for timezone retrieval
      (mockCalendarApi.calendarList.get as any).mockResolvedValue({
        data: {
          id: 'primary',
          timeZone: 'America/Los_Angeles'
        }
      });

      (mockCalendarApi.events.insert as any).mockResolvedValue({ data: mockApiResponse });

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

      expect(mockCalendarApi.calendarList.get).toHaveBeenCalledWith({ calendarId: 'primary' });
      expect(mockCalendarApi.events.insert).toHaveBeenCalledWith({
        calendarId: mockEventArgs.calendarId,
        requestBody: expect.objectContaining({
          summary: mockEventArgs.summary,
          description: mockEventArgs.description,
          start: { dateTime: mockEventArgs.start, timeZone: 'America/Los_Angeles' },
          end: { dateTime: mockEventArgs.end, timeZone: 'America/Los_Angeles' },
          attendees: mockEventArgs.attendees,
          location: mockEventArgs.location,
        }),
      });

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

    it('should use calendar default timezone when timeZone is not provided', async () => {
      const handler = new CreateEventHandler();
      const { google } = await import('googleapis');
      const mockCalendarApi = google.calendar('v3');

      const mockEventArgs = {
        calendarId: 'primary',
        summary: 'Meeting without timezone',
        start: '2024-08-15T10:00:00', // Timezone-naive datetime
        end: '2024-08-15T11:00:00', // Timezone-naive datetime
      };

      // Mock calendar details with specific timezone
      (mockCalendarApi.calendarList.get as any).mockResolvedValue({
        data: {
          id: 'primary',
          timeZone: 'Europe/London'
        }
      });

      (mockCalendarApi.events.insert as any).mockResolvedValue({
        data: { id: 'testEvent', summary: mockEventArgs.summary }
      });

      await handler.runTool(mockEventArgs, mockOAuth2Client);

      // Verify that the calendar's timezone was used
      expect(mockCalendarApi.events.insert).toHaveBeenCalledWith({
        calendarId: mockEventArgs.calendarId,
        requestBody: expect.objectContaining({
          start: { dateTime: mockEventArgs.start, timeZone: 'Europe/London' },
          end: { dateTime: mockEventArgs.end, timeZone: 'Europe/London' },
        }),
      });
    });

    it('should handle timezone-aware datetime strings correctly', async () => {
      const handler = new CreateEventHandler();
      const { google } = await import('googleapis');
      const mockCalendarApi = google.calendar('v3');

      const mockEventArgs = {
        calendarId: 'primary',
        summary: 'Meeting with timezone in datetime',
        start: '2024-08-15T10:00:00-07:00', // Timezone-aware datetime
        end: '2024-08-15T11:00:00-07:00', // Timezone-aware datetime
      };

      // Mock calendar details (should not be used since timezone is in datetime)
      (mockCalendarApi.calendarList.get as any).mockResolvedValue({
        data: {
          id: 'primary',
          timeZone: 'Europe/London'
        }
      });

      (mockCalendarApi.events.insert as any).mockResolvedValue({
        data: { id: 'testEvent', summary: mockEventArgs.summary }
      });

      await handler.runTool(mockEventArgs, mockOAuth2Client);

      // Verify that timezone from datetime was used (no timeZone property)
      expect(mockCalendarApi.events.insert).toHaveBeenCalledWith({
        calendarId: mockEventArgs.calendarId,
        requestBody: expect.objectContaining({
          start: { dateTime: mockEventArgs.start }, // No timeZone property
          end: { dateTime: mockEventArgs.end }, // No timeZone property
        }),
      });
    });

    it('should handle list-events tool correctly', async () => {
      const handler = new ListEventsHandler();
      const { google } = await import('googleapis');
      const mockCalendarApi = google.calendar('v3');

      const listEventsArgs = {
        calendarId: 'primary',
        timeMin: '2024-08-01T00:00:00Z',
        timeMax: '2024-08-31T23:59:59Z',
      };

      const mockEvents = [
        { 
          id: 'event1', 
          summary: 'Meeting', 
          start: { dateTime: '2024-08-15T10:00:00Z' }, 
          end: { dateTime: '2024-08-15T11:00:00Z' } 
        },
      ];

      (mockCalendarApi.events.list as any).mockResolvedValue({
        data: { items: mockEvents }
      });

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

      expect(mockCalendarApi.events.list).toHaveBeenCalledWith({
        calendarId: listEventsArgs.calendarId,
        timeMin: listEventsArgs.timeMin,
        timeMax: listEventsArgs.timeMax,
        singleEvents: true,
        orderBy: 'startTime'
      });

      // Should return structured JSON with events
      expect(result.content).toHaveLength(1);
      expect(result.content[0].type).toBe('text');
      const response = JSON.parse((result.content[0] as any).text);
      expect(response.events).toHaveLength(1);
      expect(response.totalCount).toBe(1);
      expect(response.events[0].id).toBe('event1');
    });
  });

  describe('Configuration and Environment Variables', () => {
    it('should parse environment variables correctly', async () => {
      const originalEnv = process.env;
      
      try {
        // Set test environment variables
        process.env.TRANSPORT = 'http';
        process.env.PORT = '4000';
        process.env.HOST = '0.0.0.0';
        process.env.DEBUG = 'true';

        // Import config parser after setting env vars
        const { parseArgs } = await import('../../config/TransportConfig.js');
        
        const config = parseArgs([]);

        expect(config.transport.type).toBe('http');
        expect(config.transport.port).toBe(4000);
        expect(config.transport.host).toBe('0.0.0.0');
        expect(config.debug).toBe(true);
      } finally {
        // Restore original environment
        process.env = originalEnv;
      }
    });

    it('should allow CLI arguments to override environment variables', async () => {
      const originalEnv = process.env;
      
      try {
        // Set environment variables
        process.env.TRANSPORT = 'http';
        process.env.PORT = '4000';

        const { parseArgs } = await import('../../config/TransportConfig.js');
        
        // CLI arguments should override env vars
        const config = parseArgs(['--transport', 'stdio', '--port', '5000']);

        expect(config.transport.type).toBe('stdio');
        expect(config.transport.port).toBe(5000);
      } finally {
        process.env = originalEnv;
      }
    });
  });
});
```

--------------------------------------------------------------------------------
/src/tests/unit/handlers/list-events-registry.test.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Comprehensive tests for list-events tool registration flow
 * Tests the complete path: schema validation → handlerFunction → handler execution
 *
 * These tests verify the fix for issue #95 by testing:
 * 1. Schema validation (accepts all formats)
 * 2. HandlerFunction preprocessing (converts single-quoted JSON, validates arrays)
 * 3. Real-world scenarios from Home Assistant and other integrations
 */

import { describe, it, expect } from 'vitest';
import { ToolSchemas, ToolRegistry } from '../../../tools/registry.js';

// Get the handlerFunction for testing the full flow
const toolDefinition = (ToolRegistry as any).tools?.find((t: any) => t.name === 'list-events');
const handlerFunction = toolDefinition?.handlerFunction;

describe('list-events Registration Flow (Schema + HandlerFunction)', () => {
  describe('Schema validation (first step)', () => {
    it('should validate native array format', () => {
      const input = {
        calendarId: ['primary', '[email protected]'],
        timeMin: '2024-01-01T00:00:00',
        timeMax: '2024-01-02T00:00:00'
      };

      const result = ToolSchemas['list-events'].safeParse(input);
      expect(result.success).toBe(true);
      expect(result.data?.calendarId).toEqual(['primary', '[email protected]']);
    });

    it('should validate single string format', () => {
      const input = {
        calendarId: 'primary',
        timeMin: '2024-01-01T00:00:00',
        timeMax: '2024-01-02T00:00:00'
      };

      const result = ToolSchemas['list-events'].safeParse(input);
      expect(result.success).toBe(true);
      expect(result.data?.calendarId).toBe('primary');
    });

    it('should validate JSON string format', () => {
      const input = {
        calendarId: '["primary", "[email protected]"]',
        timeMin: '2024-01-01T00:00:00',
        timeMax: '2024-01-02T00:00:00'
      };

      const result = ToolSchemas['list-events'].safeParse(input);
      expect(result.success).toBe(true);
      expect(result.data?.calendarId).toBe('["primary", "[email protected]"]');
    });
  });

  describe('Array validation constraints', () => {
    it('should enforce minimum array length', () => {
      const input = {
        calendarId: [],
        timeMin: '2024-01-01T00:00:00',
        timeMax: '2024-01-02T00:00:00'
      };

      const result = ToolSchemas['list-events'].safeParse(input);
      expect(result.success).toBe(false);
      if (!result.success) {
        expect(result.error.issues[0].message).toContain('At least one calendar ID is required');
      }
    });

    it('should enforce maximum array length', () => {
      const input = {
        calendarId: Array(51).fill('calendar'),
        timeMin: '2024-01-01T00:00:00',
        timeMax: '2024-01-02T00:00:00'
      };

      const result = ToolSchemas['list-events'].safeParse(input);
      expect(result.success).toBe(false);
      if (!result.success) {
        expect(result.error.issues[0].message).toContain('Maximum 50 calendars');
      }
    });

    it('should reject duplicate calendar IDs in array', () => {
      const input = {
        calendarId: ['primary', 'primary'],
        timeMin: '2024-01-01T00:00:00',
        timeMax: '2024-01-02T00:00:00'
      };

      const result = ToolSchemas['list-events'].safeParse(input);
      expect(result.success).toBe(false);
      if (!result.success) {
        expect(result.error.issues[0].message).toContain('Duplicate calendar IDs');
      }
    });

    it('should reject empty strings in array', () => {
      const input = {
        calendarId: ['primary', ''],
        timeMin: '2024-01-01T00:00:00',
        timeMax: '2024-01-02T00:00:00'
      };

      const result = ToolSchemas['list-events'].safeParse(input);
      expect(result.success).toBe(false);
    });
  });

  describe('Type preservation after validation', () => {
    it('should preserve array type for native arrays (issue #95 fix)', () => {
      const input = {
        calendarId: ['primary', '[email protected]', '[email protected]'],
        timeMin: '2024-01-01T00:00:00',
        timeMax: '2024-01-02T00:00:00'
      };

      const result = ToolSchemas['list-events'].parse(input);

      // The key fix: arrays should NOT be transformed to JSON strings by the schema
      // The handlerFunction will handle the conversion logic
      expect(Array.isArray(result.calendarId)).toBe(true);
      expect(result.calendarId).toEqual(['primary', '[email protected]', '[email protected]']);
    });

    it('should preserve string type for single strings', () => {
      const input = {
        calendarId: 'primary',
        timeMin: '2024-01-01T00:00:00',
        timeMax: '2024-01-02T00:00:00'
      };

      const result = ToolSchemas['list-events'].parse(input);
      expect(typeof result.calendarId).toBe('string');
      expect(result.calendarId).toBe('primary');
    });

    it('should preserve string type for JSON strings', () => {
      const input = {
        calendarId: '["primary", "[email protected]"]',
        timeMin: '2024-01-01T00:00:00',
        timeMax: '2024-01-02T00:00:00'
      };

      const result = ToolSchemas['list-events'].parse(input);
      expect(typeof result.calendarId).toBe('string');
      expect(result.calendarId).toBe('["primary", "[email protected]"]');
    });
  });

  describe('Real-world scenarios from issue #95', () => {
    it('should handle exact input from Home Assistant multi-mcp', () => {
      // This is the exact format that was failing in issue #95
      const input = {
        calendarId: ['primary', '[email protected]', '[email protected]', '[email protected]', '[email protected]'],
        timeMin: '2025-10-09T00:00:00',
        timeMax: '2025-10-09T23:59:59'
      };

      const result = ToolSchemas['list-events'].safeParse(input);
      expect(result.success).toBe(true);
      expect(Array.isArray(result.data?.calendarId)).toBe(true);
      expect(result.data?.calendarId).toHaveLength(5);
    });

    it('should handle mixed special characters in calendar IDs', () => {
      const input = {
        calendarId: ['primary', '[email protected]', '[email protected]'],
        timeMin: '2024-01-01T00:00:00',
        timeMax: '2024-01-02T00:00:00'
      };

      const result = ToolSchemas['list-events'].safeParse(input);
      expect(result.success).toBe(true);
      expect(result.data?.calendarId).toEqual(['primary', '[email protected]', '[email protected]']);
    });

    it('should accept single-quoted JSON string format (Python/shell style)', () => {
      // Some clients may send JSON-like strings with single quotes instead of double quotes
      // e.g., from Python str() representation or shell scripts
      // The schema should accept it as a string (handlerFunction will process it)
      const input = {
        calendarId: "['primary', '[email protected]']",
        timeMin: '2025-10-09T00:00:00',
        timeMax: '2025-10-09T23:59:59'
      };

      // Schema should accept it as a string (not reject it)
      const result = ToolSchemas['list-events'].safeParse(input);
      expect(result.success).toBe(true);
      expect(typeof result.data?.calendarId).toBe('string');
      expect(result.data?.calendarId).toBe("['primary', '[email protected]']");
    });
  });

  // HandlerFunction tests - second step after schema validation
  if (!handlerFunction) {
    console.warn('⚠️  handlerFunction not found - skipping handler tests');
  } else {
    describe('HandlerFunction preprocessing (second step)', () => {
      describe('Format handling', () => {
        it('should pass through native arrays unchanged', async () => {
          const input = {
            calendarId: ['primary', '[email protected]'],
            timeMin: '2024-01-01T00:00:00',
            timeMax: '2024-01-02T00:00:00'
          };

          const result = await handlerFunction(input);
          expect(Array.isArray(result.calendarId)).toBe(true);
          expect(result.calendarId).toEqual(['primary', '[email protected]']);
        });

        it('should pass through single strings unchanged', async () => {
          const input = {
            calendarId: 'primary',
            timeMin: '2024-01-01T00:00:00',
            timeMax: '2024-01-02T00:00:00'
          };

          const result = await handlerFunction(input);
          expect(typeof result.calendarId).toBe('string');
          expect(result.calendarId).toBe('primary');
        });

        it('should parse valid JSON strings with double quotes', async () => {
          const input = {
            calendarId: '["primary", "[email protected]"]',
            timeMin: '2024-01-01T00:00:00',
            timeMax: '2024-01-02T00:00:00'
          };

          const result = await handlerFunction(input);
          expect(Array.isArray(result.calendarId)).toBe(true);
          expect(result.calendarId).toEqual(['primary', '[email protected]']);
        });

        it('should parse single-quoted JSON-like strings (Python/shell style) - THE KEY FIX', async () => {
          // This is the failing case that needed fixing
          const input = {
            calendarId: "['primary', '[email protected]']",
            timeMin: '2025-10-09T00:00:00',
            timeMax: '2025-10-09T23:59:59'
          };

          const result = await handlerFunction(input);
          expect(Array.isArray(result.calendarId)).toBe(true);
          expect(result.calendarId).toEqual(['primary', '[email protected]']);
        });

        it('should handle calendar IDs with apostrophes in single-quoted JSON', async () => {
          // Calendar IDs can contain apostrophes (e.g., "John's Calendar")
          // Our replacement logic should not break these
          const input = {
            calendarId: "['primary', '[email protected]']",
            timeMin: '2024-01-01T00:00:00',
            timeMax: '2024-01-02T00:00:00'
          };

          const result = await handlerFunction(input);
          expect(Array.isArray(result.calendarId)).toBe(true);
          expect(result.calendarId).toEqual(['primary', '[email protected]']);
        });

        it('should handle JSON strings with whitespace', async () => {
          const input = {
            calendarId: '  ["primary", "[email protected]"]  ',
            timeMin: '2024-01-01T00:00:00',
            timeMax: '2024-01-02T00:00:00'
          };

          const result = await handlerFunction(input);
          expect(Array.isArray(result.calendarId)).toBe(true);
          expect(result.calendarId).toEqual(['primary', '[email protected]']);
        });
      });

      describe('JSON string validation', () => {
        it('should reject empty arrays in JSON strings', async () => {
          const input = {
            calendarId: '[]',
            timeMin: '2024-01-01T00:00:00',
            timeMax: '2024-01-02T00:00:00'
          };

          await expect(handlerFunction(input)).rejects.toThrow('At least one calendar ID is required');
        });

        it('should reject arrays exceeding 50 calendars', async () => {
          const input = {
            calendarId: JSON.stringify(Array(51).fill('calendar')),
            timeMin: '2024-01-01T00:00:00',
            timeMax: '2024-01-02T00:00:00'
          };

          await expect(handlerFunction(input)).rejects.toThrow('Maximum 50 calendars');
        });

        it('should reject duplicate calendar IDs in JSON strings', async () => {
          const input = {
            calendarId: '["primary", "primary"]',
            timeMin: '2024-01-01T00:00:00',
            timeMax: '2024-01-02T00:00:00'
          };

          await expect(handlerFunction(input)).rejects.toThrow('Duplicate calendar IDs');
        });
      });

      describe('Error handling', () => {
        it('should provide clear error for malformed JSON array', async () => {
          const input = {
            calendarId: '["primary", "missing-quote}]',
            timeMin: '2024-01-01T00:00:00',
            timeMax: '2024-01-02T00:00:00'
          };

          await expect(handlerFunction(input)).rejects.toThrow('Invalid JSON format for calendarId');
        });

        it('should reject JSON arrays with non-string elements', async () => {
          const input = {
            calendarId: '["primary", 123, null]',
            timeMin: '2024-01-01T00:00:00',
            timeMax: '2024-01-02T00:00:00'
          };

          await expect(handlerFunction(input)).rejects.toThrow('Array must contain only non-empty strings');
        });
      });
    });
  }
});

```

--------------------------------------------------------------------------------
/src/auth/tokenManager.ts:
--------------------------------------------------------------------------------

```typescript
import { OAuth2Client, Credentials } from 'google-auth-library';
import fs from 'fs/promises';
import { getSecureTokenPath, getAccountMode, getLegacyTokenPath } from './utils.js';
import { GaxiosError } from 'gaxios';
import { mkdir } from 'fs/promises';
import { dirname } from 'path';

// Interface for multi-account token storage
interface MultiAccountTokens {
  normal?: Credentials;
  test?: Credentials;
}

export class TokenManager {
  private oauth2Client: OAuth2Client;
  private tokenPath: string;
  private accountMode: 'normal' | 'test';

  constructor(oauth2Client: OAuth2Client) {
    this.oauth2Client = oauth2Client;
    this.tokenPath = getSecureTokenPath();
    this.accountMode = getAccountMode();
    this.setupTokenRefresh();
  }

  // Method to expose the token path
  public getTokenPath(): string {
    return this.tokenPath;
  }

  // Method to get current account mode
  public getAccountMode(): 'normal' | 'test' {
    return this.accountMode;
  }

  // Method to switch account mode (useful for testing)
  public setAccountMode(mode: 'normal' | 'test'): void {
    this.accountMode = mode;
  }

  private async ensureTokenDirectoryExists(): Promise<void> {
    try {
      await mkdir(dirname(this.tokenPath), { recursive: true });
    } catch (error) {
      process.stderr.write(`Failed to create token directory: ${error}\n`);
    }
  }

  private async loadMultiAccountTokens(): Promise<MultiAccountTokens> {
    try {
      const fileContent = await fs.readFile(this.tokenPath, "utf-8");
      const parsed = JSON.parse(fileContent);
      
      // Check if this is the old single-account format
      if (parsed.access_token || parsed.refresh_token) {
        // Convert old format to new multi-account format
        const multiAccountTokens: MultiAccountTokens = {
          normal: parsed
        };
        await this.saveMultiAccountTokens(multiAccountTokens);
        return multiAccountTokens;
      }
      
      // Already in multi-account format
      return parsed as MultiAccountTokens;
    } catch (error: unknown) {
      if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
        // File doesn't exist, return empty structure
        return {};
      }
      throw error;
    }
  }

  private async saveMultiAccountTokens(multiAccountTokens: MultiAccountTokens): Promise<void> {
    await this.ensureTokenDirectoryExists();
    await fs.writeFile(this.tokenPath, JSON.stringify(multiAccountTokens, null, 2), {
      mode: 0o600,
    });
  }

  private setupTokenRefresh(): void {
    this.oauth2Client.on("tokens", async (newTokens) => {
      try {
        const multiAccountTokens = await this.loadMultiAccountTokens();
        const currentTokens = multiAccountTokens[this.accountMode] || {};
        
        const updatedTokens = {
          ...currentTokens,
          ...newTokens,
          refresh_token: newTokens.refresh_token || currentTokens.refresh_token,
        };
        
        multiAccountTokens[this.accountMode] = updatedTokens;
        await this.saveMultiAccountTokens(multiAccountTokens);
        
        if (process.env.NODE_ENV !== 'test') {
          process.stderr.write(`Tokens updated and saved for ${this.accountMode} account\n`);
        }
      } catch (error: unknown) {
        // Handle case where file might not exist yet
        if (error instanceof Error && 'code' in error && error.code === 'ENOENT') { 
          try {
            const multiAccountTokens: MultiAccountTokens = {
              [this.accountMode]: newTokens
            };
            await this.saveMultiAccountTokens(multiAccountTokens);
            if (process.env.NODE_ENV !== 'test') {
              process.stderr.write(`New tokens saved for ${this.accountMode} account\n`);
            }
          } catch (writeError) {
            process.stderr.write("Error saving initial tokens: ");
            if (writeError) {
              process.stderr.write(writeError.toString());
            }
            process.stderr.write("\n");
          }
        } else {
          process.stderr.write("Error saving updated tokens: ");
          if (error instanceof Error) {
            process.stderr.write(error.message);
          } else if (typeof error === 'string') {
            process.stderr.write(error);
          }
          process.stderr.write("\n");
        }
      }
    });
  }

  private async migrateLegacyTokens(): Promise<boolean> {
    const legacyPath = getLegacyTokenPath();
    try {
      // Check if legacy tokens exist
      if (!(await fs.access(legacyPath).then(() => true).catch(() => false))) {
        return false; // No legacy tokens to migrate
      }

      // Read legacy tokens
      const legacyTokens = JSON.parse(await fs.readFile(legacyPath, "utf-8"));
      
      if (!legacyTokens || typeof legacyTokens !== "object") {
        process.stderr.write("Invalid legacy token format, skipping migration\n");
        return false;
      }

      // Ensure new token directory exists
      await this.ensureTokenDirectoryExists();
      
      // Copy to new location
      await fs.writeFile(this.tokenPath, JSON.stringify(legacyTokens, null, 2), {
        mode: 0o600,
      });
      
      process.stderr.write(`Migrated tokens from legacy location: ${legacyPath} to: ${this.tokenPath}\n`);
      
      // Optionally remove legacy file after successful migration
      try {
        await fs.unlink(legacyPath);
        process.stderr.write("Removed legacy token file\n");
      } catch (unlinkErr) {
        process.stderr.write(`Warning: Could not remove legacy token file: ${unlinkErr}\n`);
      }
      
      return true;
    } catch (error) {
      process.stderr.write(`Error migrating legacy tokens: ${error}\n`);
      return false;
    }
  }

  async loadSavedTokens(): Promise<boolean> {
    try {
      await this.ensureTokenDirectoryExists();
      
      // Check if current token file exists
      const tokenExists = await fs.access(this.tokenPath).then(() => true).catch(() => false);
      
      // If no current tokens, try to migrate from legacy location
      if (!tokenExists) {
        const migrated = await this.migrateLegacyTokens();
        if (!migrated) {
          process.stderr.write(`No token file found at: ${this.tokenPath}\n`);
          return false;
        }
      }

      const multiAccountTokens = await this.loadMultiAccountTokens();
      const tokens = multiAccountTokens[this.accountMode];

      if (!tokens || typeof tokens !== "object") {
        process.stderr.write(`No tokens found for ${this.accountMode} account in file: ${this.tokenPath}\n`);
        return false;
      }

      this.oauth2Client.setCredentials(tokens);
      process.stderr.write(`Loaded tokens for ${this.accountMode} account\n`);
      return true;
    } catch (error: unknown) {
      process.stderr.write(`Error loading tokens for ${this.accountMode} account: `);
      if (error instanceof Error && 'code' in error && error.code !== 'ENOENT') { 
          try { 
              await fs.unlink(this.tokenPath); 
              process.stderr.write("Removed potentially corrupted token file\n"); 
            } catch (unlinkErr) { /* ignore */ } 
      }
      return false;
    }
  }

  async refreshTokensIfNeeded(): Promise<boolean> {
    const expiryDate = this.oauth2Client.credentials.expiry_date;
    const isExpired = expiryDate
      ? Date.now() >= expiryDate - 5 * 60 * 1000 // 5 minute buffer
      : !this.oauth2Client.credentials.access_token; // No token means we need one

    if (isExpired && this.oauth2Client.credentials.refresh_token) {
      if (process.env.NODE_ENV !== 'test') {
        process.stderr.write(`Auth token expired or nearing expiry for ${this.accountMode} account, refreshing...\n`);
      }
      try {
        const response = await this.oauth2Client.refreshAccessToken();
        const newTokens = response.credentials;

        if (!newTokens.access_token) {
          throw new Error("Received invalid tokens during refresh");
        }
        // The 'tokens' event listener should handle saving
        this.oauth2Client.setCredentials(newTokens);
        if (process.env.NODE_ENV !== 'test') {
          process.stderr.write(`Token refreshed successfully for ${this.accountMode} account\n`);
        }
        return true;
      } catch (refreshError) {
        if (refreshError instanceof GaxiosError && refreshError.response?.data?.error === 'invalid_grant') {
            process.stderr.write(`Error refreshing auth token for ${this.accountMode} account: Invalid grant. Token likely expired or revoked. Please re-authenticate.\n`);
            return false; // Indicate failure due to invalid grant
        } else {
            // Handle other refresh errors
            process.stderr.write(`Error refreshing auth token for ${this.accountMode} account: `);
            if (refreshError instanceof Error) {
              process.stderr.write(refreshError.message);
            } else if (typeof refreshError === 'string') {
              process.stderr.write(refreshError);
            }
            process.stderr.write("\n");
            return false;
        }
      }
    } else if (!this.oauth2Client.credentials.access_token && !this.oauth2Client.credentials.refresh_token) {
        process.stderr.write(`No access or refresh token available for ${this.accountMode} account. Please re-authenticate.\n`);
        return false;
    } else {
        // Token is valid or no refresh token available
        return true;
    }
  }

  async validateTokens(accountMode?: 'normal' | 'test'): Promise<boolean> {
    // For unit tests that don't need real authentication, they should mock at the handler level
    // Integration tests always need real tokens

    const modeToValidate = accountMode || this.accountMode;
    const currentMode = this.accountMode;
    
    try {
      // Temporarily switch to the mode we want to validate if different
      if (modeToValidate !== currentMode) {
        this.accountMode = modeToValidate;
      }
      
      if (!this.oauth2Client.credentials || !this.oauth2Client.credentials.access_token) {
          // Try loading first if no credentials set
          if (!(await this.loadSavedTokens())) {
              return false; // No saved tokens to load
          }
          // Check again after loading
          if (!this.oauth2Client.credentials || !this.oauth2Client.credentials.access_token) {
              return false; // Still no token after loading
          }
      }
      
      const result = await this.refreshTokensIfNeeded();
      return result;
    } finally {
      // Always restore the original account mode
      if (modeToValidate !== currentMode) {
        this.accountMode = currentMode;
      }
    }
  }

  async saveTokens(tokens: Credentials): Promise<void> {
    try {
        const multiAccountTokens = await this.loadMultiAccountTokens();
        multiAccountTokens[this.accountMode] = tokens;
        
        await this.saveMultiAccountTokens(multiAccountTokens);
        this.oauth2Client.setCredentials(tokens);
        process.stderr.write(`Tokens saved successfully for ${this.accountMode} account to: ${this.tokenPath}\n`);
    } catch (error: unknown) {
        process.stderr.write(`Error saving tokens for ${this.accountMode} account: ${error}\n`);
        throw error;
    }
  }

  async clearTokens(): Promise<void> {
    try {
      this.oauth2Client.setCredentials({}); // Clear in memory
      
      const multiAccountTokens = await this.loadMultiAccountTokens();
      delete multiAccountTokens[this.accountMode];
      
      // If no accounts left, delete the entire file
      if (Object.keys(multiAccountTokens).length === 0) {
        await fs.unlink(this.tokenPath);
        process.stderr.write(`All tokens cleared, file deleted\n`);
      } else {
        await this.saveMultiAccountTokens(multiAccountTokens);
        process.stderr.write(`Tokens cleared for ${this.accountMode} account\n`);
      }
    } catch (error: unknown) {
      if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
        // File already gone, which is fine
        process.stderr.write("Token file already deleted\n");
      } else {
        process.stderr.write(`Error clearing tokens for ${this.accountMode} account: ${error}\n`);
        // Don't re-throw, clearing is best-effort
      }
    }
  }

  // Method to list available accounts
  async listAvailableAccounts(): Promise<string[]> {
    try {
      const multiAccountTokens = await this.loadMultiAccountTokens();
      return Object.keys(multiAccountTokens);
    } catch (error) {
      return [];
    }
  }

  // Method to switch to a different account (useful for runtime switching)
  async switchAccount(newMode: 'normal' | 'test'): Promise<boolean> {
    this.accountMode = newMode;
    return this.loadSavedTokens();
  }
} 
```

--------------------------------------------------------------------------------
/scripts/test-docker.sh:
--------------------------------------------------------------------------------

```bash
#!/bin/bash

# Docker Testing Script for Google Calendar MCP Server
# Tests Docker container functionality including stdio/HTTP modes and calendar integration

set -e  # Exit on any error

# Configuration
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
TEST_TIMEOUT=120
HTTP_PORT=3001  # Use different port to avoid conflicts
CONTAINER_NAME="test-calendar-mcp"
CONTAINER_NAME_STDIO="test-calendar-mcp-stdio"
CONTAINER_NAME_HTTP="test-calendar-mcp-http"

# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'  
NC='\033[0m' # No Color

# Logging functions
log_info() {
    echo -e "${BLUE}ℹ️  $1${NC}"
}

log_success() {
    echo -e "${GREEN}✅ $1${NC}"
}

log_warn() {
    echo -e "${YELLOW}⚠️  $1${NC}"
}

log_error() {
    echo -e "${RED}❌ $1${NC}"
}

# Cleanup function
cleanup() {
    log_info "Cleaning up test containers..."
    
    # Stop and remove containers
    docker stop "$CONTAINER_NAME_STDIO" 2>/dev/null || true
    docker stop "$CONTAINER_NAME_HTTP" 2>/dev/null || true
    docker rm "$CONTAINER_NAME_STDIO" 2>/dev/null || true
    docker rm "$CONTAINER_NAME_HTTP" 2>/dev/null || true
    
    # Remove test network if it exists
    docker network rm mcp-test-network 2>/dev/null || true
    
    log_info "Cleanup completed"
}

# Trap to ensure cleanup on exit
trap cleanup EXIT

# Check prerequisites
check_prerequisites() {
    log_info "Checking prerequisites..."
    
    # Check Docker
    if ! command -v docker &> /dev/null; then
        log_error "Docker is not installed"
        exit 1
    fi
    
    # Check docker compose (modern command)
    if ! docker compose version &> /dev/null; then
        log_error "Docker Compose plugin is not installed"
        log_info "Please install Docker Compose plugin: https://docs.docker.com/compose/install/"
        exit 1
    fi
    
    # Check OAuth credentials
    if [[ ! -f "$PROJECT_ROOT/gcp-oauth.keys.json" ]]; then
        log_error "OAuth credentials file not found: gcp-oauth.keys.json"
        log_info "Please download OAuth credentials from Google Cloud Console"
        exit 1
    fi
    
    # Check environment variables for integration tests
    if [[ -z "$TEST_CALENDAR_ID" ]]; then
        log_warn "TEST_CALENDAR_ID not set - integration tests will be limited"
    fi
    
    log_success "Prerequisites check passed"
}

# Build Docker image
build_image() {
    log_info "Building Docker image..."
    
    cd "$PROJECT_ROOT"
    docker build -t google-calendar-mcp:test .
    
    log_success "Docker image built successfully"
}

# Test container startup and basic functionality
test_container_health() {
    local mode=$1
    local container_name=$2
    
    log_info "Testing $mode mode container health..."
    
    case $mode in
        "stdio")
            # Start stdio container with shell to keep it running
            docker run -d \
                --name "$container_name" \
                -v "$PROJECT_ROOT/gcp-oauth.keys.json:/app/gcp-oauth.keys.json:ro" \
                -v mcp-test-tokens:/home/nodejs/.config/google-calendar-mcp \
                -e NODE_ENV=test \
                -e TRANSPORT=stdio \
                --entrypoint=/bin/sh \
                google-calendar-mcp:test -c "while true; do sleep 30; done"
            ;;
        "http")
            # Start HTTP container
            docker run -d \
                --name "$container_name" \
                -p "$HTTP_PORT:3000" \
                -v "$PROJECT_ROOT/gcp-oauth.keys.json:/app/gcp-oauth.keys.json:ro" \
                -v mcp-test-tokens:/home/nodejs/.config/google-calendar-mcp \
                -e NODE_ENV=test \
                -e TRANSPORT=http \
                -e HOST=0.0.0.0 \
                -e PORT=3000 \
                google-calendar-mcp:test
            ;;
    esac
    
    # Wait for container to be ready
    log_info "Waiting for container to be ready..."
    sleep 5
    
    # Check if container is running
    if ! docker ps | grep -q "$container_name"; then
        log_error "Container $container_name failed to start"
        docker logs "$container_name"
        return 1
    fi
    
    log_success "$mode mode container is healthy"
}

# Test HTTP endpoint accessibility
test_http_endpoints() {
    log_info "Testing HTTP endpoints..."
    
    # Wait for HTTP server to be ready
    for i in {1..30}; do
        if curl -s "http://localhost:$HTTP_PORT/health" > /dev/null 2>&1; then
            break
        fi
        sleep 1
        if [[ $i -eq 30 ]]; then
            log_error "HTTP server failed to start within 30 seconds"
            docker logs "$CONTAINER_NAME_HTTP"
            return 1
        fi
    done
    
    # Test health endpoint
    if ! curl -s "http://localhost:$HTTP_PORT/health" | grep -q "healthy"; then
        log_error "Health endpoint not responding correctly"
        return 1
    fi
    
    # Test info endpoint
    if ! curl -s "http://localhost:$HTTP_PORT/info" > /dev/null; then
        log_error "Info endpoint not accessible"
        return 1
    fi
    
    log_success "HTTP endpoints are accessible"
}

# Test MCP tool listing via Docker
test_mcp_tools() {
    local container_name=$1
    
    log_info "Testing MCP tool availability in container..."
    
    # Create a simple Node.js script to test MCP connection
    cat > "$PROJECT_ROOT/test-mcp-connection.js" << 'EOF'
const { Client } = require("@modelcontextprotocol/sdk/client/index.js");
const { StdioClientTransport } = require("@modelcontextprotocol/sdk/client/stdio.js");
const { spawn } = require('child_process');

async function testMCPConnection() {
    const client = new Client({
        name: "docker-test-client",
        version: "1.0.0"
    }, {
        capabilities: { tools: {} }
    });

    try {
        // For stdio mode, exec into the container
        const transport = new StdioClientTransport({
            command: 'docker',
            args: ['exec', '-i', process.argv[2], 'npm', 'start'],
            env: { ...process.env, NODE_ENV: 'test' }
        });

        await client.connect(transport);
        const tools = await client.listTools();
        
        console.log(`✅ Successfully connected to MCP server in container`);
        console.log(`📋 Available tools: ${tools.tools.length}`);
        
        // Test a simple tool call (list-calendars doesn't require auth setup)
        try {
            const result = await client.callTool({
                name: 'list-calendars',
                arguments: {}
            });
            console.log(`🔧 Tool execution test: SUCCESS`);
        } catch (toolError) {
            // Expected for auth issues in test environment
            console.log(`🔧 Tool execution test: ${toolError.message.includes('auth') ? 'AUTH_REQUIRED (expected)' : 'FAILED'}`);
        }

        await client.close();
        process.exit(0);
    } catch (error) {
        console.error(`❌ MCP connection failed:`, error.message);
        process.exit(1);
    }
}

if (process.argv.length < 3) {
    console.error('Usage: node test-mcp-connection.js <container-name>');
    process.exit(1);
}

testMCPConnection();
EOF

    # Run the MCP connection test
    if node "$PROJECT_ROOT/test-mcp-connection.js" "$container_name"; then
        log_success "MCP tools accessible via Docker"
    else
        log_error "MCP tools test failed"
        return 1
    fi
    
    # Cleanup test file
    rm -f "$PROJECT_ROOT/test-mcp-connection.js"
}

# Test Docker Compose integration (simplified setup)
test_docker_compose() {
    log_info "Testing Docker Compose integration..."
    
    cd "$PROJECT_ROOT"
    
    # Test stdio mode (default)
    docker compose up -d
    sleep 5
    
    if ! docker compose ps | grep -q "calendar-mcp.*Up"; then
        log_error "Docker Compose stdio mode failed"
        docker compose logs calendar-mcp
        return 1
    fi
    
    docker compose down
    
    # Test HTTP mode by temporarily modifying compose file
    log_info "Testing HTTP mode (requires manual setup)..."
    log_warn "HTTP mode test skipped - requires manual docker-compose.yml edit"
    log_info "To test HTTP mode manually:"
    log_info "1. Uncomment ports and environment sections in docker-compose.yml"
    log_info "2. Run: docker compose up -d"
    log_info "3. Test: curl http://localhost:3000/health"
    
    log_success "Docker Compose integration working"
}

# Test authentication setup (if credentials available)
test_auth_setup() {
    log_info "Testing authentication setup in container..."
    
    # This will test if the auth command works (may fail due to interactive nature)
    if docker exec "$CONTAINER_NAME_STDIO" npm run auth --help > /dev/null 2>&1; then
        log_success "Auth command accessible in container"
    else
        log_warn "Auth command test inconclusive (expected for non-interactive environment)"
    fi
    
    # Test token file paths are accessible
    if docker exec "$CONTAINER_NAME_STDIO" ls -la /home/nodejs/.config/google-calendar-mcp/ > /dev/null 2>&1; then
        log_success "Token storage directory accessible"
    else
        log_error "Token storage directory not accessible"
        return 1
    fi
}

# Run integration tests against Docker container (if environment supports it)
test_calendar_integration() {
    if [[ -z "$TEST_CALENDAR_ID" || -z "$CLAUDE_API_KEY" ]]; then
        log_warn "Skipping calendar integration tests (missing TEST_CALENDAR_ID or CLAUDE_API_KEY)"
        return 0
    fi
    
    log_info "Running calendar integration tests against Docker container..."
    
    # Use existing integration test but point it to Docker container
    cd "$PROJECT_ROOT"
    
    # Set environment to use Docker container
    export DOCKER_CONTAINER_NAME="$CONTAINER_NAME_STDIO"
    export USE_DOCKER_CONTAINER=true
    
    # Run subset of integration tests
    if timeout $TEST_TIMEOUT npm run test:integration -- --reporter=verbose --run docker 2>/dev/null; then
        log_success "Calendar integration tests passed"
    else
        log_warn "Calendar integration tests incomplete (may require manual auth)"
    fi
}

# Performance testing
test_performance() {
    log_info "Running basic performance tests..."
    
    # Test HTTP response times
    local avg_response_time
    avg_response_time=$(curl -o /dev/null -s -w '%{time_total}\n' \
        "http://localhost:$HTTP_PORT/health" \
        "http://localhost:$HTTP_PORT/health" \
        "http://localhost:$HTTP_PORT/health" | \
        awk '{sum+=$1} END {print sum/NR}')
    
    echo "Average HTTP response time: ${avg_response_time}s"
    
    # Test container resource usage
    local memory_usage
    memory_usage=$(docker stats --no-stream --format "{{.MemUsage}}" "$CONTAINER_NAME_HTTP" | cut -d'/' -f1)
    echo "Container memory usage: $memory_usage"
    
    log_success "Performance tests completed"
}

# Main test execution
main() {
    log_info "🐳 Starting Docker integration tests for Google Calendar MCP Server"
    
    # Cleanup any existing test containers
    cleanup
    
    # Run test suite
    check_prerequisites
    build_image
    
    # Test stdio mode
    test_container_health "stdio" "$CONTAINER_NAME_STDIO"
    test_mcp_tools "$CONTAINER_NAME_STDIO"
    test_auth_setup
    
    # Test HTTP mode  
    test_container_health "http" "$CONTAINER_NAME_HTTP"
    test_http_endpoints
    test_performance
    
    # Test Docker Compose integration
    test_docker_compose  
    
    # Test calendar integration (if environment supports it)
    test_calendar_integration
    
    log_success "🎉 All Docker tests completed successfully!"
    
    # Print summary
    echo ""
    echo "📋 Test Summary:"
    echo "   ✅ Container Health (stdio & HTTP)"
    echo "   ✅ MCP Tool Accessibility" 
    echo "   ✅ HTTP Endpoint Testing"
    echo "   ✅ Docker Compose Integration"
    echo "   ✅ Authentication Setup"
    echo "   ✅ Performance Metrics"
    if [[ -n "$TEST_CALENDAR_ID" && -n "$CLAUDE_API_KEY" ]]; then
        echo "   ✅ Calendar Integration"
    else
        echo "   ⚠️  Calendar Integration (skipped - missing env vars)"
    fi
}

# Handle command line arguments
case "${1:-}" in
    --help|-h)
        echo "Docker Testing Script for Google Calendar MCP Server"
        echo ""
        echo "Usage: $0 [options]"
        echo ""
        echo "Options:"
        echo "  --help, -h          Show this help message"
        echo "  --quick            Run only essential tests (faster)"
        echo "  --integration      Run full integration tests (requires auth)"
        echo ""
        echo "Environment Variables:"
        echo "  TEST_CALENDAR_ID    Calendar ID for integration tests"
        echo "  CLAUDE_API_KEY      Anthropic API key for Claude integration"
        echo "  INVITEE_1, INVITEE_2  Email addresses for event testing"
        echo ""
        echo "Prerequisites:"
        echo "  - Docker and Docker Compose plugin installed"
        echo "  - gcp-oauth.keys.json file in project root"
        echo "  - For integration tests: authenticated test account"
        exit 0
        ;;
    --quick)
        log_info "Running quick Docker tests only..."
        check_prerequisites
        build_image
        test_container_health "stdio" "$CONTAINER_NAME_STDIO"
        test_container_health "http" "$CONTAINER_NAME_HTTP"  
        test_http_endpoints
        test_docker_compose
        log_success "Quick tests completed!"
        ;;
    --integration)
        if [[ -z "$TEST_CALENDAR_ID" ]]; then
            log_error "--integration requires TEST_CALENDAR_ID environment variable"
            exit 1
        fi
        main
        ;;
    "")
        main
        ;;
    *)
        log_error "Unknown option: $1"
        echo "Use --help for usage information"
        exit 1
        ;;
esac
```

--------------------------------------------------------------------------------
/src/tests/unit/schemas/validators.test.ts:
--------------------------------------------------------------------------------

```typescript
import { describe, it, expect } from 'vitest';
import { ToolSchemas } from '../../../tools/registry.js';

// Use the unified schemas from registry  
const UpdateEventArgumentsSchema = ToolSchemas['update-event'];
const ListEventsArgumentsSchema = ToolSchemas['list-events'];

// Helper to generate a future date string in timezone-naive format
function getFutureDateString(daysFromNow: number = 365): string {
  const futureDate = new Date();
  futureDate.setDate(futureDate.getDate() + daysFromNow);
  // Format as timezone-naive ISO string (no timezone suffix)
  return futureDate.toISOString().split('.')[0];
}

describe('UpdateEventArgumentsSchema with Recurring Event Support', () => {
  describe('Basic Validation', () => {
    it('should validate basic required fields', () => {
      const validArgs = {
        calendarId: 'primary',
        eventId: 'event123',
        timeZone: 'America/Los_Angeles'
      };

      const result = UpdateEventArgumentsSchema.parse(validArgs);
      expect(result.modificationScope).toBeUndefined(); // optional with no default
      expect(result.calendarId).toBe('primary');
      expect(result.eventId).toBe('event123');
      expect(result.timeZone).toBe('America/Los_Angeles');
    });

    it('should reject missing required fields', () => {
      const invalidArgs = {
        calendarId: 'primary',
        // missing eventId and timeZone
      };

      expect(() => UpdateEventArgumentsSchema.parse(invalidArgs)).toThrow();
    });

    it('should validate optional fields when provided', () => {
      const validArgs = {
        calendarId: 'primary',
        eventId: 'event123',
        timeZone: 'America/Los_Angeles',
        summary: 'Updated Meeting',
        description: 'Updated description',
        location: 'New Location',
        colorId: '9',
        start: '2024-06-15T10:00:00',
        end: '2024-06-15T11:00:00'
      };

      const result = UpdateEventArgumentsSchema.parse(validArgs);
      expect(result.summary).toBe('Updated Meeting');
      expect(result.description).toBe('Updated description');
      expect(result.location).toBe('New Location');
      expect(result.colorId).toBe('9');
    });
  });

  describe('Modification Scope Validation', () => {
    it('should leave modificationScope undefined when not provided', () => {
      const args = {
        calendarId: 'primary',
        eventId: 'event123',
        timeZone: 'America/Los_Angeles'
      };

      const result = UpdateEventArgumentsSchema.parse(args);
      expect(result.modificationScope).toBeUndefined();
    });

    it('should accept valid modificationScope values', () => {
      const validScopes = ['thisEventOnly', 'all', 'thisAndFollowing'] as const;

      validScopes.forEach(scope => {
        const args: any = {
          calendarId: 'primary',
          eventId: 'event123',
          timeZone: 'America/Los_Angeles',
          modificationScope: scope
        };

        // Add required fields for each scope
        if (scope === 'thisEventOnly') {
          args.originalStartTime = '2024-06-15T10:00:00';
        } else if (scope === 'thisAndFollowing') {
          args.futureStartDate = getFutureDateString(90); // 90 days from now
        }

        const result = UpdateEventArgumentsSchema.parse(args);
        expect(result.modificationScope).toBe(scope);
      });
    });

    it('should reject invalid modificationScope values', () => {
      const args = {
        calendarId: 'primary',
        eventId: 'event123',
        timeZone: 'America/Los_Angeles',
        modificationScope: 'invalid'
      };

      expect(() => UpdateEventArgumentsSchema.parse(args)).toThrow();
    });
  });

  describe('Single Instance Scope Validation', () => {
    it('should require originalStartTime when modificationScope is "thisEventOnly"', () => {
      const args = {
        calendarId: 'primary',
        eventId: 'event123',
        timeZone: 'America/Los_Angeles',
        modificationScope: 'thisEventOnly'
        // missing originalStartTime
      };

      expect(() => UpdateEventArgumentsSchema.parse(args)).toThrow(
        /originalStartTime is required when modificationScope is 'thisEventOnly'/
      );
    });

    it('should accept valid originalStartTime for thisEventOnly scope', () => {
      const args = {
        calendarId: 'primary',
        eventId: 'event123',
        timeZone: 'America/Los_Angeles',
        modificationScope: 'thisEventOnly',
        originalStartTime: '2024-06-15T10:00:00'
      };

      const result = UpdateEventArgumentsSchema.parse(args);
      expect(result.modificationScope).toBe('thisEventOnly');
      expect(result.originalStartTime).toBe('2024-06-15T10:00:00');
    });

    it('should reject invalid originalStartTime format', () => {
      const args = {
        calendarId: 'primary',
        eventId: 'event123',
        timeZone: 'America/Los_Angeles',
        modificationScope: 'thisEventOnly',
        originalStartTime: '2024-06-15 10:00:00' // invalid format
      };

      expect(() => UpdateEventArgumentsSchema.parse(args)).toThrow();
    });

    it('should accept originalStartTime without timezone designator', () => {
      const args = {
        calendarId: 'primary',
        eventId: 'event123',
        timeZone: 'America/Los_Angeles',
        modificationScope: 'thisEventOnly',
        originalStartTime: '2024-06-15T10:00:00' // timezone-naive format (expected)
      };

      expect(() => UpdateEventArgumentsSchema.parse(args)).not.toThrow();
    });
  });

  describe('Future Instances Scope Validation', () => {
    it('should require futureStartDate when modificationScope is "thisAndFollowing"', () => {
      const args = {
        calendarId: 'primary',
        eventId: 'event123',
        timeZone: 'America/Los_Angeles',
        modificationScope: 'thisAndFollowing'
        // missing futureStartDate
      };

      expect(() => UpdateEventArgumentsSchema.parse(args)).toThrow(
        /futureStartDate is required when modificationScope is 'thisAndFollowing'/
      );
    });

    it('should accept valid futureStartDate for thisAndFollowing scope', () => {
      const futureDateString = getFutureDateString(30); // 30 days from now

      const args = {
        calendarId: 'primary',
        eventId: 'event123',
        timeZone: 'America/Los_Angeles',
        modificationScope: 'thisAndFollowing',
        futureStartDate: futureDateString
      };

      const result = UpdateEventArgumentsSchema.parse(args);
      expect(result.modificationScope).toBe('thisAndFollowing');
      expect(result.futureStartDate).toBe(futureDateString);
    });

    it('should reject futureStartDate in the past', () => {
      const pastDate = new Date();
      pastDate.setFullYear(pastDate.getFullYear() - 1);
      // Format as ISO string without milliseconds
      const pastDateString = pastDate.toISOString().split('.')[0] + 'Z';

      const args = {
        calendarId: 'primary',
        eventId: 'event123',
        timeZone: 'America/Los_Angeles',
        modificationScope: 'thisAndFollowing',
        futureStartDate: pastDateString
      };

      expect(() => UpdateEventArgumentsSchema.parse(args)).toThrow(
        /futureStartDate must be in the future/
      );
    });

    it('should reject invalid futureStartDate format', () => {
      const args = {
        calendarId: 'primary',
        eventId: 'event123',
        timeZone: 'America/Los_Angeles',
        modificationScope: 'thisAndFollowing',
        futureStartDate: '2024-12-31 10:00:00' // invalid format
      };

      expect(() => UpdateEventArgumentsSchema.parse(args)).toThrow();
    });
  });

  describe('Datetime Format Validation', () => {
    const validDatetimes = [
      '2024-06-15T10:00:00',      // timezone-naive (preferred)
      '2024-12-31T23:59:59',      // timezone-naive (preferred)
      '2024-01-01T00:00:00',      // timezone-naive (preferred)
      '2024-06-15T10:00:00Z',     // timezone-aware (accepted)
      '2024-06-15T10:00:00-07:00', // timezone-aware (accepted)
      '2024-06-15T10:00:00+05:30'  // timezone-aware (accepted)
    ];

    const invalidDatetimes = [
      '2024-06-15 10:00:00',     // space instead of T
      '24-06-15T10:00:00',       // short year
      '2024-6-15T10:00:00',      // single digit month
      '2024-06-15T10:00'         // missing seconds
    ];

    validDatetimes.forEach(datetime => {
      it(`should accept valid datetime format: ${datetime}`, () => {
        const args = {
          calendarId: 'primary',
          eventId: 'event123',
          timeZone: 'America/Los_Angeles',
          start: datetime,
          end: datetime
        };

        expect(() => UpdateEventArgumentsSchema.parse(args)).not.toThrow();
      });
    });

    invalidDatetimes.forEach(datetime => {
      it(`should reject invalid datetime format: ${datetime}`, () => {
        const args = {
          calendarId: 'primary',
          eventId: 'event123',
          timeZone: 'America/Los_Angeles',
          start: datetime
        };

        expect(() => UpdateEventArgumentsSchema.parse(args)).toThrow();
      });
    });
  });

  describe('Complex Scenarios', () => {
    it('should validate complete update with all fields', () => {
      const args = {
        calendarId: 'primary',
        eventId: 'event123',
        timeZone: 'America/Los_Angeles',
        modificationScope: 'thisAndFollowing',
        futureStartDate: getFutureDateString(60), // 60 days from now
        summary: 'Updated Meeting',
        description: 'Updated description',
        location: 'New Conference Room',
        start: '2024-06-15T10:00:00',
        end: '2024-06-15T11:00:00',
        colorId: '9',
        attendees: [
          { email: '[email protected]' },
          { email: '[email protected]' }
        ],
        reminders: {
          useDefault: false,
          overrides: [
            { method: 'email', minutes: 1440 },
            { method: 'popup', minutes: 10 }
          ]
        },
        recurrence: ['RRULE:FREQ=WEEKLY;BYDAY=MO']
      };

      const result = UpdateEventArgumentsSchema.parse(args);
      expect(result).toMatchObject(args);
    });

    it('should not require conditional fields for "all" scope', () => {
      const args = {
        calendarId: 'primary',
        eventId: 'event123',
        timeZone: 'America/Los_Angeles',
        modificationScope: 'all',
        summary: 'Updated Meeting'
        // no originalStartTime or futureStartDate required
      };

      expect(() => UpdateEventArgumentsSchema.parse(args)).not.toThrow();
    });

    it('should allow optional conditional fields when not required', () => {
      const args = {
        calendarId: 'primary',
        eventId: 'event123',
        timeZone: 'America/Los_Angeles',
        modificationScope: 'all',
        originalStartTime: '2024-06-15T10:00:00', // optional for 'all' scope
        summary: 'Updated Meeting'
      };

      const result = UpdateEventArgumentsSchema.parse(args);
      expect(result.originalStartTime).toBe('2024-06-15T10:00:00');
    });
  });

  describe('Backward Compatibility', () => {
    it('should maintain compatibility with existing update calls', () => {
      // Existing call format without new parameters
      const legacyArgs = {
        calendarId: 'primary',
        eventId: 'event123',
        timeZone: 'America/Los_Angeles',
        summary: 'Updated Meeting',
        location: 'Conference Room A'
      };

      const result = UpdateEventArgumentsSchema.parse(legacyArgs);
      expect(result.modificationScope).toBeUndefined(); // optional with no default
      expect(result.summary).toBe('Updated Meeting');
      expect(result.location).toBe('Conference Room A');
    });
  });
});

describe('ListEventsArgumentsSchema JSON String Handling', () => {
  it('should parse JSON string calendarId into array', () => {
    const input = {
      calendarId: '["primary", "[email protected]"]',
      timeMin: '2024-01-01T00:00:00Z',
      timeMax: '2024-01-02T00:00:00Z'
    };

    const result = ListEventsArgumentsSchema.parse(input);
    // The new schema keeps JSON strings as strings (they are parsed in the handler)
    expect(result.calendarId).toBe('["primary", "[email protected]"]');
  });

  it('should handle regular string calendarId', () => {
    const input = {
      calendarId: 'primary',
      timeMin: '2024-01-01T00:00:00Z',
      timeMax: '2024-01-02T00:00:00Z'
    };

    const result = ListEventsArgumentsSchema.parse(input);
    expect(result.calendarId).toBe('primary');
  });

  it('should handle regular array calendarId', () => {
    // Arrays are now supported via preprocessing
    const input = {
      calendarId: ['primary', '[email protected]'],
      timeMin: '2024-01-01T00:00:00Z',
      timeMax: '2024-01-02T00:00:00Z'
    };

    // Arrays are now kept as arrays (not transformed to JSON strings)
    const result = ListEventsArgumentsSchema.parse(input);
    expect(result.calendarId).toEqual(['primary', '[email protected]']);
  });

  it('should reject invalid JSON string', () => {
    // Invalid JSON strings are accepted by the schema but will fail in the handler
    const input = {
      calendarId: '["primary", invalid]',
      timeMin: '2024-01-01T00:00:00Z',
      timeMax: '2024-01-02T00:00:00Z'
    };

    // The schema accepts any string - validation happens in the handler
    const result = ListEventsArgumentsSchema.parse(input);
    expect(result.calendarId).toBe('["primary", invalid]');
  });

  it('should reject JSON string with non-string elements', () => {
    // Schema accepts any string - validation happens in the handler
    const input = {
      calendarId: '["primary", 123]',
      timeMin: '2024-01-01T00:00:00Z',
      timeMax: '2024-01-02T00:00:00Z'
    };

    // The schema accepts any string - validation happens in the handler
    const result = ListEventsArgumentsSchema.parse(input);
    expect(result.calendarId).toBe('["primary", 123]');
  });
}); 
```

--------------------------------------------------------------------------------
/src/handlers/core/BaseToolHandler.ts:
--------------------------------------------------------------------------------

```typescript
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
import { OAuth2Client } from "google-auth-library";
import { GaxiosError } from 'gaxios';
import { calendar_v3, google } from "googleapis";
import { getCredentialsProjectId } from "../../auth/utils.js";


export abstract class BaseToolHandler {
    abstract runTool(args: any, oauth2Client: OAuth2Client): Promise<CallToolResult>;

    protected handleGoogleApiError(error: unknown): never {
        if (error instanceof GaxiosError) {
            const status = error.response?.status;
            const errorData = error.response?.data;

            // Handle specific Google API errors with appropriate MCP error codes
            if (errorData?.error === 'invalid_grant') {
                throw new McpError(
                    ErrorCode.InvalidRequest,
                    'Authentication token is invalid or expired. Please re-run the authentication process (e.g., `npm run auth`).'
                );
            }

            if (status === 400) {
                // Extract detailed error information for Bad Request
                const errorMessage = errorData?.error?.message;
                const errorDetails = errorData?.error?.errors?.map((e: any) =>
                    `${e.message || e.reason}${e.location ? ` (${e.location})` : ''}`
                ).join('; ');

                // Also include raw error data for debugging if details are missing
                let fullMessage: string;
                if (errorDetails) {
                    fullMessage = `Bad Request: ${errorMessage || 'Invalid request parameters'}. Details: ${errorDetails}`;
                } else if (errorMessage) {
                    fullMessage = `Bad Request: ${errorMessage}`;
                } else {
                    // Include stringified error data for debugging
                    const errorStr = JSON.stringify(errorData, null, 2);
                    fullMessage = `Bad Request: Invalid request parameters. Raw error: ${errorStr}`;
                }

                throw new McpError(
                    ErrorCode.InvalidRequest,
                    fullMessage
                );
            }

            if (status === 403) {
                throw new McpError(
                    ErrorCode.InvalidRequest,
                    `Access denied: ${errorData?.error?.message || 'Insufficient permissions'}`
                );
            }

            if (status === 404) {
                throw new McpError(
                    ErrorCode.InvalidRequest,
                    `Resource not found: ${errorData?.error?.message || 'The requested calendar or event does not exist'}`
                );
            }

            if (status === 429) {
                const errorMessage = errorData?.error?.message || '';

                // Provide specific guidance for quota-related rate limits
                if (errorMessage.includes('User Rate Limit Exceeded')) {
                    throw new McpError(
                        ErrorCode.InvalidRequest,
                        `Rate limit exceeded. This may be due to missing quota project configuration.

Ensure your OAuth credentials include project_id information:
1. Check that your gcp-oauth.keys.json file contains project_id
2. Re-download credentials from Google Cloud Console if needed
3. The file should have format: {"installed": {"project_id": "your-project-id", ...}}

Original error: ${errorMessage}`
                    );
                }

                throw new McpError(
                    ErrorCode.InternalError,
                    `Rate limit exceeded. Please try again later. ${errorMessage}`
                );
            }

            if (status && status >= 500) {
                throw new McpError(
                    ErrorCode.InternalError,
                    `Google API server error: ${errorData?.error?.message || error.message}`
                );
            }

            // Generic Google API error with detailed information
            const errorMessage = errorData?.error?.message || error.message;
            const errorDetails = errorData?.error?.errors?.map((e: any) =>
                `${e.message || e.reason}${e.location ? ` (${e.location})` : ''}`
            ).join('; ');

            const fullMessage = errorDetails
                ? `Google API error: ${errorMessage}. Details: ${errorDetails}`
                : `Google API error: ${errorMessage}`;

            throw new McpError(
                ErrorCode.InvalidRequest,
                fullMessage
            );
        }

        // Handle non-Google API errors
        if (error instanceof Error) {
            throw new McpError(
                ErrorCode.InternalError,
                `Internal error: ${error.message}`
            );
        }

        throw new McpError(
            ErrorCode.InternalError,
            'An unknown error occurred'
        );
    }

    protected getCalendar(auth: OAuth2Client): calendar_v3.Calendar {
        // Try to get project ID from credentials file for quota project header
        const quotaProjectId = getCredentialsProjectId();

        const config: any = {
            version: 'v3',
            auth,
            timeout: 3000 // 3 second timeout for API calls
        };

        // Add quota project ID if available
        if (quotaProjectId) {
            config.quotaProjectId = quotaProjectId;
        }

        return google.calendar(config);
    }

    protected async withTimeout<T>(promise: Promise<T>, timeoutMs: number = 30000): Promise<T> {
        const timeoutPromise = new Promise<never>((_, reject) => {
            setTimeout(() => reject(new Error(`Operation timed out after ${timeoutMs}ms`)), timeoutMs);
        });

        return Promise.race([promise, timeoutPromise]);
    }

    /**
     * Gets calendar details including default timezone
     * @param client OAuth2Client
     * @param calendarId Calendar ID to fetch details for
     * @returns Calendar details with timezone
     */
    protected async getCalendarDetails(client: OAuth2Client, calendarId: string): Promise<calendar_v3.Schema$CalendarListEntry> {
        try {
            const calendar = this.getCalendar(client);
            const response = await calendar.calendarList.get({ calendarId });
            if (!response.data) {
                throw new Error(`Calendar ${calendarId} not found`);
            }
            return response.data;
        } catch (error) {
            throw this.handleGoogleApiError(error);
        }
    }

    /**
     * Gets the default timezone for a calendar, falling back to UTC if not available
     * @param client OAuth2Client
     * @param calendarId Calendar ID
     * @returns Timezone string (IANA format)
     */
    protected async getCalendarTimezone(client: OAuth2Client, calendarId: string): Promise<string> {
        try {
            const calendarDetails = await this.getCalendarDetails(client, calendarId);
            return calendarDetails.timeZone || 'UTC';
        } catch (error) {
            // If we can't get calendar details, fall back to UTC
            return 'UTC';
        }
    }

    /**
     * Resolves calendar name to calendar ID. If the input is already an ID, returns it unchanged.
     * Supports both exact and case-insensitive name matching.
     *
     * Per Google Calendar API documentation:
     * - Calendar IDs are typically email addresses (e.g., "[email protected]") or "primary" keyword
     * - Calendar names are stored in "summary" field (calendar title) and "summaryOverride" field (user's personal override)
     *
     * Matching priority (user's personal override name takes precedence):
     * 1. Exact match on summaryOverride
     * 2. Case-insensitive match on summaryOverride
     * 3. Exact match on summary
     * 4. Case-insensitive match on summary
     *
     * This ensures if a user has set a personal override, it's always checked first (both exact and fuzzy),
     * before falling back to the calendar's actual title.
     *
     * @param client OAuth2Client
     * @param nameOrId Calendar name (summary/summaryOverride) or ID
     * @returns Calendar ID
     * @throws McpError if calendar name cannot be resolved
     */
    protected async resolveCalendarId(client: OAuth2Client, nameOrId: string): Promise<string> {
        // If it looks like an ID (contains @ or is 'primary'), return as-is
        if (nameOrId === 'primary' || nameOrId.includes('@')) {
            return nameOrId;
        }

        // Try to resolve as a calendar name by fetching calendar list
        try {
            const calendar = this.getCalendar(client);
            const response = await calendar.calendarList.list();
            const calendars = response.data.items || [];

            const lowerName = nameOrId.toLowerCase();

            // Priority 1: Exact match on summaryOverride (user's personal name)
            let match = calendars.find(cal => cal.summaryOverride === nameOrId);

            // Priority 2: Case-insensitive match on summaryOverride
            if (!match) {
                match = calendars.find(cal =>
                    cal.summaryOverride?.toLowerCase() === lowerName
                );
            }

            // Priority 3: Exact match on summary (calendar's actual title)
            if (!match) {
                match = calendars.find(cal => cal.summary === nameOrId);
            }

            // Priority 4: Case-insensitive match on summary
            if (!match) {
                match = calendars.find(cal =>
                    cal.summary?.toLowerCase() === lowerName
                );
            }

            if (match && match.id) {
                return match.id;
            }

            // Calendar name not found - provide helpful error message showing both summary and override
            const availableCalendars = calendars
                .map(cal => {
                    if (cal.summaryOverride && cal.summaryOverride !== cal.summary) {
                        return `"${cal.summaryOverride}" / "${cal.summary}" (${cal.id})`;
                    }
                    return `"${cal.summary}" (${cal.id})`;
                })
                .join(', ');

            throw new McpError(
                ErrorCode.InvalidRequest,
                `Calendar "${nameOrId}" not found. Available calendars: ${availableCalendars || 'none'}. Use 'list-calendars' tool to see all available calendars.`
            );
        } catch (error) {
            if (error instanceof McpError) {
                throw error;
            }
            throw this.handleGoogleApiError(error);
        }
    }

    /**
     * Resolves multiple calendar names/IDs to calendar IDs in batch.
     * Fetches calendar list once for efficiency when resolving multiple calendars.
     * Optimized to skip API call if all inputs are already IDs.
     *
     * Matching priority (user's personal override name takes precedence):
     * 1. Exact match on summaryOverride
     * 2. Case-insensitive match on summaryOverride
     * 3. Exact match on summary
     * 4. Case-insensitive match on summary
     *
     * @param client OAuth2Client
     * @param namesOrIds Array of calendar names (summary/summaryOverride) or IDs
     * @returns Array of resolved calendar IDs
     * @throws McpError if any calendar name cannot be resolved
     */
    protected async resolveCalendarIds(client: OAuth2Client, namesOrIds: string[]): Promise<string[]> {
        // Filter out empty/whitespace-only strings
        const validInputs = namesOrIds.filter(item => item && item.trim().length > 0);

        if (validInputs.length === 0) {
            throw new McpError(
                ErrorCode.InvalidRequest,
                'At least one valid calendar identifier is required'
            );
        }

        // Quick check: if all inputs look like IDs, skip the API call
        const needsResolution = validInputs.some(item =>
            item !== 'primary' && !item.includes('@')
        );

        if (!needsResolution) {
            // All inputs are already IDs, return as-is
            return validInputs;
        }

        // Batch resolve all calendars at once by fetching calendar list once
        const calendar = this.getCalendar(client);
        const response = await calendar.calendarList.list();
        const calendars = response.data.items || [];

        // Build name-to-ID mappings for efficient lookup
        // Priority: summaryOverride takes precedence over summary
        const overrideToIdMap = new Map<string, string>();
        const summaryToIdMap = new Map<string, string>();
        const lowerOverrideToIdMap = new Map<string, string>();
        const lowerSummaryToIdMap = new Map<string, string>();

        for (const cal of calendars) {
            if (cal.id) {
                if (cal.summaryOverride) {
                    overrideToIdMap.set(cal.summaryOverride, cal.id);
                    lowerOverrideToIdMap.set(cal.summaryOverride.toLowerCase(), cal.id);
                }
                if (cal.summary) {
                    summaryToIdMap.set(cal.summary, cal.id);
                    lowerSummaryToIdMap.set(cal.summary.toLowerCase(), cal.id);
                }
            }
        }

        const resolvedIds: string[] = [];
        const errors: string[] = [];

        for (const nameOrId of validInputs) {
            // If it looks like an ID (contains @ or is 'primary'), use as-is
            if (nameOrId === 'primary' || nameOrId.includes('@')) {
                resolvedIds.push(nameOrId);
                continue;
            }

            const lowerName = nameOrId.toLowerCase();

            // Priority 1: Exact match on summaryOverride
            let id = overrideToIdMap.get(nameOrId);

            // Priority 2: Case-insensitive match on summaryOverride
            if (!id) {
                id = lowerOverrideToIdMap.get(lowerName);
            }

            // Priority 3: Exact match on summary
            if (!id) {
                id = summaryToIdMap.get(nameOrId);
            }

            // Priority 4: Case-insensitive match on summary
            if (!id) {
                id = lowerSummaryToIdMap.get(lowerName);
            }

            if (id) {
                resolvedIds.push(id);
            } else {
                errors.push(nameOrId);
            }
        }

        // If any calendars couldn't be resolved, throw error with helpful message
        if (errors.length > 0) {
            const availableCalendars = calendars
                .map(cal => {
                    if (cal.summaryOverride && cal.summaryOverride !== cal.summary) {
                        return `"${cal.summaryOverride}" / "${cal.summary}" (${cal.id})`;
                    }
                    return `"${cal.summary}" (${cal.id})`;
                })
                .join(', ');

            const errorMessage = `Calendar(s) not found: ${errors.map(e => `"${e}"`).join(', ')}. Available calendars: ${availableCalendars || 'none'}. Use 'list-calendars' tool to see all available calendars.`;

            throw new McpError(
                ErrorCode.InvalidRequest,
                errorMessage
            );
        }

        return resolvedIds;
    }

}

```

--------------------------------------------------------------------------------
/src/tests/integration/docker-integration.test.ts:
--------------------------------------------------------------------------------

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

const execAsync = promisify(exec);

/**
 * Docker Integration Tests for Google Calendar MCP Server
 * 
 * REQUIREMENTS TO RUN THESE TESTS:
 * 1. Docker and docker-compose installed
 * 2. Valid Google OAuth credentials file (gcp-oauth.keys.json)
 * 3. For full integration: Authenticated test account (npm run dev auth:test)
 * 4. Environment variables: TEST_CALENDAR_ID
 * 
 * These tests verify:
 * 1. Docker containers start and stop correctly
 * 2. MCP server is accessible within Docker
 * 3. Calendar operations work through Docker
 * 4. Both stdio and HTTP transports function
 * 5. Performance and resource usage
 */

describe('Docker Integration Tests', () => {
  let mcpClient: Client;
  let dockerProcess: ChildProcess;
  let testFactory: TestDataFactory;
  let createdEventIds: string[] = [];
  
  const TEST_CALENDAR_ID = process.env.TEST_CALENDAR_ID;
  const CONTAINER_NAME = 'test-calendar-mcp-integration';
  const HTTP_PORT = 3002; // Different port for test isolation

  beforeAll(async () => {
    console.log('🐳 Starting Docker integration tests...');
    
    if (!TEST_CALENDAR_ID) {
      throw new Error('TEST_CALENDAR_ID environment variable is required');
    }

    testFactory = new TestDataFactory();

    // Ensure any existing test containers are cleaned up
    await cleanupDockerResources();

    // Build fresh test image
    console.log('🔨 Building Docker test image...');
    await execAsync('docker build -t google-calendar-mcp:test .', { 
      cwd: process.cwd(),
      timeout: 60000 
    });

    console.log('✅ Docker image built successfully');
  }, 120000);

  afterAll(async () => {
    // Cleanup all created events
    await cleanupAllCreatedEvents();
    
    // Cleanup Docker resources
    await cleanupDockerResources();
    
    console.log('🧹 Docker integration test cleanup completed');
  }, 30000);

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

  afterEach(async () => {
    // Cleanup events created in this test
    await cleanupEvents(createdEventIds);
    createdEventIds = [];
    
    // Ensure client is closed
    if (mcpClient) {
      try {
        await mcpClient.close();
      } catch (error) {
        // Ignore close errors
      }
    }
  });

  describe('Docker Container Functionality', () => {
    it('should start stdio container and connect via MCP', async () => {
      console.log('🔌 Testing stdio container startup...');
      
      // Start container in stdio mode
      const startTime = testFactory.startTimer('docker-stdio-startup');
      
      await execAsync(`docker run -d --name ${CONTAINER_NAME} \
        -v ${process.cwd()}/gcp-oauth.keys.json:/usr/src/app/gcp-oauth.keys.json:ro \
        -v mcp-test-tokens:/home/nodejs/.config/google-calendar-mcp \
        -e NODE_ENV=test \
        -e TRANSPORT=stdio \
        --entrypoint=/bin/sh \
        google-calendar-mcp:test -c "while true; do sleep 30; done"`);
      
      testFactory.endTimer('docker-stdio-startup', startTime, true);
      
      // Verify container is running
      const { stdout } = await execAsync(`docker ps --filter name=${CONTAINER_NAME} --format "{{.Status}}"`);
      expect(stdout.trim()).toContain('Up');
      
      // Connect to MCP server in container
      mcpClient = new Client({
        name: "docker-integration-client",
        version: "1.0.0"
      }, {
        capabilities: { tools: {} }
      });

      const transport = new StdioClientTransport({
        command: 'docker',
        args: ['exec', '-i', CONTAINER_NAME, 'npm', 'start'],
        env: { ...process.env, NODE_ENV: 'test' }
      });
      
      const connectStartTime = testFactory.startTimer('mcp-connection');
      await mcpClient.connect(transport);
      testFactory.endTimer('mcp-connection', connectStartTime, true);
      
      // Test basic functionality
      const tools = await mcpClient.listTools();
      expect(tools.tools.length).toBeGreaterThan(0);
      
      // Find expected tools
      const expectedTools = ['list-calendars', 'create-event', 'list-events'];
      expectedTools.forEach(toolName => {
        const tool = tools.tools.find(t => t.name === toolName);
        expect(tool).toBeDefined();
      });
      
      console.log(`✅ Connected to MCP server in Docker container (${tools.tools.length} tools available)`);
      
      // Cleanup
      await mcpClient.close();
      await execAsync(`docker stop ${CONTAINER_NAME} && docker rm ${CONTAINER_NAME}`);
    }, 60000);

    it('should start HTTP container and serve endpoints', async () => {
      console.log('🌐 Testing HTTP container startup...');
      
      const startTime = testFactory.startTimer('docker-http-startup');
      
      // Start container in HTTP mode
      await execAsync(`docker run -d --name ${CONTAINER_NAME}-http \
        -p ${HTTP_PORT}:3000 \
        -v ${process.cwd()}/gcp-oauth.keys.json:/usr/src/app/gcp-oauth.keys.json:ro \
        -v mcp-test-tokens:/home/nodejs/.config/google-calendar-mcp \
        -e NODE_ENV=test \
        -e TRANSPORT=http \
        -e HOST=0.0.0.0 \
        -e PORT=3000 \
        google-calendar-mcp:test`);
      
      // Wait for HTTP server to be ready
      let serverReady = false;
      for (let i = 0; i < 30; i++) {
        try {
          const response = await fetch(`http://localhost:${HTTP_PORT}/health`);
          if (response.ok) {
            serverReady = true;
            break;
          }
        } catch (error) {
          // Server not ready yet
        }
        await new Promise(resolve => setTimeout(resolve, 1000));
      }
      
      testFactory.endTimer('docker-http-startup', startTime, serverReady);
      
      expect(serverReady).toBe(true);
      
      // Test health endpoint
      const healthResponse = await fetch(`http://localhost:${HTTP_PORT}/health`);
      expect(healthResponse.ok).toBe(true);
      const healthData = await healthResponse.text();
      expect(healthData).toBe('ok');
      
      // Test info endpoint
      const infoResponse = await fetch(`http://localhost:${HTTP_PORT}/info`);
      expect(infoResponse.ok).toBe(true);
      const infoData = await infoResponse.json();
      expect(infoData).toHaveProperty('name');
      expect(infoData).toHaveProperty('version');
      
      console.log('✅ HTTP container serving endpoints correctly');
      
      // Cleanup
      await execAsync(`docker stop ${CONTAINER_NAME}-http && docker rm ${CONTAINER_NAME}-http`);
    }, 60000);

    it('should work with docker-compose', async () => {
      console.log('🐳 Testing docker-compose integration...');
      
      const startTime = testFactory.startTimer('docker-compose-test');
      const composeOverridePath = `${process.cwd()}/docker-compose.override.yml`;

      try {
        // Test stdio mode (default)
        console.log('  Testing stdio mode with docker-compose...');
        await execAsync('docker compose up -d', { cwd: process.cwd() });
        
        await new Promise(resolve => setTimeout(resolve, 5000));
        
        const { stdout: psStdio } = await execAsync('docker compose ps', { cwd: process.cwd() });
        expect(psStdio).toContain('Up');
        
        await execAsync('docker compose down', { cwd: process.cwd() });
        console.log('  ✅ docker-compose stdio mode works');

        // Test HTTP mode using an override file
        console.log('  Testing http mode with docker-compose...');
        const composeOverride = `
services:
  calendar-mcp:
    ports:
      - "${HTTP_PORT}:3000"
    environment:
      TRANSPORT: http
      HOST: 0.0.0.0
      PORT: 3000
`;
        await fs.writeFile(composeOverridePath, composeOverride);

        await execAsync('docker compose up -d', { cwd: process.cwd() });
        
        let httpReady = false;
        for (let i = 0; i < 20; i++) {
          try {
            const response = await fetch(`http://localhost:${HTTP_PORT}/health`);
            if (response.ok) {
              httpReady = true;
              break;
            }
          } catch (error) { /* wait */ }
          await new Promise(resolve => setTimeout(resolve, 1000));
        }
        
        expect(httpReady).toBe(true);
        console.log('  ✅ docker-compose http mode works');

        testFactory.endTimer('docker-compose-test', startTime, true);
        console.log('✅ docker-compose integration working');
        
      } finally {
        // Always cleanup
        await execAsync('docker compose down', { cwd: process.cwd() }).catch(() => {});
        await fs.unlink(composeOverridePath).catch(() => {});
      }
    }, 90000);
  });

  describe('Calendar Operations via Docker', () => {
    beforeEach(async () => {
      // Start container for calendar operations
      await execAsync(`docker run -d --name ${CONTAINER_NAME} \
        -v ${process.cwd()}/gcp-oauth.keys.json:/usr/src/app/gcp-oauth.keys.json:ro \
        -v mcp-test-tokens:/home/nodejs/.config/google-calendar-mcp \
        -e NODE_ENV=test \
        -e TRANSPORT=stdio \
        --entrypoint=/bin/sh \
        google-calendar-mcp:test -c "while true; do sleep 30; done"`);
      
      // Connect MCP client
      mcpClient = new Client({
        name: "docker-calendar-client",
        version: "1.0.0"
      }, {
        capabilities: { tools: {} }
      });

      const transport = new StdioClientTransport({
        command: 'docker',
        args: ['exec', '-i', CONTAINER_NAME, 'npm', 'start'],
        env: { ...process.env, NODE_ENV: 'test' }
      });
      
      await mcpClient.connect(transport);
    });

    afterEach(async () => {
      if (mcpClient) {
        await mcpClient.close();
      }
      await execAsync(`docker stop ${CONTAINER_NAME} && docker rm ${CONTAINER_NAME}`).catch(() => {});
    });

    it('should list calendars through Docker', async () => {
      console.log('📅 Testing calendar listing via Docker...');
      
      const startTime = testFactory.startTimer('docker-list-calendars');
      
      try {
        const result = await mcpClient.callTool({
          name: 'list-calendars',
          arguments: {}
        });
        
        testFactory.endTimer('docker-list-calendars', startTime, true);
        
        expect(result).toBeDefined();
        expect(result.content).toBeDefined();
        const calendars = result.content as any[];
        expect(Array.isArray(calendars)).toBe(true);
        
        // Should have at least primary calendar
        expect(calendars.length).toBeGreaterThan(0);
        
        console.log(`✅ Listed ${calendars.length} calendars via Docker`);
        
      } catch (error) {
        testFactory.endTimer('docker-list-calendars', startTime, false, String(error));
        throw error;
      }
    }, 30000);

    it('should create and manage events through Docker', async () => {
      console.log('📝 Testing event creation via Docker...');
      
      const eventDetails = TestDataFactory.createSingleEvent({
        summary: 'Docker Integration Test Event'
      });
      
      const eventData = {
        ...eventDetails,
        calendarId: TEST_CALENDAR_ID
      };

      const createStartTime = testFactory.startTimer('docker-create-event');
      
      try {
        // Create event
        const createResult = await mcpClient.callTool({
          name: 'create-event',
          arguments: eventData
        });
        
        testFactory.endTimer('docker-create-event', createStartTime, true);
        
        expect(createResult).toBeDefined();
        expect(createResult.content).toBeDefined();
        
        // Extract event ID for cleanup
        const eventId = TestDataFactory.extractEventIdFromResponse(createResult);
        expect(eventId).toBeTruthy();
        if (eventId) {
          createdEventIds.push(eventId);
        }
        
        console.log(`✅ Created event ${eventId} via Docker`);
        
        // Verify event exists by listing events
        const listStartTime = testFactory.startTimer('docker-list-events');
        
        const listResult = await mcpClient.callTool({
          name: 'list-events',
          arguments: {
            calendarId: TEST_CALENDAR_ID,
            timeMin: eventData.start,
            timeMax: eventData.end
          }
        });
        
        testFactory.endTimer('docker-list-events', listStartTime, true);
        
        expect(listResult.content).toBeDefined();
        const events = Array.isArray(listResult.content) ? listResult.content : [listResult.content];
        const createdEvent = events.find((event: any) => 
          event.text && event.text.includes(eventData.summary)
        );
        expect(createdEvent).toBeDefined();
        
        console.log('✅ Verified event creation through listing');
        
      } catch (error) {
        testFactory.endTimer('docker-create-event', createStartTime, false, String(error));
        throw error;
      }
    }, 45000);

    it('should handle current time requests through Docker', async () => {
      console.log('🕐 Testing current time via Docker...');
      
      const startTime = testFactory.startTimer('docker-current-time');
      
      try {
        const result = await mcpClient.callTool({
          name: 'get-current-time',
          arguments: {
            timeZone: 'America/Los_Angeles'
          }
        });
        
        testFactory.endTimer('docker-current-time', startTime, true);
        
        expect(result).toBeDefined();
        expect(result.content).toBeDefined();
        
        console.log('✅ Current time retrieved via Docker');
        
      } catch (error) {
        testFactory.endTimer('docker-current-time', startTime, false, String(error));
        throw error;
      }
    }, 15000);
  });

  describe('Performance and Resource Testing', () => {
    it('should perform within acceptable resource limits', async () => {
      console.log('📊 Testing Docker container performance...');
      
      // Start container
      await execAsync(`docker run -d --name ${CONTAINER_NAME} \
        -v ${process.cwd()}/gcp-oauth.keys.json:/usr/src/app/gcp-oauth.keys.json:ro \
        -v mcp-test-tokens:/home/nodejs/.config/google-calendar-mcp \
        -e NODE_ENV=test \
        -e TRANSPORT=stdio \
        --entrypoint=/bin/sh \
        google-calendar-mcp:test -c "while true; do sleep 30; done"`);
      
      // Wait for container to stabilize
      await new Promise(resolve => setTimeout(resolve, 5000));
      
      // Get container stats
      const { stdout } = await execAsync(`docker stats --no-stream --format "{{.MemUsage}},{{.CPUPerc}}" ${CONTAINER_NAME}`);
      const [memUsage, cpuUsage] = stdout.trim().split(',');
      
      console.log(`Memory usage: ${memUsage}`);
      console.log(`CPU usage: ${cpuUsage}`);
      
      // Parse memory usage (e.g., "45.2MiB / 512MiB")
      const memoryMB = parseFloat(memUsage.split('/')[0].replace('MiB', '').trim());
      expect(memoryMB).toBeLessThan(200); // Should use less than 200MB
      
      // Parse CPU usage (e.g., "1.23%")
      const cpuPercent = parseFloat(cpuUsage.replace('%', ''));
      expect(cpuPercent).toBeLessThan(50); // Should use less than 50% CPU when idle
      
      console.log('✅ Container performance within acceptable limits');
      
      // Cleanup
      await execAsync(`docker stop ${CONTAINER_NAME} && docker rm ${CONTAINER_NAME}`);
    }, 30000);

    it('should handle concurrent requests efficiently', async () => {
      console.log('🚀 Testing concurrent request handling...');
      
      // Start HTTP container for concurrent testing
      await execAsync(`docker run -d --name ${CONTAINER_NAME}-http \
        -p ${HTTP_PORT}:3000 \
        -v ${process.cwd()}/gcp-oauth.keys.json:/usr/src/app/gcp-oauth.keys.json:ro \
        -v mcp-test-tokens:/home/nodejs/.config/google-calendar-mcp \
        -e NODE_ENV=test \
        -e TRANSPORT=http \
        -e HOST=0.0.0.0 \
        -e PORT=3000 \
        google-calendar-mcp:test`);
      
      // Wait for server to be ready
      for (let i = 0; i < 20; i++) {
        try {
          const response = await fetch(`http://localhost:${HTTP_PORT}/health`);
          if (response.ok) break;
        } catch (error) {
          // Not ready yet
        }
        await new Promise(resolve => setTimeout(resolve, 1000));
      }
      
      // Make concurrent health check requests
      const concurrentRequests = 10;
      const startTime = Date.now();
      
      const requests = Array(concurrentRequests).fill(null).map(async () => {
        const response = await fetch(`http://localhost:${HTTP_PORT}/health`);
        return { ok: response.ok, time: Date.now() };
      });
      
      const results = await Promise.all(requests);
      const totalTime = Date.now() - startTime;
      
      // All requests should succeed
      expect(results.every(r => r.ok)).toBe(true);
      
      // Average response time should be reasonable
      expect(totalTime / concurrentRequests).toBeLessThan(1000); // Less than 1 second per request on average
      
      console.log(`✅ Handled ${concurrentRequests} concurrent requests in ${totalTime}ms`);
      
      // Cleanup
      await execAsync(`docker stop ${CONTAINER_NAME}-http && docker rm ${CONTAINER_NAME}-http`);
    }, 60000);
  });

  // Helper Functions
  async function cleanupDockerResources(): Promise<void> {
    const containerNames = [
      CONTAINER_NAME,
      `${CONTAINER_NAME}-http`
    ];
    
    for (const name of containerNames) {
      try {
        await execAsync(`docker stop ${name} 2>/dev/null || true`);
        await execAsync(`docker rm ${name} 2>/dev/null || true`);
      } catch (error) {
        // Ignore cleanup errors
      }
    }
    
    // Remove test volume
    try {
      await execAsync('docker volume rm mcp-test-tokens 2>/dev/null || true');
    } catch (error) {
      // Ignore cleanup errors
    }
  }

  async function cleanupEvents(eventIds: string[]): Promise<void> {
    if (!mcpClient || eventIds.length === 0) 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> {
    await cleanupEvents(createdEventIds);
  }
});
```

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

```typescript
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { CreateEventHandler } from '../../../handlers/core/CreateEventHandler.js';
import { OAuth2Client } from 'google-auth-library';

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

// Mock the event ID validator
vi.mock('../../../utils/event-id-validator.js', () => ({
  validateEventId: vi.fn((eventId: string) => {
    if (eventId && eventId.length < 5 || eventId.length > 1024) {
      throw new Error(`Invalid event ID: length must be between 5 and 1024 characters`);
    }
    if (eventId && !/^[a-zA-Z0-9-]+$/.test(eventId)) {
      throw new Error(`Invalid event ID: can only contain letters, numbers, and hyphens`);
    }
  })
}));

// Mock datetime utilities
vi.mock('../../../utils/datetime.js', () => ({
  createTimeObject: vi.fn((datetime: string, timezone: string) => ({ 
    dateTime: datetime,
    timeZone: timezone 
  }))
}));

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

  beforeEach(() => {
    handler = new CreateEventHandler();
    mockOAuth2Client = new OAuth2Client();
    
    // Setup mock calendar
    mockCalendar = {
      events: {
        insert: 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 Creation', () => {
    it('should create an event without custom ID', async () => {
      const mockCreatedEvent = {
        id: 'generated-id-123',
        summary: 'Test Event',
        start: { dateTime: '2025-01-15T10:00:00Z' },
        end: { dateTime: '2025-01-15T11:00:00Z' },
        htmlLink: 'https://calendar.google.com/event?eid=abc123'
      };

      mockCalendar.events.insert.mockResolvedValue({ data: mockCreatedEvent });

      const args = {
        calendarId: 'primary',
        summary: 'Test Event',
        start: '2025-01-15T10:00:00',
        end: '2025-01-15T11:00:00'
      };

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

      expect(mockCalendar.events.insert).toHaveBeenCalledWith({
        calendarId: 'primary',
        requestBody: expect.objectContaining({
          summary: 'Test Event',
          start: { dateTime: '2025-01-15T10:00:00', timeZone: 'America/Los_Angeles' },
          end: { dateTime: '2025-01-15T11:00:00', timeZone: 'America/Los_Angeles' }
        })
      });

      // Should not include id field when no custom ID provided
      expect(mockCalendar.events.insert.mock.calls[0][0].requestBody.id).toBeUndefined();

      expect(result.content[0].type).toBe('text');
      const response = JSON.parse(result.content[0].text);
      expect(response.event).toBeDefined();
      expect(response.event.id).toBe('generated-id-123');
      expect(response.event.summary).toBe('Test Event');
    });

    it('should create event with all basic optional fields', async () => {
      const mockCreatedEvent = {
        id: 'full-event',
        summary: 'Full Event',
        description: 'Event description',
        location: 'Conference Room A',
        start: { dateTime: '2025-01-15T10:00:00Z' },
        end: { dateTime: '2025-01-15T11:00:00Z' },
        attendees: [{ email: '[email protected]' }],
        colorId: '5',
        reminders: { useDefault: false, overrides: [{ method: 'email', minutes: 30 }] }
      };

      mockCalendar.events.insert.mockResolvedValue({ data: mockCreatedEvent });

      const args = {
        calendarId: 'primary',
        eventId: 'full-event',
        summary: 'Full Event',
        description: 'Event description',
        location: 'Conference Room A',
        start: '2025-01-15T10:00:00',
        end: '2025-01-15T11:00:00',
        attendees: [{ email: '[email protected]' }],
        colorId: '5',
        reminders: {
          useDefault: false,
          overrides: [{ method: 'email' as const, minutes: 30 }]
        }
      };

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

      expect(mockCalendar.events.insert).toHaveBeenCalledWith({
        calendarId: 'primary',
        requestBody: expect.objectContaining({
          id: 'full-event',
          summary: 'Full Event',
          description: 'Event description',
          location: 'Conference Room A',
          attendees: [{ email: '[email protected]' }],
          colorId: '5',
          reminders: {
            useDefault: false,
            overrides: [{ method: 'email', minutes: 30 }]
          }
        })
      });

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

  describe('Custom Event IDs', () => {
    it('should create an event with custom ID', async () => {
      const mockCreatedEvent = {
        id: 'customevent2025',
        summary: 'Test Event',
        start: { dateTime: '2025-01-15T10:00:00Z' },
        end: { dateTime: '2025-01-15T11:00:00Z' },
        htmlLink: 'https://calendar.google.com/event?eid=abc123'
      };

      mockCalendar.events.insert.mockResolvedValue({ data: mockCreatedEvent });

      const args = {
        calendarId: 'primary',
        eventId: 'customevent2025',
        summary: 'Test Event',
        start: '2025-01-15T10:00:00',
        end: '2025-01-15T11:00:00'
      };

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

      expect(mockCalendar.events.insert).toHaveBeenCalledWith({
        calendarId: 'primary',
        requestBody: expect.objectContaining({
          id: 'customevent2025',
          summary: 'Test Event',
          start: { dateTime: '2025-01-15T10:00:00', timeZone: 'America/Los_Angeles' },
          end: { dateTime: '2025-01-15T11:00:00', timeZone: 'America/Los_Angeles' }
        })
      });

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

    it('should validate event ID before making API call', async () => {
      const args = {
        calendarId: 'primary',
        eventId: 'abc', // Too short (< 5 chars)
        summary: 'Test Event',
        start: '2025-01-15T10:00:00',
        end: '2025-01-15T11:00:00'
      };

      await expect(handler.runTool(args, mockOAuth2Client)).rejects.toThrow(
        'Invalid event ID: length must be between 5 and 1024 characters'
      );

      // Should not call the API if validation fails
      expect(mockCalendar.events.insert).not.toHaveBeenCalled();
    });

    it('should handle invalid custom event ID', async () => {
      const args = {
        calendarId: 'primary',
        eventId: 'bad id', // Contains space
        summary: 'Test Event',
        start: '2025-01-15T10:00:00',
        end: '2025-01-15T11:00:00'
      };

      await expect(handler.runTool(args, mockOAuth2Client)).rejects.toThrow(
        'Invalid event ID: can only contain letters, numbers, and hyphens'
      );

      expect(mockCalendar.events.insert).not.toHaveBeenCalled();
    });

    it('should handle event ID conflict (409 error)', async () => {
      const conflictError = new Error('Conflict');
      (conflictError as any).code = 409;
      mockCalendar.events.insert.mockRejectedValue(conflictError);

      const args = {
        calendarId: 'primary',
        eventId: 'existing-event',
        summary: 'Test Event',
        start: '2025-01-15T10:00:00',
        end: '2025-01-15T11:00:00'
      };

      await expect(handler.runTool(args, mockOAuth2Client)).rejects.toThrow(
        "Event ID 'existing-event' already exists. Please use a different ID."
      );
    });

    it('should handle event ID conflict with response status', async () => {
      const conflictError = new Error('Conflict');
      (conflictError as any).response = { status: 409 };
      mockCalendar.events.insert.mockRejectedValue(conflictError);

      const args = {
        calendarId: 'primary',
        eventId: 'existing-event',
        summary: 'Test Event',
        start: '2025-01-15T10:00:00',
        end: '2025-01-15T11:00:00'
      };

      await expect(handler.runTool(args, mockOAuth2Client)).rejects.toThrow(
        "Event ID 'existing-event' already exists. Please use a different ID."
      );
    });
  });

  describe('Guest Management Properties', () => {
    it('should create event with transparency setting', async () => {
      const mockCreatedEvent = {
        id: 'event123',
        summary: 'Focus Time',
        transparency: 'transparent'
      };

      mockCalendar.events.insert.mockResolvedValue({ data: mockCreatedEvent });

      const args = {
        calendarId: 'primary',
        summary: 'Focus Time',
        start: '2025-01-15T10:00:00',
        end: '2025-01-15T11:00:00',
        transparency: 'transparent' as const
      };

      await handler.runTool(args, mockOAuth2Client);

      expect(mockCalendar.events.insert).toHaveBeenCalledWith(
        expect.objectContaining({
          requestBody: expect.objectContaining({
            transparency: 'transparent'
          })
        })
      );
    });

    it('should create event with visibility settings', async () => {
      const mockCreatedEvent = {
        id: 'event123',
        summary: 'Private Meeting',
        visibility: 'private'
      };

      mockCalendar.events.insert.mockResolvedValue({ data: mockCreatedEvent });

      const args = {
        calendarId: 'primary',
        summary: 'Private Meeting',
        start: '2025-01-15T10:00:00',
        end: '2025-01-15T11:00:00',
        visibility: 'private' as const
      };

      await handler.runTool(args, mockOAuth2Client);

      expect(mockCalendar.events.insert).toHaveBeenCalledWith(
        expect.objectContaining({
          requestBody: expect.objectContaining({
            visibility: 'private'
          })
        })
      );
    });

    it('should create event with guest permissions', async () => {
      const mockCreatedEvent = {
        id: 'event123',
        summary: 'Team Meeting'
      };

      mockCalendar.events.insert.mockResolvedValue({ data: mockCreatedEvent });

      const args = {
        calendarId: 'primary',
        summary: 'Team Meeting',
        start: '2025-01-15T10:00:00',
        end: '2025-01-15T11:00:00',
        guestsCanInviteOthers: false,
        guestsCanModify: true,
        guestsCanSeeOtherGuests: false,
        anyoneCanAddSelf: true
      };

      await handler.runTool(args, mockOAuth2Client);

      expect(mockCalendar.events.insert).toHaveBeenCalledWith(
        expect.objectContaining({
          requestBody: expect.objectContaining({
            guestsCanInviteOthers: false,
            guestsCanModify: true,
            guestsCanSeeOtherGuests: false,
            anyoneCanAddSelf: true
          })
        })
      );
    });

    it('should send update notifications when specified', async () => {
      const mockCreatedEvent = {
        id: 'event123',
        summary: 'Meeting'
      };

      mockCalendar.events.insert.mockResolvedValue({ data: mockCreatedEvent });

      const args = {
        calendarId: 'primary',
        summary: 'Meeting',
        start: '2025-01-15T10:00:00',
        end: '2025-01-15T11:00:00',
        sendUpdates: 'externalOnly' as const
      };

      await handler.runTool(args, mockOAuth2Client);

      expect(mockCalendar.events.insert).toHaveBeenCalledWith(
        expect.objectContaining({
          sendUpdates: 'externalOnly'
        })
      );
    });
  });

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

      mockCalendar.events.insert.mockResolvedValue({ data: mockCreatedEvent });

      const args = {
        calendarId: 'primary',
        summary: 'Video Call',
        start: '2025-01-15T10:00:00',
        end: '2025-01-15T11:00:00',
        conferenceData: {
          createRequest: {
            requestId: 'unique-request-123',
            conferenceSolutionKey: {
              type: 'hangoutsMeet' as const
            }
          }
        }
      };

      await handler.runTool(args, mockOAuth2Client);

      expect(mockCalendar.events.insert).toHaveBeenCalledWith(
        expect.objectContaining({
          requestBody: expect.objectContaining({
            conferenceData: {
              createRequest: {
                requestId: 'unique-request-123',
                conferenceSolutionKey: {
                  type: 'hangoutsMeet'
                }
              }
            }
          }),
          conferenceDataVersion: 1
        })
      );
    });
  });

  describe('Extended Properties', () => {
    it('should create event with extended properties', async () => {
      const mockCreatedEvent = {
        id: 'event123',
        summary: 'Custom Event'
      };

      mockCalendar.events.insert.mockResolvedValue({ data: mockCreatedEvent });

      const args = {
        calendarId: 'primary',
        summary: 'Custom Event',
        start: '2025-01-15T10:00:00',
        end: '2025-01-15T11:00:00',
        extendedProperties: {
          private: {
            'appId': '12345',
            'customField': 'value1'
          },
          shared: {
            'projectId': 'proj-789',
            'category': 'meeting'
          }
        }
      };

      await handler.runTool(args, mockOAuth2Client);

      expect(mockCalendar.events.insert).toHaveBeenCalledWith(
        expect.objectContaining({
          requestBody: expect.objectContaining({
            extendedProperties: {
              private: {
                'appId': '12345',
                'customField': 'value1'
              },
              shared: {
                'projectId': 'proj-789',
                'category': 'meeting'
              }
            }
          })
        })
      );
    });
  });

  describe('Attachments', () => {
    it('should create event with attachments', async () => {
      const mockCreatedEvent = {
        id: 'event123',
        summary: 'Meeting with Docs'
      };

      mockCalendar.events.insert.mockResolvedValue({ data: mockCreatedEvent });

      const args = {
        calendarId: 'primary',
        summary: 'Meeting with Docs',
        start: '2025-01-15T10:00:00',
        end: '2025-01-15T11:00:00',
        attachments: [
          {
            fileUrl: 'https://docs.google.com/document/d/123',
            title: 'Meeting Agenda',
            mimeType: 'application/vnd.google-apps.document'
          },
          {
            fileUrl: 'https://drive.google.com/file/d/456',
            title: 'Presentation',
            mimeType: 'application/vnd.google-apps.presentation',
            fileId: '456'
          }
        ]
      };

      await handler.runTool(args, mockOAuth2Client);

      expect(mockCalendar.events.insert).toHaveBeenCalledWith(
        expect.objectContaining({
          requestBody: expect.objectContaining({
            attachments: [
              {
                fileUrl: 'https://docs.google.com/document/d/123',
                title: 'Meeting Agenda',
                mimeType: 'application/vnd.google-apps.document'
              },
              {
                fileUrl: 'https://drive.google.com/file/d/456',
                title: 'Presentation',
                mimeType: 'application/vnd.google-apps.presentation',
                fileId: '456'
              }
            ]
          }),
          supportsAttachments: true
        })
      );
    });
  });

  describe('Enhanced Attendees', () => {
    it('should create event with detailed attendee information', async () => {
      const mockCreatedEvent = {
        id: 'event123',
        summary: 'Team Sync'
      };

      mockCalendar.events.insert.mockResolvedValue({ data: mockCreatedEvent });

      const args = {
        calendarId: 'primary',
        summary: 'Team Sync',
        start: '2025-01-15T10:00:00',
        end: '2025-01-15T11:00:00',
        attendees: [
          {
            email: '[email protected]',
            displayName: 'Alice Smith',
            optional: false,
            responseStatus: 'accepted' as const
          },
          {
            email: '[email protected]',
            displayName: 'Bob Jones',
            optional: true,
            responseStatus: 'needsAction' as const,
            comment: 'May join late',
            additionalGuests: 2
          }
        ]
      };

      await handler.runTool(args, mockOAuth2Client);

      expect(mockCalendar.events.insert).toHaveBeenCalledWith(
        expect.objectContaining({
          requestBody: expect.objectContaining({
            attendees: [
              {
                email: '[email protected]',
                displayName: 'Alice Smith',
                optional: false,
                responseStatus: 'accepted'
              },
              {
                email: '[email protected]',
                displayName: 'Bob Jones',
                optional: true,
                responseStatus: 'needsAction',
                comment: 'May join late',
                additionalGuests: 2
              }
            ]
          })
        })
      );
    });
  });

  describe('Source Property', () => {
    it('should create event with source information', async () => {
      const mockCreatedEvent = {
        id: 'event123',
        summary: 'Follow-up Meeting'
      };

      mockCalendar.events.insert.mockResolvedValue({ data: mockCreatedEvent });

      const args = {
        calendarId: 'primary',
        summary: 'Follow-up Meeting',
        start: '2025-01-15T10:00:00',
        end: '2025-01-15T11:00:00',
        source: {
          url: 'https://example.com/meetings/123',
          title: 'Original Meeting Request'
        }
      };

      await handler.runTool(args, mockOAuth2Client);

      expect(mockCalendar.events.insert).toHaveBeenCalledWith(
        expect.objectContaining({
          requestBody: expect.objectContaining({
            source: {
              url: 'https://example.com/meetings/123',
              title: 'Original Meeting Request'
            }
          })
        })
      );
    });
  });

  describe('Error Handling', () => {
    it('should handle API errors other than 409', async () => {
      const apiError = new Error('API Error');
      (apiError as any).code = 500;
      mockCalendar.events.insert.mockRejectedValue(apiError);

      const args = {
        calendarId: 'primary',
        summary: 'Test Event',
        start: '2025-01-15T10:00:00',
        end: '2025-01-15T11:00:00'
      };

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

      await expect(handler.runTool(args, mockOAuth2Client)).rejects.toThrow('Handled API Error');
    });

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

      const args = {
        calendarId: 'primary',
        summary: 'Test Event',
        start: '2025-01-15T10:00:00',
        end: '2025-01-15T11:00:00'
      };

      await expect(handler.runTool(args, mockOAuth2Client)).rejects.toThrow(
        'Failed to create event, no data returned'
      );
    });
  });

  describe('Combined Properties', () => {
    it('should create event with multiple enhanced properties', async () => {
      const mockCreatedEvent = {
        id: 'event123',
        summary: 'Complex Event'
      };

      mockCalendar.events.insert.mockResolvedValue({ data: mockCreatedEvent });

      const args = {
        calendarId: 'primary',
        eventId: 'customcomplexevent',
        summary: 'Complex Event',
        description: 'An event with all features',
        start: '2025-01-15T10:00:00',
        end: '2025-01-15T11:00:00',
        location: 'Conference Room A',
        transparency: 'opaque' as const,
        visibility: 'public' as const,
        guestsCanInviteOthers: true,
        guestsCanModify: false,
        conferenceData: {
          createRequest: {
            requestId: 'conf-123',
            conferenceSolutionKey: {
              type: 'hangoutsMeet' as const
            }
          }
        },
        attendees: [
          {
            email: '[email protected]',
            displayName: 'Team',
            optional: false
          }
        ],
        extendedProperties: {
          private: {
            'trackingId': '789'
          }
        },
        source: {
          url: 'https://example.com/source',
          title: 'Source System'
        },
        sendUpdates: 'all' as const
      };

      await handler.runTool(args, mockOAuth2Client);

      const callArgs = mockCalendar.events.insert.mock.calls[0][0];
      
      expect(callArgs.requestBody).toMatchObject({
        id: 'customcomplexevent',
        summary: 'Complex Event',
        description: 'An event with all features',
        location: 'Conference Room A',
        transparency: 'opaque',
        visibility: 'public',
        guestsCanInviteOthers: true,
        guestsCanModify: false
      });
      
      expect(callArgs.conferenceDataVersion).toBe(1);
      expect(callArgs.sendUpdates).toBe('all');
    });
  });
});
```

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

```typescript
/**
 * @jest-environment node
 */
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { OAuth2Client } from 'google-auth-library';
import { calendar_v3 } from 'googleapis';
// Import the types and schemas we're testing
import { ToolSchemas } from '../../../tools/registry.js';

// Get the schema for validation testing
const ListEventsArgumentsSchema = ToolSchemas['list-events'];
import { ListEventsHandler } from '../../../handlers/core/ListEventsHandler.js';

// Mock the BatchRequestHandler that we'll implement
class MockBatchRequestHandler {
  constructor(_auth: OAuth2Client) {}

  async executeBatch(_requests: any[]): Promise<any[]> {
    // This will be mocked in tests
    return [];
  }
}

// Mock dependencies
vi.mock('google-auth-library');
vi.mock('googleapis');

interface ExtendedEvent extends calendar_v3.Schema$Event {
  calendarId?: string;
}

describe('Batch List Events Functionality', () => {
  let mockOAuth2Client: OAuth2Client;
  let listEventsHandler: ListEventsHandler;
  let mockCalendarApi: any;

  beforeEach(() => {
    // Reset all mocks
    vi.clearAllMocks();

    // Create mock OAuth2Client
    mockOAuth2Client = new OAuth2Client();
    
    // Create mock calendar API
    mockCalendarApi = {
      events: {
        list: vi.fn()
      },
      calendarList: {
        list: vi.fn().mockResolvedValue({
          data: {
            items: [
              { id: 'primary', summary: 'Primary Calendar', summaryOverride: undefined },
              { id: '[email protected]', summary: 'Work Calendar', summaryOverride: undefined },
              { id: '[email protected]', summary: 'Personal Calendar', summaryOverride: undefined }
            ]
          }
        })
      }
    };

    // Mock the getCalendar method in BaseToolHandler
    listEventsHandler = new ListEventsHandler();
    vi.spyOn(listEventsHandler as any, 'getCalendar').mockReturnValue(mockCalendarApi);
  });

  describe('Input Validation', () => {
    it('should validate single calendar ID string', () => {
      const input = {
        calendarId: 'primary',
        timeMin: '2024-01-01T00:00:00Z',
        timeMax: '2024-12-31T23:59:59Z'
      };

      const result = ListEventsArgumentsSchema.safeParse(input);
      expect(result.success).toBe(true);
      expect(result.data?.calendarId).toBe('primary');
    });

    it('should validate array of calendar IDs', () => {
      // Arrays must be passed as JSON strings in the new schema
      const input = {
        calendarId: '["primary", "[email protected]", "[email protected]"]',
        timeMin: '2024-01-01T00:00:00Z'
      };

      const result = ListEventsArgumentsSchema.safeParse(input);
      expect(result.success).toBe(true);
      expect(typeof result.data?.calendarId).toBe('string');
      expect(result.data?.calendarId).toBe('["primary", "[email protected]", "[email protected]"]');
    });

    it('should accept actual array of calendar IDs (not JSON string)', () => {
      // Arrays are now directly supported and converted to JSON strings
      const input = {
        calendarId: ['primary', '[email protected]', '[email protected]'],
        timeMin: '2024-01-01T00:00:00Z'
      };

      const result = ListEventsArgumentsSchema.safeParse(input);
      expect(result.success).toBe(true);
      // Arrays are now kept as arrays (not transformed to JSON strings)
      expect(result.data?.calendarId).toEqual(['primary', '[email protected]', '[email protected]']);
    });

    it('should handle malformed JSON string gracefully', () => {
      // Test that malformed JSON is treated as a regular string
      const input = {
        calendarId: '["primary", "[email protected]"', // Missing closing bracket
        timeMin: '2024-01-01T00:00:00Z'
      };

      const result = ListEventsArgumentsSchema.safeParse(input);
      expect(result.success).toBe(true);
      expect(typeof result.data?.calendarId).toBe('string');
      expect(result.data?.calendarId).toBe('["primary", "[email protected]"');
    });

    it('should reject empty calendar ID array', () => {
      const input = {
        calendarId: [],
        timeMin: '2024-01-01T00:00:00Z'
      };

      const result = ListEventsArgumentsSchema.safeParse(input);
      expect(result.success).toBe(false);
    });

    it('should reject array with too many calendar IDs (> 50)', () => {
      const input = {
        calendarId: Array(51).fill('cal').map((c, i) => `${c}${i}@example.com`),
        timeMin: '2024-01-01T00:00:00Z'
      };

      const result = ListEventsArgumentsSchema.safeParse(input);
      expect(result.success).toBe(false);
    });

    it('should reject invalid time format', () => {
      const input = {
        calendarId: 'primary',
        timeMin: '2024-01-01' // Missing time and timezone
      };

      const result = ListEventsArgumentsSchema.safeParse(input);
      expect(result.success).toBe(false);
    });
  });

  describe('Single Calendar Events (Existing Functionality)', () => {
    it('should handle single calendar ID as string', async () => {
      // Arrange
      const mockEvents: ExtendedEvent[] = [
        {
          id: 'event1',
          summary: 'Meeting',
          start: { dateTime: '2024-01-15T10:00:00Z' },
          end: { dateTime: '2024-01-15T11:00:00Z' }
        },
        {
          id: 'event2',
          summary: 'Lunch',
          start: { dateTime: '2024-01-15T12:00:00Z' },
          end: { dateTime: '2024-01-15T13:00:00Z' },
          location: 'Restaurant'
        }
      ];

      mockCalendarApi.events.list.mockResolvedValue({
        data: { items: mockEvents }
      });

      const args = {
        calendarId: 'primary',
        timeMin: '2024-01-01T00:00:00Z',
        timeMax: '2024-01-31T23:59:59Z'
      };

      // Act
      const result = await listEventsHandler.runTool(args, mockOAuth2Client);

      // Assert
      expect(mockCalendarApi.events.list).toHaveBeenCalledWith({
        calendarId: 'primary',
        timeMin: args.timeMin,
        timeMax: args.timeMax,
        singleEvents: true,
        orderBy: 'startTime'
      });

      // Should return structured JSON with events
      expect(result.content).toHaveLength(1);
      expect(result.content[0].type).toBe('text');
      const response = JSON.parse((result.content[0] as any).text);
      expect(response.events).toHaveLength(2);
      expect(response.totalCount).toBe(2);
    });

    it('should handle empty results for single calendar', async () => {
      // Arrange
      mockCalendarApi.events.list.mockResolvedValue({
        data: { items: [] }
      });

      const args = {
        calendarId: 'primary',
        timeMin: '2024-01-01T00:00:00Z'
      };

      // Act
      const result = await listEventsHandler.runTool(args, mockOAuth2Client);

      // Assert - no events means empty array in JSON
      expect(result.content).toHaveLength(1);
      expect(result.content[0].type).toBe('text');
      const response = JSON.parse((result.content[0] as any).text);
      expect(response.events).toHaveLength(0);
      expect(response.totalCount).toBe(0);
    });
  });

  describe('Batch Request Creation', () => {
    it('should create proper batch requests for multiple calendars', () => {
      // This tests the batch request creation logic
      const calendarIds = ['primary', '[email protected]', '[email protected]'];
      const options = {
        timeMin: '2024-01-01T00:00:00Z',
        timeMax: '2024-01-31T23:59:59Z'
      };

      // Expected batch requests
      const expectedRequests = calendarIds.map(calendarId => ({
        method: 'GET',
        path: `/calendar/v3/calendars/${encodeURIComponent(calendarId)}/events?` + 
          new URLSearchParams({
            singleEvents: 'true',
            orderBy: 'startTime',
            timeMin: options.timeMin,
            timeMax: options.timeMax
          }).toString()
      }));

      // Verify the expected structure
      expect(expectedRequests).toHaveLength(3);
      expect(expectedRequests[0].path).toContain('calendars/primary/events');
      expect(expectedRequests[1].path).toContain('calendars/work%40example.com/events');
      expect(expectedRequests[2].path).toContain('calendars/personal%40example.com/events');
      
      // All should have proper query parameters
      expectedRequests.forEach(req => {
        expect(req.path).toContain('singleEvents=true');
        expect(req.path).toContain('orderBy=startTime');
        expect(req.path).toContain('timeMin=2024-01-01T00%3A00%3A00Z');
        expect(req.path).toContain('timeMax=2024-01-31T23%3A59%3A59Z');
      });
    });

    it('should handle optional parameters in batch requests', () => {
      const options = { timeMin: '2024-01-01T00:00:00Z' }; // Only timeMin, no timeMax

      const expectedRequest = {
        method: 'GET',
        path: `/calendar/v3/calendars/primary/events?` + 
          new URLSearchParams({
            singleEvents: 'true',
            orderBy: 'startTime',
            timeMin: options.timeMin
          }).toString()
      };

      expect(expectedRequest.path).toContain('timeMin=2024-01-01T00%3A00%3A00Z');
      expect(expectedRequest.path).not.toContain('timeMax');
    });
  });

  describe('Batch Response Parsing', () => {
    it('should parse successful batch responses correctly', () => {
      // Mock successful batch responses
      const mockBatchResponses = [
        {
          statusCode: 200,
          headers: {},
          body: {
            items: [
              {
                id: 'work1',
                summary: 'Work Meeting',
                start: { dateTime: '2024-01-15T09:00:00Z' },
                end: { dateTime: '2024-01-15T10:00:00Z' }
              }
            ]
          }
        },
        {
          statusCode: 200,
          headers: {},
          body: {
            items: [
              {
                id: 'personal1',
                summary: 'Gym',
                start: { dateTime: '2024-01-15T18:00:00Z' },
                end: { dateTime: '2024-01-15T19:00:00Z' }
              }
            ]
          }
        }
      ];

      const calendarIds = ['[email protected]', '[email protected]'];
      
      // Simulate processing batch responses
      const allEvents: ExtendedEvent[] = [];
      const errors: Array<{ calendarId: string; error: any }> = [];

      mockBatchResponses.forEach((response, index) => {
        const calendarId = calendarIds[index];
        
        if (response.statusCode === 200 && response.body.items) {
          const events = response.body.items.map((event: any) => ({
            ...event,
            calendarId
          }));
          allEvents.push(...events);
        } else {
          errors.push({
            calendarId,
            error: response.body
          });
        }
      });

      // Assert results
      expect(allEvents).toHaveLength(2);
      expect(allEvents[0].calendarId).toBe('[email protected]');
      expect(allEvents[0].summary).toBe('Work Meeting');
      expect(allEvents[1].calendarId).toBe('[email protected]');
      expect(allEvents[1].summary).toBe('Gym');
      expect(errors).toHaveLength(0);
    });

    it('should handle partial failures in batch responses', () => {
      // Mock mixed success/failure responses
      const mockBatchResponses = [
        {
          statusCode: 200,
          headers: {},
          body: {
            items: [
              {
                id: 'event1',
                summary: 'Success Event',
                start: { dateTime: '2024-01-15T09:00:00Z' },
                end: { dateTime: '2024-01-15T10:00:00Z' }
              }
            ]
          }
        },
        {
          statusCode: 404,
          headers: {},
          body: {
            error: {
              code: 404,
              message: 'Calendar not found'
            }
          }
        },
        {
          statusCode: 403,
          headers: {},
          body: {
            error: {
              code: 403,
              message: 'Access denied'
            }
          }
        }
      ];

      const calendarIds = ['primary', '[email protected]', '[email protected]'];
      
      // Simulate processing
      const allEvents: ExtendedEvent[] = [];
      const errors: Array<{ calendarId: string; error: any }> = [];

      mockBatchResponses.forEach((response, index) => {
        const calendarId = calendarIds[index];
        
        if (response.statusCode === 200 && response.body.items) {
          const events = response.body.items.map((event: any) => ({
            ...event,
            calendarId
          }));
          allEvents.push(...events);
        } else {
          errors.push({
            calendarId,
            error: response.body
          });
        }
      });

      // Assert partial success
      expect(allEvents).toHaveLength(1);
      expect(allEvents[0].summary).toBe('Success Event');
      expect(errors).toHaveLength(2);
      expect(errors[0].calendarId).toBe('[email protected]');
      expect(errors[1].calendarId).toBe('[email protected]');
    });

    it('should handle empty results from some calendars', () => {
      const mockBatchResponses = [
        {
          statusCode: 200,
          headers: {},
          body: { items: [] } // Empty calendar
        },
        {
          statusCode: 200,
          headers: {},
          body: {
            items: [
              {
                id: 'event1',
                summary: 'Only Event',
                start: { dateTime: '2024-01-15T09:00:00Z' },
                end: { dateTime: '2024-01-15T10:00:00Z' }
              }
            ]
          }
        }
      ];

      const calendarIds = ['[email protected]', '[email protected]'];
      
      const allEvents: ExtendedEvent[] = [];
      
      mockBatchResponses.forEach((response, index) => {
        const calendarId = calendarIds[index];
        
        if (response.statusCode === 200 && response.body.items) {
          const events = response.body.items.map((event: any) => ({
            ...event,
            calendarId
          }));
          allEvents.push(...events);
        }
      });

      expect(allEvents).toHaveLength(1);
      expect(allEvents[0].calendarId).toBe('[email protected]');
    });
  });

  describe('Event Sorting and Formatting', () => {
    it('should sort events by start time across multiple calendars', () => {
      const events: ExtendedEvent[] = [
        {
          id: 'event2',
          summary: 'Second Event',
          start: { dateTime: '2024-01-15T14:00:00Z' },
          end: { dateTime: '2024-01-15T15:00:00Z' },
          calendarId: 'cal2'
        },
        {
          id: 'event1',
          summary: 'First Event',
          start: { dateTime: '2024-01-15T09:00:00Z' },
          end: { dateTime: '2024-01-15T10:00:00Z' },
          calendarId: 'cal1'
        },
        {
          id: 'event3',
          summary: 'Third Event',
          start: { dateTime: '2024-01-15T18:00:00Z' },
          end: { dateTime: '2024-01-15T19:00:00Z' },
          calendarId: 'cal1'
        }
      ];

      // Sort events by start time
      const sortedEvents = events.sort((a, b) => {
        const aStart = a.start?.dateTime || a.start?.date || '';
        const bStart = b.start?.dateTime || b.start?.date || '';
        return aStart.localeCompare(bStart);
      });

      expect(sortedEvents[0].summary).toBe('First Event');
      expect(sortedEvents[1].summary).toBe('Second Event');
      expect(sortedEvents[2].summary).toBe('Third Event');
    });

    it('should format multiple calendar events with calendar grouping', () => {
      const events: ExtendedEvent[] = [
        {
          id: 'work1',
          summary: 'Work Meeting',
          start: { dateTime: '2024-01-15T09:00:00Z' },
          end: { dateTime: '2024-01-15T10:00:00Z' },
          calendarId: '[email protected]'
        },
        {
          id: 'personal1',
          summary: 'Gym',
          start: { dateTime: '2024-01-15T18:00:00Z' },
          end: { dateTime: '2024-01-15T19:00:00Z' },
          calendarId: '[email protected]'
        }
      ];

      // Group events by calendar
      const grouped = events.reduce((acc, event) => {
        const calId = (event as any).calendarId || 'unknown';
        if (!acc[calId]) acc[calId] = [];
        acc[calId].push(event);
        return acc;
      }, {} as Record<string, ExtendedEvent[]>);

      // Since we now return resources instead of formatted text,
      // we just verify that events are grouped correctly
      expect(grouped['[email protected]']).toHaveLength(1);
      expect(grouped['[email protected]']).toHaveLength(1);
      expect(grouped['[email protected]'][0].summary).toBe('Work Meeting');
      expect(grouped['[email protected]'][0].summary).toBe('Gym');
    });

    it('should handle date-only events in sorting', () => {
      const events: ExtendedEvent[] = [
        {
          id: 'all-day',
          summary: 'All Day Event',
          start: { date: '2024-01-15' },
          end: { date: '2024-01-16' }
        },
        {
          id: 'timed',
          summary: 'Timed Event',
          start: { dateTime: '2024-01-15T09:00:00Z' },
          end: { dateTime: '2024-01-15T10:00:00Z' }
        }
      ];

      const sortedEvents = events.sort((a, b) => {
        const aStart = a.start?.dateTime || a.start?.date || '';
        const bStart = b.start?.dateTime || b.start?.date || '';
        return aStart.localeCompare(bStart);
      });

      // Date-only event should come before timed event on same day
      expect(sortedEvents[0].summary).toBe('All Day Event');
      expect(sortedEvents[1].summary).toBe('Timed Event');
    });
  });

  describe('Error Handling', () => {
    it('should handle authentication errors', async () => {
      // Mock authentication failure
      const authError = new Error('Authentication required');
      vi.spyOn(listEventsHandler as any, 'handleGoogleApiError').mockImplementation(() => {
        throw authError;
      });

      mockCalendarApi.events.list.mockRejectedValue(new Error('invalid_grant'));

      const args = {
        calendarId: 'primary',
        timeMin: '2024-01-01T00:00:00Z'
      };

      await expect(listEventsHandler.runTool(args, mockOAuth2Client))
        .rejects.toThrow('Authentication required');
    });

    it('should handle rate limiting gracefully', () => {
      const rateLimitResponse = {
        statusCode: 429,
        headers: { 'Retry-After': '60' },
        body: {
          error: {
            code: 429,
            message: 'Rate limit exceeded'
          }
        }
      };

      // This would be handled in the batch response processing
      const calendarId = 'primary';
      const errors: Array<{ calendarId: string; error: any }> = [];

      errors.push({
        calendarId,
        error: rateLimitResponse.body
      });

      expect(errors).toHaveLength(1);
      expect(errors[0].error.error.code).toBe(429);
      expect(errors[0].error.error.message).toContain('Rate limit');
    });

    it('should handle network errors in batch requests', () => {
      const networkError = {
        statusCode: 0,
        headers: {},
        body: null,
        error: new Error('Network connection failed')
      };

      const calendarId = 'primary';
      const errors: Array<{ calendarId: string; error: any }> = [];

      errors.push({
        calendarId,
        error: networkError.error
      });

      expect(errors).toHaveLength(1);
      expect(errors[0].error.message).toContain('Network connection failed');
    });
  });

  describe('Integration Scenarios', () => {
    it('should handle maximum allowed calendars (50)', () => {
      const maxCalendars = Array(50).fill('cal').map((c, i) => `${c}${i}@example.com`);
      
      // Arrays must be passed as JSON strings in the new schema
      const input = {
        calendarId: JSON.stringify(maxCalendars),
        timeMin: '2024-01-01T00:00:00Z'
      };

      const result = ListEventsArgumentsSchema.safeParse(input);
      expect(result.success).toBe(true);
      expect(typeof result.data?.calendarId).toBe('string');
      // Verify the JSON string contains all 50 calendars
      const parsed = JSON.parse(result.data?.calendarId as string);
      expect(parsed).toHaveLength(50);
    });

    it('should prefer existing single calendar path for single array item', async () => {
      // When array has only one item, should use existing implementation
      const args = {
        calendarId: ['primary'], // Array with single item
        timeMin: '2024-01-01T00:00:00Z'
      };

      const mockEvents: ExtendedEvent[] = [
        {
          id: 'event1',
          summary: 'Single Calendar Event',
          start: { dateTime: '2024-01-15T10:00:00Z' },
          end: { dateTime: '2024-01-15T11:00:00Z' }
        }
      ];

      mockCalendarApi.events.list.mockResolvedValue({
        data: { items: mockEvents }
      });

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

      // Should call regular API, not batch
      expect(mockCalendarApi.events.list).toHaveBeenCalledWith({
        calendarId: 'primary',
        timeMin: args.timeMin,
        timeMax: undefined,
        singleEvents: true,
        orderBy: 'startTime'
      });

      // Should return structured JSON with events
      expect(result.content).toHaveLength(1);
      expect(result.content[0].type).toBe('text');
      const response = JSON.parse((result.content[0] as any).text);
      expect(response.events).toHaveLength(1);
      expect(response.totalCount).toBe(1);
      expect(response.events[0].id).toBe('event1');
    });
  });
}); 
```

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

```typescript
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { calendar_v3 } from 'googleapis';
import { RecurringEventHelpers } from '../../../handlers/core/RecurringEventHelpers.js';

describe('RecurringEventHelpers', () => {
  let helpers: RecurringEventHelpers;
  let mockCalendar: any;

  beforeEach(() => {
    mockCalendar = {
      events: {
        get: vi.fn(),
        patch: vi.fn(),
        insert: vi.fn()
      }
    };
    helpers = new RecurringEventHelpers(mockCalendar);
  });

  describe('detectEventType', () => {
    it('should detect recurring events', async () => {
      const mockEvent = {
        data: {
          id: 'event123',
          summary: 'Weekly Meeting',
          recurrence: ['RRULE:FREQ=WEEKLY;BYDAY=MO']
        }
      };
      mockCalendar.events.get.mockResolvedValue(mockEvent);

      const result = await helpers.detectEventType('event123', 'primary');
      expect(result).toBe('recurring');
      expect(mockCalendar.events.get).toHaveBeenCalledWith({
        calendarId: 'primary',
        eventId: 'event123'
      });
    });

    it('should detect single events', async () => {
      const mockEvent = {
        data: {
          id: 'event123',
          summary: 'One-time Meeting',
          // no recurrence property
        }
      };
      mockCalendar.events.get.mockResolvedValue(mockEvent);

      const result = await helpers.detectEventType('event123', 'primary');
      expect(result).toBe('single');
    });

    it('should detect single events with empty recurrence array', async () => {
      const mockEvent = {
        data: {
          id: 'event123',
          summary: 'One-time Meeting',
          recurrence: []
        }
      };
      mockCalendar.events.get.mockResolvedValue(mockEvent);

      const result = await helpers.detectEventType('event123', 'primary');
      expect(result).toBe('single');
    });

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

      await expect(helpers.detectEventType('invalid123', 'primary'))
        .rejects.toThrow('Event not found');
    });
  });

  describe('formatInstanceId', () => {
    const testCases = [
      {
        eventId: 'event123',
        originalStartTime: '2024-06-15T10:00:00-07:00',
        expected: 'event123_20240615T170000Z'
      },
      {
        eventId: 'meeting456',
        originalStartTime: '2024-12-31T23:59:59Z',
        expected: 'meeting456_20241231T235959Z'
      },
      {
        eventId: 'recurring_event',
        originalStartTime: '2024-06-15T14:30:00+05:30',
        expected: 'recurring_event_20240615T090000Z'
      }
    ];

    testCases.forEach(({ eventId, originalStartTime, expected }) => {
      it(`should format instance ID correctly for ${originalStartTime}`, () => {
        const result = helpers.formatInstanceId(eventId, originalStartTime);
        expect(result).toBe(expected);
      });
    });

    it('should handle datetime with milliseconds', () => {
      const result = helpers.formatInstanceId('event123', '2024-06-15T10:00:00.000Z');
      expect(result).toBe('event123_20240615T100000Z');
    });
  });

  describe('calculateUntilDate', () => {
    it('should calculate UNTIL date one day before future start date', () => {
      const futureStartDate = '2024-06-20T10:00:00-07:00';
      const result = helpers.calculateUntilDate(futureStartDate);
      
      // Should be June 19th, 2024 at 10:00:00 in basic format
      expect(result).toBe('20240619T170000Z');
    });

    it('should handle timezone conversions correctly', () => {
      const futureStartDate = '2024-06-20T00:00:00Z';
      const result = helpers.calculateUntilDate(futureStartDate);
      
      // Should be June 19th, 2024 at 00:00:00 in basic format
      expect(result).toBe('20240619T000000Z');
    });

    it('should handle different timezones', () => {
      const futureStartDate = '2024-06-20T10:00:00+05:30';
      const result = helpers.calculateUntilDate(futureStartDate);
      
      // Should be June 19th, 2024 at 04:30:00 UTC in basic format
      expect(result).toBe('20240619T043000Z');
    });
  });

  describe('calculateEndTime', () => {
    it('should calculate end time based on original duration', () => {
      const originalEvent: calendar_v3.Schema$Event = {
        start: { dateTime: '2024-06-15T10:00:00-07:00' },
        end: { dateTime: '2024-06-15T11:00:00-07:00' }
      };
      const newStartTime = '2024-06-15T14:00:00-07:00';

      const result = helpers.calculateEndTime(newStartTime, originalEvent);
      
      // Should preserve the 1 hour duration from original event
      expect(result).toBe('2024-06-15T22:00:00.000Z');
    });

    it('should handle different durations', () => {
      const originalEvent: calendar_v3.Schema$Event = {
        start: { dateTime: '2024-06-15T10:00:00Z' },
        end: { dateTime: '2024-06-15T12:30:00Z' } // 2.5 hour duration
      };
      const newStartTime = '2024-06-16T09:00:00Z';

      const result = helpers.calculateEndTime(newStartTime, originalEvent);
      
      // Should be 2.5 hours later
      expect(result).toBe('2024-06-16T11:30:00.000Z');
    });

    it('should handle cross-timezone calculations', () => {
      const originalEvent: calendar_v3.Schema$Event = {
        start: { dateTime: '2024-06-15T10:00:00-07:00' },
        end: { dateTime: '2024-06-15T11:00:00-07:00' }
      };
      const newStartTime = '2024-06-15T10:00:00+05:30';

      const result = helpers.calculateEndTime(newStartTime, originalEvent);
      
      // Should maintain 1 hour duration
      expect(result).toBe('2024-06-15T05:30:00.000Z');
    });
  });

  describe('updateRecurrenceWithUntil', () => {
    it('should add UNTIL clause to simple recurrence rule', () => {
      const recurrence = ['RRULE:FREQ=WEEKLY;BYDAY=MO'];
      const untilDate = '20240630T170000Z';

      const result = helpers.updateRecurrenceWithUntil(recurrence, untilDate);
      
      expect(result).toEqual(['RRULE:FREQ=WEEKLY;BYDAY=MO;UNTIL=20240630T170000Z']);
    });

    it('should replace existing UNTIL clause', () => {
      const recurrence = ['RRULE:FREQ=WEEKLY;BYDAY=MO;UNTIL=20240531T170000Z'];
      const untilDate = '20240630T170000Z';

      const result = helpers.updateRecurrenceWithUntil(recurrence, untilDate);
      
      expect(result).toEqual(['RRULE:FREQ=WEEKLY;BYDAY=MO;UNTIL=20240630T170000Z']);
    });

    it('should replace COUNT with UNTIL', () => {
      const recurrence = ['RRULE:FREQ=WEEKLY;BYDAY=MO;COUNT=10'];
      const untilDate = '20240630T170000Z';

      const result = helpers.updateRecurrenceWithUntil(recurrence, untilDate);
      
      expect(result).toEqual(['RRULE:FREQ=WEEKLY;BYDAY=MO;UNTIL=20240630T170000Z']);
    });

    it('should handle complex recurrence rules', () => {
      const recurrence = ['RRULE:FREQ=DAILY;INTERVAL=2;BYHOUR=10;BYMINUTE=0;COUNT=20'];
      const untilDate = '20240630T170000Z';

      const result = helpers.updateRecurrenceWithUntil(recurrence, untilDate);
      
      expect(result).toEqual(['RRULE:FREQ=DAILY;INTERVAL=2;BYHOUR=10;BYMINUTE=0;UNTIL=20240630T170000Z']);
    });

    it('should throw error for empty recurrence', () => {
      expect(() => helpers.updateRecurrenceWithUntil([], '20240630T170000Z'))
        .toThrow('No recurrence rule found');
      
      expect(() => helpers.updateRecurrenceWithUntil(undefined as any, '20240630T170000Z'))
        .toThrow('No recurrence rule found');
    });

    it('should handle recurrence with EXDATE rules', () => {
      const recurrence = [
        'RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR',
        'EXDATE:20240610T100000Z',
        'EXDATE:20240612T100000Z'
      ];
      const untilDate = '20240630T170000Z';

      const result = helpers.updateRecurrenceWithUntil(recurrence, untilDate);
      
      expect(result).toEqual([
        'RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR;UNTIL=20240630T170000Z',
        'EXDATE:20240610T100000Z',
        'EXDATE:20240612T100000Z'
      ]);
    });

    it('should handle EXDATE rules appearing before RRULE', () => {
      const recurrence = [
        'EXDATE:20240610T100000Z',
        'RRULE:FREQ=WEEKLY;BYDAY=MO',
        'EXDATE:20240612T100000Z'
      ];
      const untilDate = '20240630T170000Z';

      const result = helpers.updateRecurrenceWithUntil(recurrence, untilDate);
      
      expect(result).toEqual([
        'EXDATE:20240610T100000Z',
        'RRULE:FREQ=WEEKLY;BYDAY=MO;UNTIL=20240630T170000Z',
        'EXDATE:20240612T100000Z'
      ]);
    });

    it('should throw error when no RRULE found', () => {
      const recurrence = [
        'EXDATE:20240610T100000Z',
        'EXDATE:20240612T100000Z'
      ];
      const untilDate = '20240630T170000Z';

      expect(() => helpers.updateRecurrenceWithUntil(recurrence, untilDate))
        .toThrow('No RRULE found in recurrence rules');
    });

    it('should handle complex recurrence with multiple EXDATE rules as reported in user issue', () => {
      // This test case reproduces the exact scenario from the user's error
      const recurrence = [
        'EXDATE;TZID=America/Los_Angeles:20250702T130500',
        'EXDATE;TZID=America/Los_Angeles:20250704T130500',
        'EXDATE;TZID=America/Los_Angeles:20250707T130500',
        'RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR',
        'EXDATE;TZID=America/Los_Angeles:20250709T130500',
        'EXDATE;TZID=America/Los_Angeles:20250711T130500'
      ];
      const untilDate = '20251102T210500Z';

      const result = helpers.updateRecurrenceWithUntil(recurrence, untilDate);
      
      // Should preserve all EXDATE rules and only modify the RRULE
      expect(result).toEqual([
        'EXDATE;TZID=America/Los_Angeles:20250702T130500',
        'EXDATE;TZID=America/Los_Angeles:20250704T130500',
        'EXDATE;TZID=America/Los_Angeles:20250707T130500',
        'RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR;UNTIL=20251102T210500Z',
        'EXDATE;TZID=America/Los_Angeles:20250709T130500',
        'EXDATE;TZID=America/Los_Angeles:20250711T130500'
      ]);
    });
  });

  describe('cleanEventForDuplication', () => {
    it('should remove system-generated fields', () => {
      const originalEvent: calendar_v3.Schema$Event = {
        id: 'event123',
        etag: '"abc123"',
        iCalUID: '[email protected]',
        created: '2024-01-01T00:00:00Z',
        updated: '2024-01-01T00:00:00Z',
        htmlLink: 'https://calendar.google.com/event?eid=...',
        hangoutLink: 'https://meet.google.com/...',
        summary: 'Meeting',
        description: 'Meeting description',
        location: 'Conference Room',
        start: { dateTime: '2024-06-15T10:00:00Z' },
        end: { dateTime: '2024-06-15T11:00:00Z' }
      };

      const result = helpers.cleanEventForDuplication(originalEvent);

      // Should remove system fields
      expect(result.id).toBeUndefined();
      expect(result.etag).toBeUndefined();
      expect(result.iCalUID).toBeUndefined();
      expect(result.created).toBeUndefined();
      expect(result.updated).toBeUndefined();
      expect(result.htmlLink).toBeUndefined();
      expect(result.hangoutLink).toBeUndefined();

      // Should preserve user fields
      expect(result.summary).toBe('Meeting');
      expect(result.description).toBe('Meeting description');
      expect(result.location).toBe('Conference Room');
      expect(result.start).toEqual({ dateTime: '2024-06-15T10:00:00Z' });
      expect(result.end).toEqual({ dateTime: '2024-06-15T11:00:00Z' });
    });

    it('should not modify original event object', () => {
      const originalEvent: calendar_v3.Schema$Event = {
        id: 'event123',
        summary: 'Meeting'
      };

      const result = helpers.cleanEventForDuplication(originalEvent);

      // Original should be unchanged
      expect(originalEvent.id).toBe('event123');
      // Result should be cleaned
      expect(result.id).toBeUndefined();
      expect(result.summary).toBe('Meeting');
    });
  });

  describe('buildUpdateRequestBody', () => {
    it('should build request body with provided fields', () => {
      const args = {
        summary: 'Updated Meeting',
        description: 'Updated description',
        location: 'New Location',
        colorId: '9'
        // No timeZone or start/end - these should not be added
      };

      const result = helpers.buildUpdateRequestBody(args);

      expect(result).toEqual({
        summary: 'Updated Meeting',
        description: 'Updated description',
        location: 'New Location',
        colorId: '9'
        // No start/end should be present
      });
    });

    it('should handle time changes correctly', () => {
      const args = {
        start: '2024-06-15T10:00:00-07:00',
        end: '2024-06-15T11:00:00-07:00',
        timeZone: 'America/Los_Angeles',
        summary: 'Meeting'
      };

      const result = helpers.buildUpdateRequestBody(args);

      expect(result).toEqual({
        summary: 'Meeting',
        start: {
          dateTime: '2024-06-15T10:00:00-07:00',
          date: null
          // No timeZone when datetime already includes timezone
        },
        end: {
          dateTime: '2024-06-15T11:00:00-07:00',
          date: null
          // No timeZone when datetime already includes timezone
        }
      });
    });

    it('should handle partial time changes', () => {
      const args = {
        start: '2024-06-15T10:00:00-07:00',
        // no end provided
        timeZone: 'America/Los_Angeles',
        summary: 'Meeting'
      };

      const result = helpers.buildUpdateRequestBody(args);

      expect(result.start).toEqual({
        dateTime: '2024-06-15T10:00:00-07:00',
        date: null
        // No timeZone when datetime already includes timezone
      });
      expect(result.end).toBeUndefined();
    });

    it('should use default timezone when no timezone provided', () => {
      const args = {
        start: '2024-06-15T10:00:00',
        end: '2024-06-15T11:00:00',
        summary: 'Meeting'
      };

      const defaultTimeZone = 'Europe/London';
      const result = helpers.buildUpdateRequestBody(args, defaultTimeZone);

      expect(result).toEqual({
        summary: 'Meeting',
        start: {
          dateTime: '2024-06-15T10:00:00',
          timeZone: 'Europe/London',
          date: null
        },
        end: {
          dateTime: '2024-06-15T11:00:00',
          timeZone: 'Europe/London',
          date: null
        }
      });
    });

    it('should handle attendees and reminders', () => {
      const args = {
        attendees: [
          { email: '[email protected]' },
          { email: '[email protected]' }
        ],
        reminders: {
          useDefault: false,
          overrides: [
            { method: 'email', minutes: 1440 },
            { method: 'popup', minutes: 10 }
          ]
        },
        timeZone: 'UTC'
      };

      const result = helpers.buildUpdateRequestBody(args);

      expect(result.attendees).toEqual(args.attendees);
      expect(result.reminders).toEqual(args.reminders);
    });

    it('should not include undefined fields', () => {
      const args = {
        summary: 'Meeting',
        description: undefined,
        location: null,
        timeZone: 'UTC'
      };

      const result = helpers.buildUpdateRequestBody(args);

      expect(result.summary).toBe('Meeting');
      expect('description' in result).toBe(false);
      expect('location' in result).toBe(false);
    });
  });

  describe('Edge Cases and Boundary Conditions', () => {
    it('should handle leap year dates correctly in formatInstanceId', () => {
      const leapYearCases = [
        {
          eventId: 'leap123',
          originalStartTime: '2024-02-29T10:00:00Z', // Leap year
          expected: 'leap123_20240229T100000Z'
        },
        {
          eventId: 'leap456',
          originalStartTime: '2024-02-29T23:59:59-12:00', // Edge timezone
          expected: 'leap456_20240301T115959Z'
        }
      ];

      leapYearCases.forEach(({ eventId, originalStartTime, expected }) => {
        const result = helpers.formatInstanceId(eventId, originalStartTime);
        expect(result).toBe(expected);
      });
    });

    it('should handle extreme timezone offsets in formatInstanceId', () => {
      const extremeTimezoneCases = [
        {
          eventId: 'extreme1',
          originalStartTime: '2024-06-15T10:00:00+14:00', // UTC+14 (Kiribati)
          expected: 'extreme1_20240614T200000Z'
        },
        {
          eventId: 'extreme2',
          originalStartTime: '2024-06-15T10:00:00-12:00', // UTC-12 (Baker Island)
          expected: 'extreme2_20240615T220000Z'
        }
      ];

      extremeTimezoneCases.forEach(({ eventId, originalStartTime, expected }) => {
        const result = helpers.formatInstanceId(eventId, originalStartTime);
        expect(result).toBe(expected);
      });
    });

    it('should handle calculateUntilDate with edge dates', () => {
      const edgeCases = [
        {
          futureStartDate: '2024-01-01T00:00:00Z', // New Year
          expected: '20231231T000000Z'
        },
        {
          futureStartDate: '2024-12-31T23:59:59Z', // End of year
          expected: '20241230T235959Z'
        },
        {
          futureStartDate: '2024-03-01T00:00:00Z', // Day after leap day
          expected: '20240229T000000Z'
        }
      ];

      edgeCases.forEach(({ futureStartDate, expected }) => {
        const result = helpers.calculateUntilDate(futureStartDate);
        expect(result).toBe(expected);
      });
    });

    it('should handle calculateEndTime with very short and very long durations', () => {
      // Very short duration (1 minute)
      const shortDurationEvent: calendar_v3.Schema$Event = {
        start: { dateTime: '2024-06-15T10:00:00Z' },
        end: { dateTime: '2024-06-15T10:01:00Z' }
      };
      const shortResult = helpers.calculateEndTime('2024-06-16T15:30:00Z', shortDurationEvent);
      expect(shortResult).toBe('2024-06-16T15:31:00.000Z');

      // Very long duration (8 hours)
      const longDurationEvent: calendar_v3.Schema$Event = {
        start: { dateTime: '2024-06-15T09:00:00Z' },
        end: { dateTime: '2024-06-15T17:00:00Z' }
      };
      const longResult = helpers.calculateEndTime('2024-06-16T10:00:00Z', longDurationEvent);
      expect(longResult).toBe('2024-06-16T18:00:00.000Z');

      // Multi-day duration
      const multiDayEvent: calendar_v3.Schema$Event = {
        start: { dateTime: '2024-06-15T10:00:00Z' },
        end: { dateTime: '2024-06-17T10:00:00Z' } // 48 hours
      };
      const multiDayResult = helpers.calculateEndTime('2024-06-20T10:00:00Z', multiDayEvent);
      expect(multiDayResult).toBe('2024-06-22T10:00:00.000Z');
    });

    it('should handle updateRecurrenceWithUntil with various RRULE formats', () => {
      const complexRRuleCases = [
        {
          original: ['RRULE:FREQ=MONTHLY;BYMONTHDAY=15;BYHOUR=10;BYMINUTE=30'],
          untilDate: '20241215T103000Z',
          expected: ['RRULE:FREQ=MONTHLY;BYMONTHDAY=15;BYHOUR=10;BYMINUTE=30;UNTIL=20241215T103000Z']
        },
        {
          original: ['RRULE:FREQ=YEARLY;BYMONTH=6;BYMONTHDAY=15;COUNT=5'],
          untilDate: '20291215T103000Z',
          expected: ['RRULE:FREQ=YEARLY;BYMONTH=6;BYMONTHDAY=15;UNTIL=20291215T103000Z']
        },
        {
          original: ['RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR;INTERVAL=2;UNTIL=20241201T100000Z'],
          untilDate: '20241115T100000Z',
          expected: ['RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR;INTERVAL=2;UNTIL=20241115T100000Z']
        }
      ];

      complexRRuleCases.forEach(({ original, untilDate, expected }) => {
        const result = helpers.updateRecurrenceWithUntil(original, untilDate);
        expect(result).toEqual(expected);
      });
    });

    it('should handle cleanEventForDuplication with all possible system fields', () => {
      const eventWithAllSystemFields: calendar_v3.Schema$Event = {
        id: 'event123',
        etag: '"abc123"',
        iCalUID: '[email protected]',
        created: '2024-01-01T00:00:00Z',
        updated: '2024-01-01T00:00:00Z',
        htmlLink: 'https://calendar.google.com/event?eid=...',
        hangoutLink: 'https://meet.google.com/...',
        conferenceData: { entryPoints: [] },
        creator: { email: '[email protected]' },
        organizer: { email: '[email protected]' },
        sequence: 1,
        status: 'confirmed',
        transparency: 'opaque',
        visibility: 'default',
        // User fields that should be preserved
        summary: 'Meeting',
        description: 'Meeting description',
        location: 'Conference Room',
        start: { dateTime: '2024-06-15T10:00:00Z' },
        end: { dateTime: '2024-06-15T11:00:00Z' },
        attendees: [{ email: '[email protected]' }],
        recurrence: ['RRULE:FREQ=WEEKLY']
      };

      const result = helpers.cleanEventForDuplication(eventWithAllSystemFields);

      // Should remove all system fields
      expect(result.id).toBeUndefined();
      expect(result.etag).toBeUndefined();
      expect(result.iCalUID).toBeUndefined();
      expect(result.created).toBeUndefined();
      expect(result.updated).toBeUndefined();
      expect(result.htmlLink).toBeUndefined();
      expect(result.hangoutLink).toBeUndefined();

      // Should preserve user fields
      expect(result.summary).toBe('Meeting');
      expect(result.description).toBe('Meeting description');
      expect(result.location).toBe('Conference Room');
      expect(result.attendees).toEqual([{ email: '[email protected]' }]);
      expect(result.recurrence).toEqual(['RRULE:FREQ=WEEKLY']);
    });

    it('should handle buildUpdateRequestBody with complex nested objects', () => {
      const complexArgs = {
        summary: 'Complex Meeting',
        attendees: [
          { 
            email: '[email protected]',
            displayName: 'User One',
            responseStatus: 'accepted'
          },
          { 
            email: '[email protected]',
            displayName: 'User Two',
            responseStatus: 'tentative'
          }
        ],
        reminders: {
          useDefault: false,
          overrides: [
            { method: 'email', minutes: 1440 },
            { method: 'popup', minutes: 10 },
            { method: 'sms', minutes: 60 }
          ]
        },
        recurrence: [
          'RRULE:FREQ=WEEKLY;BYDAY=MO',
          'EXDATE:20240610T100000Z'
        ],
        timeZone: 'America/Los_Angeles'
      };

      const result = helpers.buildUpdateRequestBody(complexArgs);

      expect(result.attendees).toEqual(complexArgs.attendees);
      expect(result.reminders).toEqual(complexArgs.reminders);
      expect(result.recurrence).toEqual(complexArgs.recurrence);
      // No start/end should be added when only timezone is provided without start/end values
      expect(result.start).toBeUndefined();
      expect(result.end).toBeUndefined();
    });

    it('should handle buildUpdateRequestBody with mixed null, undefined, and valid values', () => {
      const mixedArgs = {
        summary: 'Valid Summary',
        description: null,
        location: undefined,
        colorId: '',
        attendees: [],
        reminders: null,
        start: '2024-06-15T10:00:00Z',
        end: null,
        timeZone: 'UTC'
      };

      const result = helpers.buildUpdateRequestBody(mixedArgs);

      expect(result.summary).toBe('Valid Summary');
      expect('description' in result).toBe(false);
      expect('location' in result).toBe(false);
      expect(result.colorId).toBe(''); // Empty string should be included
      expect(result.attendees).toEqual([]); // Empty array should be included
      expect('reminders' in result).toBe(false);
      expect(result.start).toEqual({
        dateTime: '2024-06-15T10:00:00Z',
        date: null
        // No timeZone when datetime already includes timezone (Z suffix)
      });
      expect(result.end).toBeUndefined(); // end is null, so should not be set
    });
  });
}); 
```
Page 3/5FirstPrevNextLast