mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 04:11:10 -05:00
Merge pull request #270 from RooVetGit/feat/full_Integration_Tests
Set up vscode integration test
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -11,3 +11,6 @@ roo-cline-*.vsix
|
|||||||
|
|
||||||
# Local prompts and rules
|
# Local prompts and rules
|
||||||
/local-prompts
|
/local-prompts
|
||||||
|
|
||||||
|
# Test environment
|
||||||
|
.test_env
|
||||||
@@ -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
14
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
5
src/exports/cline.d.ts
vendored
5
src/exports/cline.d.ts
vendored
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ export function createClineAPI(outputChannel: vscode.OutputChannel, sidebarProvi
|
|||||||
invoke: "secondaryButtonClick",
|
invoke: "secondaryButtonClick",
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
sidebarProvider: sidebarProvider,
|
||||||
}
|
}
|
||||||
|
|
||||||
return api
|
return api
|
||||||
|
|||||||
@@ -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
19
src/test/tsconfig.json
Normal 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"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user