diff --git a/.env.integration.example b/.env.integration.example new file mode 100644 index 0000000..40c9da1 --- /dev/null +++ b/.env.integration.example @@ -0,0 +1 @@ +OPENROUTER_API_KEY=sk-or-v1-... diff --git a/.github/workflows/code-qa.yml b/.github/workflows/code-qa.yml index 19682b6..f17a648 100644 --- a/.github/workflows/code-qa.yml +++ b/.github/workflows/code-qa.yml @@ -1,6 +1,7 @@ name: Code QA Roo Code on: + workflow_dispatch: push: branches: [main] pull_request: @@ -13,33 +14,55 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 - - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '18' cache: 'npm' - - name: Install dependencies run: npm run install:all - - - name: Compile TypeScript + - name: Compile run: npm run compile + - name: Check types + run: npm run check-types + - name: Lint + run: npm run lint unit-test: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '18' cache: 'npm' - - name: Install dependencies run: npm run install:all - - name: Run unit tests - run: npm test \ No newline at end of file + run: npm test + + integration-test: + strategy: + matrix: + os: [ubuntu-latest] # macos-latest, windows-latest + runs-on: ${{ matrix.os }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + cache: 'npm' + - name: Create env.integration file + run: echo "OPENROUTER_API_KEY=${{ secrets.OPENROUTER_API_KEY }}" > .env.integration + - name: Check env.integration file + run: cat .env.integration + - name: Install dependencies + run: npm run install:all + - run: xvfb-run -a npm run test:integration + if: runner.os == 'Linux' + - run: npm run test:integration + if: runner.os != 'Linux' diff --git a/.gitignore b/.gitignore index 95f7106..bf3b19e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ -out dist +out +out-integration node_modules coverage/ @@ -18,3 +19,6 @@ roo-cline-*.vsix # Docs docs/_site/ + +# Dotenv +.env.integration diff --git a/.husky/pre-commit b/.husky/pre-commit index 2f3977a..9d86983 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -6,4 +6,7 @@ if [ "$branch" = "main" ]; then fi npx lint-staged + npm run compile +npm run lint +npm run check-types diff --git a/.vscode-test.mjs b/.vscode-test.mjs index 1ce01d1..dd77607 100644 --- a/.vscode-test.mjs +++ b/.vscode-test.mjs @@ -1,11 +1,16 @@ +/** + * See: https://code.visualstudio.com/api/working-with-extensions/testing-extension + */ + import { defineConfig } from '@vscode/test-cli'; export default defineConfig({ - files: 'src/test/extension.test.ts', + label: 'integrationTest', + files: 'out-integration/test/**/*.test.js', workspaceFolder: '.', mocha: { + ui: 'tdd', timeout: 60000, - ui: 'tdd' }, launchArgs: [ '--enable-proposed-api=RooVeterinaryInc.roo-cline', diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..5d5fa53 --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1737569578, + "narHash": "sha256-6qY0pk2QmUtBT9Mywdvif0i/CLVgpCjMUn6g9vB+f3M=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "47addd76727f42d351590c905d9d1905ca895b82", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-24.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..74c3c62 --- /dev/null +++ b/flake.nix @@ -0,0 +1,33 @@ +{ + description = "Roo Code development environment"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-24.11"; + }; + + outputs = { self, nixpkgs, ... }: let + systems = [ "aarch64-darwin" "x86_64-linux" ]; + + forAllSystems = nixpkgs.lib.genAttrs systems; + + mkDevShell = system: let + pkgs = import nixpkgs { inherit system; }; + in pkgs.mkShell { + name = "roo-code"; + + packages = with pkgs; [ + zsh + nodejs_18 + corepack_18 + ]; + + shellHook = '' + exec zsh + ''; + }; + in { + devShells = forAllSystems (system: { + default = mkDevShell system; + }); + }; +} diff --git a/package-lock.json b/package-lock.json index 2b49174..78dcbc4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55,6 +55,7 @@ "devDependencies": { "@changesets/cli": "^2.27.10", "@changesets/types": "^6.0.0", + "@dotenvx/dotenvx": "^1.34.0", "@types/diff": "^5.2.1", "@types/diff-match-patch": "^1.0.36", "@types/jest": "^29.5.14", @@ -65,7 +66,6 @@ "@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", @@ -3030,6 +3030,110 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/@dotenvx/dotenvx": { + "version": "1.34.0", + "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.34.0.tgz", + "integrity": "sha512-+Dp/xaI3IZ4eKv+b2vg4V89VnqLKbmJ7UZ7unnZxMu9SNLOSc2jYaXey1YHCJM+67T0pOr2Gbej3TewnuoqTWQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "commander": "^11.1.0", + "dotenv": "^16.4.5", + "eciesjs": "^0.4.10", + "execa": "^5.1.1", + "fdir": "^6.2.0", + "ignore": "^5.3.0", + "object-treeify": "1.1.33", + "picomatch": "^4.0.2", + "which": "^4.0.0" + }, + "bin": { + "dotenvx": "src/cli/dotenvx.js", + "git-dotenvx": "src/cli/dotenvx.js" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/fdir": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz", + "integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@dotenvx/dotenvx/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/@ecies/ciphers": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@ecies/ciphers/-/ciphers-0.2.2.tgz", + "integrity": "sha512-ylfGR7PyTd+Rm2PqQowG08BCKA22QuX8NzrL+LxAAvazN10DMwdJ2fWwAzRj05FI/M8vNFGm3cv9Wq/GFWCBLg==", + "dev": true, + "license": "MIT", + "engines": { + "bun": ">=1", + "deno": ">=2", + "node": ">=16" + }, + "peerDependencies": { + "@noble/ciphers": "^1.0.0" + } + }, "node_modules/@esbuild/darwin-arm64": { "version": "0.24.0", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.0.tgz", @@ -3964,6 +4068,48 @@ "zod": "^3.23.8" } }, + "node_modules/@noble/ciphers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.2.1.tgz", + "integrity": "sha512-rONPWMC7PeExE077uLE4oqWrZ1IvAfz3oH9LibVAcVCopJiA9R62uavnbEzdkVmJYI6M6Zgkbeb07+tWjlq2XA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.1.tgz", + "integrity": "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.7.1" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.7.1.tgz", + "integrity": "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -7772,6 +7918,24 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/eciesjs": { + "version": "0.4.13", + "resolved": "https://registry.npmjs.org/eciesjs/-/eciesjs-0.4.13.tgz", + "integrity": "sha512-zBdtR4K+wbj10bWPpIOF9DW+eFYQu8miU5ypunh0t4Bvt83ZPlEWgT5Dq/0G6uwEXumZKjfb5BZxYUZQ2Hzn/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ecies/ciphers": "^0.2.2", + "@noble/ciphers": "^1.0.0", + "@noble/curves": "^1.6.0", + "@noble/hashes": "^1.5.0" + }, + "engines": { + "bun": ">=1", + "deno": ">=2", + "node": ">=16" + } + }, "node_modules/eight-colors": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/eight-colors/-/eight-colors-1.3.1.tgz", @@ -12247,6 +12411,16 @@ "node": ">= 0.4" } }, + "node_modules/object-treeify": { + "version": "1.1.33", + "resolved": "https://registry.npmjs.org/object-treeify/-/object-treeify-1.1.33.tgz", + "integrity": "sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, "node_modules/object.assign": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", diff --git a/package.json b/package.json index 568d1b1..a56619f 100644 --- a/package.json +++ b/package.json @@ -221,16 +221,16 @@ "build:webview": "cd webview-ui && npm run build", "changeset": "changeset", "check-types": "tsc --noEmit", - "compile": "npm run check-types && npm run lint && node esbuild.js", - "compile-tests": "tsc -p . --outDir out", + "compile": "tsc -p . --outDir out && node esbuild.js", + "compile:integration": "tsc -p tsconfig.integration.json", "install:all": "npm install && cd webview-ui && npm install", "lint": "eslint src --ext ts && npm run lint --prefix webview-ui", "package": "npm run build:webview && npm run check-types && npm run lint && node esbuild.js --production", - "pretest": "npm run compile-tests && npm run compile && npm run lint", + "pretest": "npm run compile && npm run compile:integration", "dev": "cd webview-ui && npm run dev", "test": "jest && npm run test:webview", "test:webview": "cd webview-ui && npm run test", - "test:extension": "vscode-test", + "test:integration": "npm run build && npm run compile:integration && npx dotenvx run -f .env.integration -- vscode-test", "prepare": "husky", "publish:marketplace": "vsce publish && ovsx publish", "publish": "npm run build && changeset publish && npm install --package-lock-only", @@ -245,6 +245,7 @@ "devDependencies": { "@changesets/cli": "^2.27.10", "@changesets/types": "^6.0.0", + "@dotenvx/dotenvx": "^1.34.0", "@types/diff": "^5.2.1", "@types/diff-match-patch": "^1.0.36", "@types/jest": "^29.5.14", @@ -255,7 +256,6 @@ "@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", diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 139ed90..3daf0d3 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -19,15 +19,7 @@ import { findLast } from "../../shared/array" import { ApiConfigMeta, ExtensionMessage } from "../../shared/ExtensionMessage" import { HistoryItem } from "../../shared/HistoryItem" import { WebviewMessage } from "../../shared/WebviewMessage" -import { - Mode, - modes, - CustomModePrompts, - PromptComponent, - ModeConfig, - defaultModeSlug, - getModeBySlug, -} from "../../shared/modes" +import { Mode, CustomModePrompts, PromptComponent, defaultModeSlug } from "../../shared/modes" import { SYSTEM_PROMPT } from "../prompts/system" import { fileExistsAtPath } from "../../utils/fs" import { Cline } from "../Cline" @@ -37,7 +29,7 @@ import { getUri } from "./getUri" import { playSound, setSoundEnabled, setSoundVolume } from "../../utils/sound" import { checkExistKey } from "../../shared/checkExistApiConfig" import { singleCompletionHandler } from "../../utils/single-completion-handler" -import { getCommitInfo, searchCommits, getWorkingState } from "../../utils/git" +import { searchCommits } from "../../utils/git" import { ConfigManager } from "../config/ConfigManager" import { CustomModesManager } from "../config/CustomModesManager" import { EXPERIMENT_IDS, experiments as Experiments, experimentDefault, ExperimentId } from "../../shared/experiments" @@ -140,6 +132,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { private static activeInstances: Set = new Set() private disposables: vscode.Disposable[] = [] private view?: vscode.WebviewView | vscode.WebviewPanel + private isViewLaunched = false private cline?: Cline private workspaceTracker?: WorkspaceTracker mcpHub?: McpHub @@ -652,6 +645,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { ), ) + this.isViewLaunched = true break case "newTask": // Code that should run in response to the hello message command @@ -2422,7 +2416,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { // secrets - private async storeSecret(key: SecretKey, value?: string) { + public async storeSecret(key: SecretKey, value?: string) { if (value) { await this.context.secrets.store(key, value) } else { @@ -2476,4 +2470,14 @@ export class ClineProvider implements vscode.WebviewViewProvider { await this.postStateToWebview() await this.postMessageToWebview({ type: "action", action: "chatButtonClicked" }) } + + // integration tests + + get viewLaunched() { + return this.isViewLaunched + } + + get messages() { + return this.cline?.clineMessages || [] + } } diff --git a/src/test/extension.test.ts b/src/test/extension.test.ts index aa6b780..56ce9c8 100644 --- a/src/test/extension.test.ts +++ b/src/test/extension.test.ts @@ -1,121 +1,18 @@ -const assert = require("assert") -const vscode = require("vscode") -const path = require("path") -const fs = require("fs") -const dotenv = require("dotenv") +import * as assert from "assert" +import * as vscode from "vscode" -// Load test environment variables -const testEnvPath = path.join(__dirname, ".test_env") -dotenv.config({ path: testEnvPath }) - -suite("Roo Code Extension Test Suite", () => { - vscode.window.showInformationMessage("Starting Roo Code extension tests.") - - test("Extension should be present", () => { - const extension = vscode.extensions.getExtension("RooVeterinaryInc.roo-cline") - assert.notStrictEqual(extension, undefined) - }) - - test("Extension should activate", async () => { - const extension = vscode.extensions.getExtension("RooVeterinaryInc.roo-cline") - if (!extension) { - assert.fail("Extension not found") +suite("Roo Code Extension", () => { + test("OPENROUTER_API_KEY environment variable is set", () => { + if (!process.env.OPENROUTER_API_KEY) { + assert.fail("OPENROUTER_API_KEY environment variable is not set") } - 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) + const timeout = 10 * 1_000 + const interval = 1_000 + const startTime = Date.now() - // Test core commands are registered const expectedCommands = [ "roo-cline.plusButtonClicked", "roo-cline.mcpButtonClicked", @@ -128,204 +25,39 @@ suite("Roo Code Extension Test Suite", () => { "roo-cline.improveCode", ] + while (Date.now() - startTime < timeout) { + const commands = await vscode.commands.getCommands(true) + const missingCommands = [] + + for (const cmd of expectedCommands) { + if (!commands.includes(cmd)) { + missingCommands.push(cmd) + } + } + + if (missingCommands.length === 0) { + break + } + + await new Promise((resolve) => setTimeout(resolve, interval)) + } + + const commands = await vscode.commands.getCommands(true) + for (const cmd of expectedCommands) { - assert.strictEqual(commands.includes(cmd), true, `Command ${cmd} should be registered`) + assert.ok(commands.includes(cmd), `Command ${cmd} should be registered`) } }) - test("Views should be registered", () => { + test("Webview panel can be created", () => { const view = vscode.window.createWebviewPanel( "roo-cline.SidebarProvider", "Roo Code", vscode.ViewColumn.One, {}, ) - assert.notStrictEqual(view, undefined) + + assert.ok(view, "Failed to create webview panel") 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 Code", 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: { type: string; text: string }) => - 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: { type: string; text: string }) => - 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/task.test.ts b/src/test/task.test.ts new file mode 100644 index 0000000..cda766c --- /dev/null +++ b/src/test/task.test.ts @@ -0,0 +1,77 @@ +import * as assert from "assert" +import * as vscode from "vscode" + +import { ClineAPI } from "../exports/cline" +import { ClineProvider } from "../core/webview/ClineProvider" + +suite("Roo Code Task", () => { + test("Should handle prompt and response correctly", async function () { + const timeout = 30000 + const interval = 1000 + + const extension = vscode.extensions.getExtension("RooVeterinaryInc.roo-cline") + + if (!extension) { + assert.fail("Extension not found") + } + + const api: ClineAPI = await extension.activate() + const provider = api.sidebarProvider as ClineProvider + await provider.updateGlobalState("apiProvider", "openrouter") + await provider.updateGlobalState("openRouterModelId", "anthropic/claude-3.5-sonnet") + await provider.storeSecret("openRouterApiKey", process.env.OPENROUTER_API_KEY || "sk-or-v1-fake-api-key") + + // Create webview panel with development options. + const panel = vscode.window.createWebviewPanel("roo-cline.SidebarProvider", "Roo Code", vscode.ViewColumn.One, { + enableScripts: true, + enableCommandUris: true, + retainContextWhenHidden: true, + localResourceRoots: [extension.extensionUri], + }) + + try { + // Initialize provider with panel. + provider.resolveWebviewView(panel) + + // Wait for webview to launch. + let startTime = Date.now() + + while (Date.now() - startTime < timeout) { + if (provider.viewLaunched) { + break + } + + await new Promise((resolve) => setTimeout(resolve, interval)) + } + + await api.startNewTask("Hello world, what is your name? Respond with 'My name is ...'") + + // 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) { + break + } + + await new Promise((resolve) => setTimeout(resolve, interval)) + } + + if (provider.messages.length === 0) { + assert.fail("No messages received") + } + + // console.log("Provider messages:", JSON.stringify(provider.messages, null, 2)) + + assert.ok( + provider.messages.some(({ type, text }) => type === "say" && text?.includes("My name is Roo")), + "Did not receive expected response containing 'My name is Roo'", + ) + } finally { + panel.dispose() + } + }) +}) diff --git a/src/test/tsconfig.json b/src/test/tsconfig.json deleted file mode 100644 index 5c488e9..0000000 --- a/src/test/tsconfig.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "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"] -} diff --git a/tsconfig.integration.json b/tsconfig.integration.json new file mode 100644 index 0000000..0de0ea7 --- /dev/null +++ b/tsconfig.integration.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "module": "CommonJS", + "moduleResolution": "Node", + "esModuleInterop": true, + "target": "ES2022", + "lib": ["ES2022", "ESNext.Disposable", "DOM"], + "sourceMap": true, + "strict": true, + "skipLibCheck": true, + "useUnknownInCatchVariables": false, + "rootDir": "src", + "outDir": "out-integration" + }, + "include": ["**/*.ts"], + "exclude": [".vscode-test", "benchmark", "dist", "**/node_modules/**", "out", "out-integration", "webview-ui"] +}