Files
automaker/plan/cursor-cli-integration/phases/phase-10-testing.md
Kacper 81f35ad6aa chore: Add Cursor CLI integration plan and phases
- Introduced a comprehensive integration plan for the Cursor CLI, including detailed phases for implementation.
- Created initial markdown files for each phase, outlining objectives, tasks, and verification steps.
- Established a global prompt template for starting new sessions with the Cursor CLI.
- Added necessary types and configuration for Cursor models and their integration into the AutoMaker architecture.
- Implemented routing logic to ensure proper model handling between Cursor and Claude providers.
- Developed UI components for setup and settings management related to Cursor integration.
- Included extensive testing and validation plans to ensure robust functionality across all scenarios.
2025-12-27 23:50:17 +01:00

18 KiB

Phase 10: Testing & Validation

Status: pending Dependencies: All previous phases Estimated Effort: Medium (comprehensive testing)


Objective

Create comprehensive tests and perform validation to ensure the Cursor CLI integration works correctly across all scenarios.


Tasks

Task 10.1: Unit Tests - Cursor Provider

Status: pending

File: apps/server/tests/unit/providers/cursor-provider.test.ts

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { CursorProvider, CursorErrorCode } from '../../../src/providers/cursor-provider';
import { execSync, spawn } from 'child_process';
import * as fs from 'fs';

// Mock child_process
vi.mock('child_process', () => ({
  execSync: vi.fn(),
  spawn: vi.fn(),
}));

// Mock fs
vi.mock('fs', () => ({
  existsSync: vi.fn(),
  readFileSync: vi.fn(),
}));

describe('CursorProvider', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  describe('getName', () => {
    it('should return "cursor"', () => {
      const provider = new CursorProvider();
      expect(provider.getName()).toBe('cursor');
    });
  });

  describe('isInstalled', () => {
    it('should return true when CLI is found in PATH', async () => {
      vi.mocked(execSync).mockReturnValue('/usr/local/bin/cursor-agent\n');
      vi.mocked(fs.existsSync).mockReturnValue(true);

      const provider = new CursorProvider();
      const result = await provider.isInstalled();

      expect(result).toBe(true);
    });

    it('should return false when CLI is not found', async () => {
      vi.mocked(execSync).mockImplementation(() => {
        throw new Error('not found');
      });
      vi.mocked(fs.existsSync).mockReturnValue(false);

      const provider = new CursorProvider();
      const result = await provider.isInstalled();

      expect(result).toBe(false);
    });
  });

  describe('checkAuth', () => {
    it('should detect API key authentication', async () => {
      process.env.CURSOR_API_KEY = 'test-key';

      const provider = new CursorProvider();
      const result = await provider.checkAuth();

      expect(result.authenticated).toBe(true);
      expect(result.method).toBe('api_key');

      delete process.env.CURSOR_API_KEY;
    });

    it('should detect login authentication from credentials file', async () => {
      vi.mocked(fs.existsSync).mockReturnValue(true);
      vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ accessToken: 'token' }));

      const provider = new CursorProvider();
      const result = await provider.checkAuth();

      expect(result.authenticated).toBe(true);
      expect(result.method).toBe('login');
    });

    it('should return not authenticated when no credentials', async () => {
      vi.mocked(fs.existsSync).mockReturnValue(false);

      const provider = new CursorProvider();
      const result = await provider.checkAuth();

      expect(result.authenticated).toBe(false);
      expect(result.method).toBe('none');
    });
  });

  describe('parseStreamLine', () => {
    it('should parse valid JSON event', () => {
      const provider = new CursorProvider();
      const line = '{"type":"system","subtype":"init","session_id":"abc"}';

      const result = (provider as any).parseStreamLine(line);

      expect(result).toEqual({
        type: 'system',
        subtype: 'init',
        session_id: 'abc',
      });
    });

    it('should return null for invalid JSON', () => {
      const provider = new CursorProvider();
      const result = (provider as any).parseStreamLine('not json');

      expect(result).toBeNull();
    });

    it('should return null for empty lines', () => {
      const provider = new CursorProvider();
      expect((provider as any).parseStreamLine('')).toBeNull();
      expect((provider as any).parseStreamLine('   ')).toBeNull();
    });
  });

  describe('mapError', () => {
    it('should map authentication errors', () => {
      const provider = new CursorProvider();

      const error = (provider as any).mapError('Error: not authenticated', 1);

      expect(error.code).toBe(CursorErrorCode.NOT_AUTHENTICATED);
      expect(error.recoverable).toBe(true);
      expect(error.suggestion).toBeDefined();
    });

    it('should map rate limit errors', () => {
      const provider = new CursorProvider();

      const error = (provider as any).mapError('Rate limit exceeded', 1);

      expect(error.code).toBe(CursorErrorCode.RATE_LIMITED);
      expect(error.recoverable).toBe(true);
    });

    it('should map network errors', () => {
      const provider = new CursorProvider();

      const error = (provider as any).mapError('ECONNREFUSED', 1);

      expect(error.code).toBe(CursorErrorCode.NETWORK_ERROR);
      expect(error.recoverable).toBe(true);
    });

    it('should return unknown error for unrecognized messages', () => {
      const provider = new CursorProvider();

      const error = (provider as any).mapError('Something weird happened', 1);

      expect(error.code).toBe(CursorErrorCode.UNKNOWN);
    });
  });

  describe('getAvailableModels', () => {
    it('should return all Cursor models', () => {
      const provider = new CursorProvider();
      const models = provider.getAvailableModels();

      expect(models.length).toBeGreaterThan(0);
      expect(models.every((m) => m.provider === 'cursor')).toBe(true);
      expect(models.some((m) => m.id.includes('auto'))).toBe(true);
    });
  });
});

Task 10.2: Unit Tests - Provider Factory

Status: pending

File: apps/server/tests/unit/providers/provider-factory.test.ts

import { describe, it, expect } from 'vitest';
import { ProviderFactory } from '../../../src/providers/provider-factory';
import { ClaudeProvider } from '../../../src/providers/claude-provider';
import { CursorProvider } from '../../../src/providers/cursor-provider';

describe('ProviderFactory', () => {
  describe('getProviderNameForModel', () => {
    it('should route cursor-prefixed models to cursor', () => {
      expect(ProviderFactory.getProviderNameForModel('cursor-auto')).toBe('cursor');
      expect(ProviderFactory.getProviderNameForModel('cursor-gpt-4o')).toBe('cursor');
      expect(ProviderFactory.getProviderNameForModel('cursor-claude-sonnet-4')).toBe('cursor');
    });

    it('should route claude models to claude', () => {
      expect(ProviderFactory.getProviderNameForModel('claude-sonnet-4')).toBe('claude');
      expect(ProviderFactory.getProviderNameForModel('opus')).toBe('claude');
      expect(ProviderFactory.getProviderNameForModel('sonnet')).toBe('claude');
      expect(ProviderFactory.getProviderNameForModel('haiku')).toBe('claude');
    });

    it('should default unknown models to claude', () => {
      expect(ProviderFactory.getProviderNameForModel('unknown-model')).toBe('claude');
      expect(ProviderFactory.getProviderNameForModel('random')).toBe('claude');
    });
  });

  describe('getProviderForModel', () => {
    it('should return CursorProvider for cursor models', () => {
      const provider = ProviderFactory.getProviderForModel('cursor-auto');
      expect(provider).toBeInstanceOf(CursorProvider);
      expect(provider.getName()).toBe('cursor');
    });

    it('should return ClaudeProvider for claude models', () => {
      const provider = ProviderFactory.getProviderForModel('sonnet');
      expect(provider).toBeInstanceOf(ClaudeProvider);
      expect(provider.getName()).toBe('claude');
    });
  });

  describe('getAllProviders', () => {
    it('should return both providers', () => {
      const providers = ProviderFactory.getAllProviders();
      const names = providers.map((p) => p.getName());

      expect(names).toContain('claude');
      expect(names).toContain('cursor');
    });
  });

  describe('getProviderByName', () => {
    it('should return correct provider by name', () => {
      expect(ProviderFactory.getProviderByName('cursor')?.getName()).toBe('cursor');
      expect(ProviderFactory.getProviderByName('claude')?.getName()).toBe('claude');
      expect(ProviderFactory.getProviderByName('unknown')).toBeNull();
    });
  });

  describe('getAllAvailableModels', () => {
    it('should include models from all providers', () => {
      const models = ProviderFactory.getAllAvailableModels();

      const cursorModels = models.filter((m) => m.provider === 'cursor');
      const claudeModels = models.filter((m) => m.provider === 'claude');

      expect(cursorModels.length).toBeGreaterThan(0);
      expect(claudeModels.length).toBeGreaterThan(0);
    });
  });
});

Task 10.3: Unit Tests - Types

Status: pending

File: libs/types/tests/cursor-types.test.ts

import { describe, it, expect } from 'vitest';
import {
  CURSOR_MODEL_MAP,
  cursorModelHasThinking,
  getCursorModelLabel,
  getAllCursorModelIds,
  CursorModelId,
} from '../src/cursor-models';
import { profileHasThinking, getProfileModelString, AIProfile } from '../src/settings';

describe('Cursor Model Types', () => {
  describe('CURSOR_MODEL_MAP', () => {
    it('should have all required models', () => {
      const requiredModels: CursorModelId[] = [
        'auto',
        'claude-sonnet-4',
        'claude-sonnet-4-thinking',
        'gpt-4o',
        'gpt-4o-mini',
      ];

      for (const model of requiredModels) {
        expect(CURSOR_MODEL_MAP[model]).toBeDefined();
        expect(CURSOR_MODEL_MAP[model].id).toBe(model);
      }
    });

    it('should have valid tier values', () => {
      for (const config of Object.values(CURSOR_MODEL_MAP)) {
        expect(['free', 'pro']).toContain(config.tier);
      }
    });
  });

  describe('cursorModelHasThinking', () => {
    it('should return true for thinking models', () => {
      expect(cursorModelHasThinking('claude-sonnet-4-thinking')).toBe(true);
      expect(cursorModelHasThinking('o3-mini')).toBe(true);
    });

    it('should return false for non-thinking models', () => {
      expect(cursorModelHasThinking('auto')).toBe(false);
      expect(cursorModelHasThinking('gpt-4o')).toBe(false);
      expect(cursorModelHasThinking('claude-sonnet-4')).toBe(false);
    });
  });

  describe('getCursorModelLabel', () => {
    it('should return correct labels', () => {
      expect(getCursorModelLabel('auto')).toBe('Auto (Recommended)');
      expect(getCursorModelLabel('gpt-4o')).toBe('GPT-4o');
    });

    it('should return model ID for unknown models', () => {
      expect(getCursorModelLabel('unknown' as CursorModelId)).toBe('unknown');
    });
  });
});

describe('Profile Helpers', () => {
  describe('profileHasThinking', () => {
    it('should detect Claude thinking levels', () => {
      const profile: AIProfile = {
        id: '1',
        name: 'Test',
        description: '',
        provider: 'claude',
        model: 'sonnet',
        thinkingLevel: 'high',
        isBuiltIn: false,
      };
      expect(profileHasThinking(profile)).toBe(true);

      profile.thinkingLevel = 'none';
      expect(profileHasThinking(profile)).toBe(false);
    });

    it('should detect Cursor thinking models', () => {
      const profile: AIProfile = {
        id: '1',
        name: 'Test',
        description: '',
        provider: 'cursor',
        cursorModel: 'claude-sonnet-4-thinking',
        isBuiltIn: false,
      };
      expect(profileHasThinking(profile)).toBe(true);

      profile.cursorModel = 'gpt-4o';
      expect(profileHasThinking(profile)).toBe(false);
    });
  });

  describe('getProfileModelString', () => {
    it('should format Cursor models correctly', () => {
      const profile: AIProfile = {
        id: '1',
        name: 'Test',
        description: '',
        provider: 'cursor',
        cursorModel: 'gpt-4o',
        isBuiltIn: false,
      };
      expect(getProfileModelString(profile)).toBe('cursor-gpt-4o');
    });

    it('should format Claude models correctly', () => {
      const profile: AIProfile = {
        id: '1',
        name: 'Test',
        description: '',
        provider: 'claude',
        model: 'sonnet',
        isBuiltIn: false,
      };
      expect(getProfileModelString(profile)).toBe('sonnet');
    });
  });
});

Task 10.4: Integration Tests

Status: pending

File: apps/server/tests/integration/cursor-integration.test.ts

import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { CursorProvider } from '../../src/providers/cursor-provider';
import { ProviderFactory } from '../../src/providers/provider-factory';

describe('Cursor Integration (requires cursor-agent)', () => {
  let provider: CursorProvider;
  let isInstalled: boolean;

  beforeAll(async () => {
    provider = new CursorProvider();
    isInstalled = await provider.isInstalled();
  });

  describe('when cursor-agent is installed', () => {
    it.skipIf(!isInstalled)('should get version', async () => {
      const version = await provider.getVersion();
      expect(version).toBeTruthy();
      expect(typeof version).toBe('string');
    });

    it.skipIf(!isInstalled)('should check auth status', async () => {
      const auth = await provider.checkAuth();
      expect(auth).toHaveProperty('authenticated');
      expect(auth).toHaveProperty('method');
    });

    it.skipIf(!isInstalled)('should detect installation', async () => {
      const status = await provider.detectInstallation();
      expect(status.installed).toBe(true);
      expect(status.path).toBeTruthy();
    });
  });

  describe('when cursor-agent is not installed', () => {
    it.skipIf(isInstalled)('should report not installed', async () => {
      const status = await provider.detectInstallation();
      expect(status.installed).toBe(false);
    });
  });
});

Task 10.5: E2E Tests

Status: pending

File: apps/ui/tests/e2e/cursor-setup.spec.ts

import { test, expect } from '@playwright/test';

test.describe('Cursor Setup Wizard', () => {
  test('should show Cursor setup step', async ({ page }) => {
    // Navigate to setup (fresh install)
    await page.goto('/setup');

    // Wait for Cursor step to appear
    await expect(page.getByText('Cursor CLI Setup')).toBeVisible();
    await expect(page.getByText('Optional')).toBeVisible();
  });

  test('should allow skipping Cursor setup', async ({ page }) => {
    await page.goto('/setup');

    // Find and click skip button
    await page.getByRole('button', { name: 'Skip for now' }).click();

    // Should proceed to next step
    await expect(page.getByText('Cursor CLI Setup')).not.toBeVisible();
  });

  test('should show installation instructions when not installed', async ({ page }) => {
    await page.goto('/setup');

    // Check for install command
    await expect(page.getByText('curl https://cursor.com/install')).toBeVisible();
  });
});

test.describe('Cursor Settings', () => {
  test('should show Cursor tab in settings', async ({ page }) => {
    await page.goto('/settings/providers');

    // Should have tabs for both providers
    await expect(page.getByRole('tab', { name: 'Claude' })).toBeVisible();
    await expect(page.getByRole('tab', { name: 'Cursor' })).toBeVisible();
  });

  test('should switch between provider tabs', async ({ page }) => {
    await page.goto('/settings/providers');

    // Click Cursor tab
    await page.getByRole('tab', { name: 'Cursor' }).click();

    // Should show Cursor settings
    await expect(page.getByText('Cursor CLI Status')).toBeVisible();
  });
});

Task 10.6: Manual Testing Checklist

Status: pending

Create a manual testing checklist:

## Manual Testing Checklist

### Setup Flow

- [ ] Fresh install shows Cursor step
- [ ] Can skip Cursor setup
- [ ] Installation status is accurate
- [ ] Login flow works (copy command, poll for auth)
- [ ] Refresh button updates status

### Settings

- [ ] Provider tabs work
- [ ] Cursor status shows correctly
- [ ] Model selection works
- [ ] Default model saves
- [ ] Enabled models save

### Profiles

- [ ] Can create Cursor profile
- [ ] Provider switch resets options
- [ ] Cursor models show thinking badge
- [ ] Built-in Cursor profiles appear
- [ ] Profile cards show provider info

### Execution

- [ ] Tasks with Cursor models execute
- [ ] Streaming works correctly
- [ ] Tool calls are displayed
- [ ] Errors show suggestions
- [ ] Can abort Cursor tasks

### Log Viewer

- [ ] Cursor events parsed correctly
- [ ] Tool calls categorized
- [ ] File paths highlighted
- [ ] Provider badge shown

### Edge Cases

- [ ] Switch provider mid-session
- [ ] Cursor not installed handling
- [ ] Network errors handled
- [ ] Rate limiting handled
- [ ] Auth expired handling

Verification

Test 1: Run All Unit Tests

pnpm test:unit

All tests should pass.

Test 2: Run Integration Tests

pnpm test:integration

Tests requiring cursor-agent will be skipped if not installed.

Test 3: Run E2E Tests

pnpm test:e2e

Browser tests should pass.

Test 4: Type Check

pnpm typecheck

No TypeScript errors.

Test 5: Lint Check

pnpm lint

No linting errors.

Test 6: Build

pnpm build

Build should succeed without errors.


Verification Checklist

Before marking this phase complete:

  • Unit tests pass (cursor-provider)
  • Unit tests pass (provider-factory)
  • Unit tests pass (types)
  • Integration tests pass (or skip if not installed)
  • E2E tests pass
  • Manual testing checklist completed
  • No TypeScript errors
  • No linting errors
  • Build succeeds
  • Documentation updated

Files Changed

File Action Description
apps/server/tests/unit/providers/cursor-provider.test.ts Create Provider tests
apps/server/tests/unit/providers/provider-factory.test.ts Create Factory tests
libs/types/tests/cursor-types.test.ts Create Type tests
apps/server/tests/integration/cursor-integration.test.ts Create Integration tests
apps/ui/tests/e2e/cursor-setup.spec.ts Create E2E tests

Notes

  • Integration tests may be skipped if cursor-agent is not installed
  • E2E tests should work regardless of cursor-agent installation
  • Manual testing should cover both installed and not-installed scenarios