Merge pull request #270 from RooVetGit/feat/full_Integration_Tests

Set up vscode integration test
This commit is contained in:
Mike C
2025-01-06 22:00:08 -06:00
committed by GitHub
8 changed files with 403 additions and 16 deletions

3
.gitignore vendored
View File

@@ -11,3 +11,6 @@ roo-cline-*.vsix
# Local prompts and rules # Local prompts and rules
/local-prompts /local-prompts
# Test environment
.test_env

View File

@@ -1,5 +1,14 @@
import { defineConfig } from "@vscode/test-cli" import { defineConfig } from '@vscode/test-cli';
export default defineConfig({ export default defineConfig({
files: "out/test/**/*.test.js", files: 'src/test/extension.test.ts',
}) workspaceFolder: '.',
mocha: {
timeout: 60000,
ui: 'tdd'
},
launchArgs: [
'--enable-proposed-api=RooVeterinaryInc.roo-cline',
'--disable-extensions'
]
});

14
package-lock.json generated
View File

@@ -56,6 +56,7 @@
"@typescript-eslint/parser": "^7.11.0", "@typescript-eslint/parser": "^7.11.0",
"@vscode/test-cli": "^0.0.9", "@vscode/test-cli": "^0.0.9",
"@vscode/test-electron": "^2.4.0", "@vscode/test-electron": "^2.4.0",
"dotenv": "^16.4.7",
"esbuild": "^0.24.0", "esbuild": "^0.24.0",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"husky": "^9.1.7", "husky": "^9.1.7",
@@ -8000,6 +8001,19 @@
"url": "https://github.com/fb55/domutils?sponsor=1" "url": "https://github.com/fb55/domutils?sponsor=1"
} }
}, },
"node_modules/dotenv": {
"version": "16.4.7",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
"integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/duck": { "node_modules/duck": {
"version": "0.1.12", "version": "0.1.12",
"resolved": "https://registry.npmjs.org/duck/-/duck-0.1.12.tgz", "resolved": "https://registry.npmjs.org/duck/-/duck-0.1.12.tgz",

View File

@@ -20,6 +20,9 @@
"url": "https://github.com/RooVetGit/Roo-Cline" "url": "https://github.com/RooVetGit/Roo-Cline"
}, },
"homepage": "https://github.com/RooVetGit/Roo-Cline", "homepage": "https://github.com/RooVetGit/Roo-Cline",
"enabledApiProposals": [
"extensionRuntime"
],
"categories": [ "categories": [
"AI", "AI",
"Chat", "Chat",
@@ -159,6 +162,7 @@
"start:webview": "cd webview-ui && npm run start", "start:webview": "cd webview-ui && npm run start",
"test": "jest && npm run test:webview", "test": "jest && npm run test:webview",
"test:webview": "cd webview-ui && npm run test", "test:webview": "cd webview-ui && npm run test",
"test:extension": "vscode-test",
"prepare": "husky", "prepare": "husky",
"publish:marketplace": "vsce publish", "publish:marketplace": "vsce publish",
"publish": "npm run build && changeset publish && npm install --package-lock-only", "publish": "npm run build && changeset publish && npm install --package-lock-only",
@@ -181,6 +185,7 @@
"@typescript-eslint/parser": "^7.11.0", "@typescript-eslint/parser": "^7.11.0",
"@vscode/test-cli": "^0.0.9", "@vscode/test-cli": "^0.0.9",
"@vscode/test-electron": "^2.4.0", "@vscode/test-electron": "^2.4.0",
"dotenv": "^16.4.7",
"esbuild": "^0.24.0", "esbuild": "^0.24.0",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"husky": "^9.1.7", "husky": "^9.1.7",
@@ -192,9 +197,9 @@
}, },
"dependencies": { "dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.10.2", "@anthropic-ai/bedrock-sdk": "^0.10.2",
"@aws-sdk/client-bedrock-runtime": "^3.706.0",
"@anthropic-ai/sdk": "^0.26.0", "@anthropic-ai/sdk": "^0.26.0",
"@anthropic-ai/vertex-sdk": "^0.4.1", "@anthropic-ai/vertex-sdk": "^0.4.1",
"@aws-sdk/client-bedrock-runtime": "^3.706.0",
"@google/generative-ai": "^0.18.0", "@google/generative-ai": "^0.18.0",
"@modelcontextprotocol/sdk": "^1.0.1", "@modelcontextprotocol/sdk": "^1.0.1",
"@types/clone-deep": "^4.0.4", "@types/clone-deep": "^4.0.4",

View File

@@ -34,4 +34,9 @@ export interface ClineAPI {
* Simulates pressing the secondary button in the chat interface. * Simulates pressing the secondary button in the chat interface.
*/ */
pressSecondaryButton(): Promise<void> pressSecondaryButton(): Promise<void>
/**
* The sidebar provider instance.
*/
sidebarProvider: ClineSidebarProvider
} }

View File

@@ -56,6 +56,8 @@ export function createClineAPI(outputChannel: vscode.OutputChannel, sidebarProvi
invoke: "secondaryButtonClick", invoke: "secondaryButtonClick",
}) })
}, },
sidebarProvider: sidebarProvider,
} }
return api return api

View File

@@ -1,15 +1,345 @@
import * as assert from "assert" const assert = require('assert');
const vscode = require('vscode');
const path = require('path');
const fs = require('fs');
const dotenv = require('dotenv');
// You can import and use all API from the 'vscode' module // Load test environment variables
// as well as import your extension to test it const testEnvPath = path.join(__dirname, '.test_env');
import * as vscode from "vscode" dotenv.config({ path: testEnvPath });
// import * as myExtension from '../../extension';
suite("Extension Test Suite", () => { suite('Roo Cline Extension Test Suite', () => {
vscode.window.showInformationMessage("Start all tests.") vscode.window.showInformationMessage('Starting Roo Cline extension tests.');
test("Sample test", () => { test('Extension should be present', () => {
assert.strictEqual(-1, [1, 2, 3].indexOf(5)) const extension = vscode.extensions.getExtension('RooVeterinaryInc.roo-cline');
assert.strictEqual(-1, [1, 2, 3].indexOf(0)) assert.notStrictEqual(extension, undefined);
}) });
})
test('Extension should activate', async () => {
const extension = vscode.extensions.getExtension('RooVeterinaryInc.roo-cline');
if (!extension) {
assert.fail('Extension not found');
}
await extension.activate();
assert.strictEqual(extension.isActive, true);
});
test('OpenRouter API key and models should be configured correctly', function(done) {
// @ts-ignore
this.timeout(60000); // Increase timeout to 60s for network requests
(async () => {
try {
// Get extension instance
const extension = vscode.extensions.getExtension('RooVeterinaryInc.roo-cline');
if (!extension) {
done(new Error('Extension not found'));
return;
}
// Verify API key is set and valid
const apiKey = process.env.OPEN_ROUTER_API_KEY;
if (!apiKey) {
done(new Error('OPEN_ROUTER_API_KEY environment variable is not set'));
return;
}
if (!apiKey.startsWith('sk-or-v1-')) {
done(new Error('OpenRouter API key should have correct format'));
return;
}
// Activate extension and get provider
const api = await extension.activate();
if (!api) {
done(new Error('Extension API not found'));
return;
}
// Get the provider from the extension's exports
const provider = api.sidebarProvider;
if (!provider) {
done(new Error('Provider not found'));
return;
}
// Set up the API configuration
await provider.updateGlobalState('apiProvider', 'openrouter');
await provider.storeSecret('openRouterApiKey', apiKey);
// Set up timeout to fail test if models don't load
const timeout = setTimeout(() => {
done(new Error('Timeout waiting for models to load'));
}, 30000);
// Wait for models to be loaded
const checkModels = setInterval(async () => {
try {
const models = await provider.readOpenRouterModels();
if (!models) {
return;
}
clearInterval(checkModels);
clearTimeout(timeout);
// Verify expected Claude models are available
const expectedModels = [
'anthropic/claude-3.5-sonnet:beta',
'anthropic/claude-3-sonnet:beta',
'anthropic/claude-3.5-sonnet',
'anthropic/claude-3.5-sonnet-20240620',
'anthropic/claude-3.5-sonnet-20240620:beta',
'anthropic/claude-3.5-haiku:beta'
];
for (const modelId of expectedModels) {
assert.strictEqual(
modelId in models,
true,
`Model ${modelId} should be available`
);
}
done();
} catch (error) {
clearInterval(checkModels);
clearTimeout(timeout);
done(error);
}
}, 1000);
// Trigger model loading
await provider.refreshOpenRouterModels();
} catch (error) {
done(error);
}
})();
});
test('Commands should be registered', async () => {
const commands = await vscode.commands.getCommands(true);
// Test core commands are registered
const expectedCommands = [
'roo-cline.plusButtonClicked',
'roo-cline.mcpButtonClicked',
'roo-cline.historyButtonClicked',
'roo-cline.popoutButtonClicked',
'roo-cline.settingsButtonClicked',
'roo-cline.openInNewTab'
];
for (const cmd of expectedCommands) {
assert.strictEqual(
commands.includes(cmd),
true,
`Command ${cmd} should be registered`
);
}
});
test('Views should be registered', () => {
const view = vscode.window.createWebviewPanel(
'roo-cline.SidebarProvider',
'Roo Cline',
vscode.ViewColumn.One,
{}
);
assert.notStrictEqual(view, undefined);
view.dispose();
});
test('Should handle prompt and response correctly', async function() {
// @ts-ignore
this.timeout(60000); // Increase timeout for API request
const timeout = 30000;
const interval = 1000;
// Get extension instance
const extension = vscode.extensions.getExtension('RooVeterinaryInc.roo-cline');
if (!extension) {
assert.fail('Extension not found');
return;
}
// Activate extension and get API
const api = await extension.activate();
if (!api) {
assert.fail('Extension API not found');
return;
}
// Get provider
const provider = api.sidebarProvider;
if (!provider) {
assert.fail('Provider not found');
return;
}
// Set up API configuration
await provider.updateGlobalState('apiProvider', 'openrouter');
await provider.updateGlobalState('openRouterModelId', 'anthropic/claude-3.5-sonnet');
const apiKey = process.env.OPEN_ROUTER_API_KEY;
if (!apiKey) {
assert.fail('OPEN_ROUTER_API_KEY environment variable is not set');
return;
}
await provider.storeSecret('openRouterApiKey', apiKey);
// Create webview panel with development options
const extensionUri = extension.extensionUri;
const panel = vscode.window.createWebviewPanel(
'roo-cline.SidebarProvider',
'Roo Cline',
vscode.ViewColumn.One,
{
enableScripts: true,
enableCommandUris: true,
retainContextWhenHidden: true,
localResourceRoots: [extensionUri]
}
);
try {
// Initialize webview with development context
panel.webview.options = {
enableScripts: true,
enableCommandUris: true,
localResourceRoots: [extensionUri]
};
// Initialize provider with panel
provider.resolveWebviewView(panel);
// Set up message tracking
let webviewReady = false;
let messagesReceived = false;
const originalPostMessage = provider.postMessageToWebview.bind(provider);
// @ts-ignore
provider.postMessageToWebview = async (message) => {
if (message.type === 'state') {
webviewReady = true;
console.log('Webview state received:', message);
if (message.state?.clineMessages?.length > 0) {
messagesReceived = true;
console.log('Messages in state:', message.state.clineMessages);
}
}
await originalPostMessage(message);
};
// Wait for webview to launch and receive initial state
let startTime = Date.now();
while (Date.now() - startTime < timeout) {
if (webviewReady) {
// Wait an additional second for webview to fully initialize
await new Promise(resolve => setTimeout(resolve, 1000));
break;
}
await new Promise(resolve => setTimeout(resolve, interval));
}
if (!webviewReady) {
throw new Error('Timeout waiting for webview to be ready');
}
// Send webviewDidLaunch to initialize chat
await provider.postMessageToWebview({ type: 'webviewDidLaunch' });
console.log('Sent webviewDidLaunch');
// Wait for webview to fully initialize
await new Promise(resolve => setTimeout(resolve, 2000));
// Restore original postMessage
provider.postMessageToWebview = originalPostMessage;
// Wait for OpenRouter models to be fully loaded
startTime = Date.now();
while (Date.now() - startTime < timeout) {
const models = await provider.readOpenRouterModels();
if (models && Object.keys(models).length > 0) {
console.log('OpenRouter models loaded');
break;
}
await new Promise(resolve => setTimeout(resolve, interval));
}
// Send prompt
const prompt = "Hello world, what is your name?";
console.log('Sending prompt:', prompt);
// Start task
try {
await api.startNewTask(prompt);
console.log('Task started');
} catch (error) {
console.error('Error starting task:', error);
throw error;
}
// Wait for task to appear in history with tokens
startTime = Date.now();
while (Date.now() - startTime < timeout) {
const state = await provider.getState();
const task = state.taskHistory?.[0];
if (task && task.tokensOut > 0) {
console.log('Task completed with tokens:', task);
break;
}
await new Promise(resolve => setTimeout(resolve, interval));
}
// Wait for messages to be processed
startTime = Date.now();
let responseReceived = false;
while (Date.now() - startTime < timeout) {
// Check provider.clineMessages
const messages = provider.clineMessages;
if (messages && messages.length > 0) {
console.log('Provider messages:', JSON.stringify(messages, null, 2));
// @ts-ignore
const hasResponse = messages.some(m =>
m.type === 'say' &&
m.text &&
m.text.toLowerCase().includes('cline')
);
if (hasResponse) {
console.log('Found response containing "Cline" in provider messages');
responseReceived = true;
break;
}
}
// Check provider.cline.clineMessages
const clineMessages = provider.cline?.clineMessages;
if (clineMessages && clineMessages.length > 0) {
console.log('Cline messages:', JSON.stringify(clineMessages, null, 2));
// @ts-ignore
const hasResponse = clineMessages.some(m =>
m.type === 'say' &&
m.text &&
m.text.toLowerCase().includes('cline')
);
if (hasResponse) {
console.log('Found response containing "Cline" in cline messages');
responseReceived = true;
break;
}
}
await new Promise(resolve => setTimeout(resolve, interval));
}
if (!responseReceived) {
console.log('Final provider state:', await provider.getState());
console.log('Final cline messages:', provider.cline?.clineMessages);
throw new Error('Did not receive expected response containing "Cline"');
}
} finally {
panel.dispose();
}
});
});

19
src/test/tsconfig.json Normal file
View File

@@ -0,0 +1,19 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "ES2020",
"lib": ["ES2020"],
"sourceMap": true,
"rootDir": "../..",
"strict": false,
"noImplicitAny": false,
"noImplicitThis": false,
"alwaysStrict": false,
"skipLibCheck": true,
"baseUrl": "../..",
"paths": {
"*": ["*", "src/*"]
}
},
"exclude": ["node_modules", ".vscode-test"]
}