mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 04:11:10 -05:00
424 lines
14 KiB
TypeScript
424 lines
14 KiB
TypeScript
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<ClineProvider>;
|
|
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<ClineProvider>;
|
|
|
|
// 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
|
|
});
|
|
});
|
|
});
|