import { Cline } from '../Cline'; import { ClineProvider } from '../webview/ClineProvider'; import { ApiConfiguration } from '../../shared/api'; import * as vscode from 'vscode'; // Mock all MCP-related modules jest.mock('@modelcontextprotocol/sdk/types.js', () => ({ CallToolResultSchema: {}, ListResourcesResultSchema: {}, ListResourceTemplatesResultSchema: {}, ListToolsResultSchema: {}, ReadResourceResultSchema: {}, ErrorCode: { InvalidRequest: 'InvalidRequest', MethodNotFound: 'MethodNotFound', InternalError: 'InternalError' }, McpError: class McpError extends Error { code: string; constructor(code: string, message: string) { super(message); this.code = code; this.name = 'McpError'; } } }), { virtual: true }); jest.mock('@modelcontextprotocol/sdk/client/index.js', () => ({ Client: jest.fn().mockImplementation(() => ({ connect: jest.fn().mockResolvedValue(undefined), close: jest.fn().mockResolvedValue(undefined), listTools: jest.fn().mockResolvedValue({ tools: [] }), callTool: jest.fn().mockResolvedValue({ content: [] }) })) }), { virtual: true }); jest.mock('@modelcontextprotocol/sdk/client/stdio.js', () => ({ StdioClientTransport: jest.fn().mockImplementation(() => ({ connect: jest.fn().mockResolvedValue(undefined), close: jest.fn().mockResolvedValue(undefined) })) }), { virtual: true }); // Mock fileExistsAtPath jest.mock('../../utils/fs', () => ({ fileExistsAtPath: jest.fn().mockImplementation((filePath) => { return filePath.includes('ui_messages.json') || filePath.includes('api_conversation_history.json'); }) })); // Mock fs/promises const mockMessages = [{ ts: Date.now(), type: 'say', say: 'text', text: 'historical task' }]; jest.mock('fs/promises', () => ({ mkdir: jest.fn().mockResolvedValue(undefined), writeFile: jest.fn().mockResolvedValue(undefined), readFile: jest.fn().mockImplementation((filePath) => { if (filePath.includes('ui_messages.json')) { return Promise.resolve(JSON.stringify(mockMessages)); } if (filePath.includes('api_conversation_history.json')) { return Promise.resolve('[]'); } return Promise.resolve('[]'); }), unlink: jest.fn().mockResolvedValue(undefined), rmdir: jest.fn().mockResolvedValue(undefined) })); // Mock dependencies jest.mock('vscode', () => { const mockDisposable = { dispose: jest.fn() }; const mockEventEmitter = { event: jest.fn(), fire: jest.fn() }; const mockTextDocument = { uri: { fsPath: '/mock/workspace/path/file.ts' } }; const mockTextEditor = { document: mockTextDocument }; const mockTab = { input: { uri: { fsPath: '/mock/workspace/path/file.ts' } } }; const mockTabGroup = { tabs: [mockTab] }; return { window: { createTextEditorDecorationType: jest.fn().mockReturnValue({ dispose: jest.fn() }), visibleTextEditors: [mockTextEditor], tabGroups: { all: [mockTabGroup] } }, workspace: { workspaceFolders: [{ uri: { fsPath: '/mock/workspace/path' }, name: 'mock-workspace', index: 0 }], createFileSystemWatcher: jest.fn(() => ({ onDidCreate: jest.fn(() => mockDisposable), onDidDelete: jest.fn(() => mockDisposable), onDidChange: jest.fn(() => mockDisposable), dispose: jest.fn() })), fs: { stat: jest.fn().mockResolvedValue({ type: 1 }) // FileType.File = 1 }, onDidSaveTextDocument: jest.fn(() => mockDisposable) }, env: { uriScheme: 'vscode', language: 'en' }, EventEmitter: jest.fn().mockImplementation(() => mockEventEmitter), Disposable: { from: jest.fn() }, TabInputText: jest.fn() }; }); // Mock p-wait-for to resolve immediately jest.mock('p-wait-for', () => ({ __esModule: true, default: jest.fn().mockImplementation(async () => Promise.resolve()) })); jest.mock('delay', () => ({ __esModule: true, default: jest.fn().mockImplementation(async () => Promise.resolve()) })); jest.mock('serialize-error', () => ({ __esModule: true, serializeError: jest.fn().mockImplementation((error) => ({ name: error.name, message: error.message, stack: error.stack })) })); jest.mock('strip-ansi', () => ({ __esModule: true, default: jest.fn().mockImplementation((str) => str.replace(/\u001B\[\d+m/g, '')) })); jest.mock('globby', () => ({ __esModule: true, globby: jest.fn().mockImplementation(async () => []) })); jest.mock('os-name', () => ({ __esModule: true, default: jest.fn().mockReturnValue('Mock OS Name') })); jest.mock('default-shell', () => ({ __esModule: true, default: '/bin/bash' // Mock default shell path })); describe('Cline', () => { let mockProvider: jest.Mocked; let mockApiConfig: ApiConfiguration; let mockOutputChannel: any; let mockExtensionContext: vscode.ExtensionContext; beforeEach(() => { // Setup mock extension context mockExtensionContext = { globalState: { get: jest.fn().mockImplementation((key) => { if (key === 'taskHistory') { return [{ id: '123', ts: Date.now(), task: 'historical task', tokensIn: 100, tokensOut: 200, cacheWrites: 0, cacheReads: 0, totalCost: 0.001 }]; } return undefined; }), update: jest.fn().mockImplementation((key, value) => Promise.resolve()), keys: jest.fn().mockReturnValue([]) }, workspaceState: { get: jest.fn().mockImplementation((key) => undefined), update: jest.fn().mockImplementation((key, value) => Promise.resolve()), keys: jest.fn().mockReturnValue([]) }, secrets: { get: jest.fn().mockImplementation((key) => Promise.resolve(undefined)), store: jest.fn().mockImplementation((key, value) => Promise.resolve()), delete: jest.fn().mockImplementation((key) => Promise.resolve()) }, extensionUri: { fsPath: '/mock/extension/path' }, globalStorageUri: { fsPath: '/mock/storage/path' }, extension: { packageJSON: { version: '1.0.0' } } } as unknown as vscode.ExtensionContext; // Setup mock output channel mockOutputChannel = { appendLine: jest.fn(), append: jest.fn(), clear: jest.fn(), show: jest.fn(), hide: jest.fn(), dispose: jest.fn() }; // Setup mock provider with output channel mockProvider = new ClineProvider(mockExtensionContext, mockOutputChannel) as jest.Mocked; // Setup mock API configuration mockApiConfig = { apiProvider: 'anthropic', apiModelId: 'claude-3-5-sonnet-20241022' }; // Mock provider methods mockProvider.postMessageToWebview = jest.fn().mockResolvedValue(undefined); mockProvider.postStateToWebview = jest.fn().mockResolvedValue(undefined); mockProvider.getTaskWithId = jest.fn().mockImplementation(async (id) => ({ historyItem: { id, ts: Date.now(), task: 'historical task', tokensIn: 100, tokensOut: 200, cacheWrites: 0, cacheReads: 0, totalCost: 0.001 }, taskDirPath: '/mock/storage/path/tasks/123', apiConversationHistoryFilePath: '/mock/storage/path/tasks/123/api_conversation_history.json', uiMessagesFilePath: '/mock/storage/path/tasks/123/ui_messages.json', apiConversationHistory: [] })); }); describe('constructor', () => { it('should respect provided settings', () => { const cline = new Cline( mockProvider, mockApiConfig, 'custom instructions', false, 0.95, // 95% threshold 'test task' ); expect(cline.customInstructions).toBe('custom instructions'); expect(cline.diffEnabled).toBe(false); }); it('should use default fuzzy match threshold when not provided', () => { const cline = new Cline( mockProvider, mockApiConfig, 'custom instructions', true, undefined, 'test task' ); expect(cline.diffEnabled).toBe(true); // The diff strategy should be created with default threshold (1.0) expect(cline.diffStrategy).toBeDefined(); }); it('should use provided fuzzy match threshold', () => { const getDiffStrategySpy = jest.spyOn(require('../diff/DiffStrategy'), 'getDiffStrategy'); const cline = new Cline( mockProvider, mockApiConfig, 'custom instructions', true, 0.9, // 90% threshold 'test task' ); expect(cline.diffEnabled).toBe(true); expect(cline.diffStrategy).toBeDefined(); expect(getDiffStrategySpy).toHaveBeenCalledWith('claude-3-5-sonnet-20241022', 0.9); getDiffStrategySpy.mockRestore(); }); it('should pass default threshold to diff strategy when not provided', () => { const getDiffStrategySpy = jest.spyOn(require('../diff/DiffStrategy'), 'getDiffStrategy'); const cline = new Cline( mockProvider, mockApiConfig, 'custom instructions', true, undefined, 'test task' ); expect(cline.diffEnabled).toBe(true); expect(cline.diffStrategy).toBeDefined(); expect(getDiffStrategySpy).toHaveBeenCalledWith('claude-3-5-sonnet-20241022', 1.0); getDiffStrategySpy.mockRestore(); }); it('should require either task or historyItem', () => { expect(() => { new Cline( mockProvider, mockApiConfig, undefined, // customInstructions false, // diffEnabled undefined, // fuzzyMatchThreshold undefined // task ); }).toThrow('Either historyItem or task/images must be provided'); }); }); describe('getEnvironmentDetails', () => { let originalDate: DateConstructor; let mockDate: Date; beforeEach(() => { originalDate = global.Date; const fixedTime = new Date('2024-01-01T12:00:00Z'); mockDate = new Date(fixedTime); mockDate.getTimezoneOffset = jest.fn().mockReturnValue(420); // UTC-7 class MockDate extends Date { constructor() { super(); return mockDate; } static override now() { return mockDate.getTime(); } } global.Date = MockDate as DateConstructor; // Create a proper mock of Intl.DateTimeFormat const mockDateTimeFormat = { resolvedOptions: () => ({ timeZone: 'America/Los_Angeles' }), format: () => '1/1/2024, 5:00:00 AM' }; const MockDateTimeFormat = function(this: any) { return mockDateTimeFormat; } as any; MockDateTimeFormat.prototype = mockDateTimeFormat; MockDateTimeFormat.supportedLocalesOf = jest.fn().mockReturnValue(['en-US']); global.Intl.DateTimeFormat = MockDateTimeFormat; }); afterEach(() => { global.Date = originalDate; }); it('should include timezone information in environment details', async () => { const cline = new Cline( mockProvider, mockApiConfig, undefined, false, undefined, 'test task' ); const details = await cline['getEnvironmentDetails'](false); // Verify timezone information is present and formatted correctly expect(details).toContain('America/Los_Angeles'); expect(details).toMatch(/UTC-7:00/); // Fixed offset for America/Los_Angeles expect(details).toContain('# Current Time'); expect(details).toMatch(/1\/1\/2024.*5:00:00 AM.*\(America\/Los_Angeles, UTC-7:00\)/); // Full time string format }); }); });