- 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.
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