From 66c54855342416d09af7d6c289549adb1bd5bda9 Mon Sep 17 00:00:00 2001 From: ColemanRoo Date: Fri, 3 Jan 2025 14:26:26 -0600 Subject: [PATCH 1/2] Set up vscode integration test --- .gitignore | 3 + .vscode-test.mjs | 15 +- package.json | 4 + src/exports/cline.d.ts | 5 + src/exports/index.ts | 2 + src/test/extension.test.ts | 354 +++++++++++++++++++++++++++++++++++-- src/test/tsconfig.json | 19 ++ 7 files changed, 387 insertions(+), 15 deletions(-) create mode 100644 src/test/tsconfig.json diff --git a/.gitignore b/.gitignore index ad38c8b..4914bf2 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,6 @@ roo-cline-*.vsix # Local prompts and rules /local-prompts + +# Test environment +.test_env \ No newline at end of file diff --git a/.vscode-test.mjs b/.vscode-test.mjs index 605b44f..1ce01d1 100644 --- a/.vscode-test.mjs +++ b/.vscode-test.mjs @@ -1,5 +1,14 @@ -import { defineConfig } from "@vscode/test-cli" +import { defineConfig } from '@vscode/test-cli'; 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' + ] +}); diff --git a/package.json b/package.json index 7d82a24..0f9666b 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,9 @@ "url": "https://github.com/RooVetGit/Roo-Cline" }, "homepage": "https://github.com/RooVetGit/Roo-Cline", + "enabledApiProposals": [ + "extensionRuntime" + ], "categories": [ "AI", "Chat", @@ -159,6 +162,7 @@ "start:webview": "cd webview-ui && npm run start", "test": "jest && npm run test:webview", "test:webview": "cd webview-ui && npm run test", + "test:extension": "vscode-test", "prepare": "husky", "publish:marketplace": "vsce publish", "publish": "npm run build && changeset publish && npm install --package-lock-only", diff --git a/src/exports/cline.d.ts b/src/exports/cline.d.ts index 1ae285f..fcf93fc 100644 --- a/src/exports/cline.d.ts +++ b/src/exports/cline.d.ts @@ -34,4 +34,9 @@ export interface ClineAPI { * Simulates pressing the secondary button in the chat interface. */ pressSecondaryButton(): Promise + + /** + * The sidebar provider instance. + */ + sidebarProvider: ClineSidebarProvider } diff --git a/src/exports/index.ts b/src/exports/index.ts index 04d26d8..a0680b0 100644 --- a/src/exports/index.ts +++ b/src/exports/index.ts @@ -56,6 +56,8 @@ export function createClineAPI(outputChannel: vscode.OutputChannel, sidebarProvi invoke: "secondaryButtonClick", }) }, + + sidebarProvider: sidebarProvider, } return api diff --git a/src/test/extension.test.ts b/src/test/extension.test.ts index 35bee7b..d57654b 100644 --- a/src/test/extension.test.ts +++ b/src/test/extension.test.ts @@ -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'); -// You can import and use all API from the 'vscode' module -// as well as import your extension to test it -import * as vscode from "vscode" -// import * as myExtension from '../../extension'; +suite('Roo Cline Extension Test Suite', () => { + vscode.window.showInformationMessage('Starting Roo Cline extension tests.'); -suite("Extension Test Suite", () => { - vscode.window.showInformationMessage("Start all tests.") + test('Extension should be present', () => { + const extension = vscode.extensions.getExtension('RooVeterinaryInc.roo-cline'); + assert.notStrictEqual(extension, undefined); + }); - test("Sample test", () => { - assert.strictEqual(-1, [1, 2, 3].indexOf(5)) - assert.strictEqual(-1, [1, 2, 3].indexOf(0)) - }) -}) + 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 testEnvPath = path.join(__dirname, '.test_env'); + const envContent = fs.readFileSync(testEnvPath, 'utf8'); + const match = envContent.match(/OPEN_ROUTER_API_KEY=(.+)/); + if (!match) { + done(new Error('OpenRouter API key should be present in .test_env')); + return; + } + const apiKey = match[1]; + 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 testEnvPath = path.join(__dirname, '.test_env'); + const envContent = fs.readFileSync(testEnvPath, 'utf8'); + const match = envContent.match(/OPEN_ROUTER_API_KEY=(.+)/); + if (!match) { + assert.fail('OpenRouter API key should be present in .test_env'); + return; + } + await provider.storeSecret('openRouterApiKey', match[1]); + + // 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(); + } + }); +}); diff --git a/src/test/tsconfig.json b/src/test/tsconfig.json new file mode 100644 index 0000000..0560c90 --- /dev/null +++ b/src/test/tsconfig.json @@ -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"] +} \ No newline at end of file From 8e54360a8606b8aa217918fd84674993122fffef Mon Sep 17 00:00:00 2001 From: ColemanRoo Date: Fri, 3 Jan 2025 21:35:25 -0600 Subject: [PATCH 2/2] Switch to using dotenv to get test environment variables --- package-lock.json | 14 ++++++++++++++ package.json | 3 ++- src/test/extension.test.ts | 24 ++++++++++++------------ 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6143970..fb2065a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -56,6 +56,7 @@ "@typescript-eslint/parser": "^7.11.0", "@vscode/test-cli": "^0.0.9", "@vscode/test-electron": "^2.4.0", + "dotenv": "^16.4.7", "esbuild": "^0.24.0", "eslint": "^8.57.0", "husky": "^9.1.7", @@ -8000,6 +8001,19 @@ "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": { "version": "0.1.12", "resolved": "https://registry.npmjs.org/duck/-/duck-0.1.12.tgz", diff --git a/package.json b/package.json index 0f9666b..0ac1434 100644 --- a/package.json +++ b/package.json @@ -185,6 +185,7 @@ "@typescript-eslint/parser": "^7.11.0", "@vscode/test-cli": "^0.0.9", "@vscode/test-electron": "^2.4.0", + "dotenv": "^16.4.7", "esbuild": "^0.24.0", "eslint": "^8.57.0", "husky": "^9.1.7", @@ -196,9 +197,9 @@ }, "dependencies": { "@anthropic-ai/bedrock-sdk": "^0.10.2", - "@aws-sdk/client-bedrock-runtime": "^3.706.0", "@anthropic-ai/sdk": "^0.26.0", "@anthropic-ai/vertex-sdk": "^0.4.1", + "@aws-sdk/client-bedrock-runtime": "^3.706.0", "@google/generative-ai": "^0.18.0", "@modelcontextprotocol/sdk": "^1.0.1", "@types/clone-deep": "^4.0.4", diff --git a/src/test/extension.test.ts b/src/test/extension.test.ts index d57654b..7377f3f 100644 --- a/src/test/extension.test.ts +++ b/src/test/extension.test.ts @@ -2,6 +2,11 @@ const assert = require('assert'); const vscode = require('vscode'); const path = require('path'); const fs = require('fs'); +const dotenv = require('dotenv'); + +// Load test environment variables +const testEnvPath = path.join(__dirname, '.test_env'); +dotenv.config({ path: testEnvPath }); suite('Roo Cline Extension Test Suite', () => { vscode.window.showInformationMessage('Starting Roo Cline extension tests.'); @@ -34,14 +39,11 @@ suite('Roo Cline Extension Test Suite', () => { } // Verify API key is set and valid - const testEnvPath = path.join(__dirname, '.test_env'); - const envContent = fs.readFileSync(testEnvPath, 'utf8'); - const match = envContent.match(/OPEN_ROUTER_API_KEY=(.+)/); - if (!match) { - done(new Error('OpenRouter API key should be present in .test_env')); + const apiKey = process.env.OPEN_ROUTER_API_KEY; + if (!apiKey) { + done(new Error('OPEN_ROUTER_API_KEY environment variable is not set')); return; } - const apiKey = match[1]; if (!apiKey.startsWith('sk-or-v1-')) { done(new Error('OpenRouter API key should have correct format')); return; @@ -180,14 +182,12 @@ suite('Roo Cline Extension Test Suite', () => { // Set up API configuration await provider.updateGlobalState('apiProvider', 'openrouter'); await provider.updateGlobalState('openRouterModelId', 'anthropic/claude-3.5-sonnet'); - const testEnvPath = path.join(__dirname, '.test_env'); - const envContent = fs.readFileSync(testEnvPath, 'utf8'); - const match = envContent.match(/OPEN_ROUTER_API_KEY=(.+)/); - if (!match) { - assert.fail('OpenRouter API key should be present in .test_env'); + 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', match[1]); + await provider.storeSecret('openRouterApiKey', apiKey); // Create webview panel with development options const extensionUri = extension.extensionUri;