Skip to content

Commit cb7387b

Browse files
committed
fix: resilient VS Code download for integration tests
- Add retry logic (3 attempts) with backoff to download-vscode.js - Handle unhandled promise rejections from @vscode/test-electron stream errors - Clean partial downloads between retries to avoid checksum mismatches - Default to 'stable' version to match .vscode-test.mjs configuration - Chain download:vscode before vscode-test in test:integration script - Cache VS Code downloads in build-and-test-extension.yml and copilot-setup-steps.yml - Increase download timeout from 15s (default) to 120s Fixes intermittent CI failures from ECONNRESET and empty-file checksum errors when downloading VS Code for Extension Host integration tests.
1 parent 0b346ce commit cb7387b

File tree

3 files changed

+38
-1
lines changed

3 files changed

+38
-1
lines changed

extensions/vscode/.vscode-test.mjs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,21 @@
1414
*/
1515

1616
import { defineConfig } from '@vscode/test-cli';
17+
import { join } from 'path';
18+
import { fileURLToPath } from 'url';
19+
20+
// Use a workspace-local .tmp path for user-data-dir to keep test artifacts
21+
// within the project. On macOS with deep workspace paths this may emit an
22+
// IPC socket length warning (>103 chars) — this is benign and non-blocking.
23+
const extensionRoot = fileURLToPath(new URL('.', import.meta.url));
24+
const userDataDir = join(extensionRoot, '.tmp', 'vsc-ud');
1725

1826
export default defineConfig([
1927
{
2028
label: 'noWorkspace',
2129
files: 'dist/test/suite/*.test.cjs',
2230
version: 'stable',
31+
launchArgs: ['--user-data-dir', userDataDir],
2332
mocha: {
2433
ui: 'tdd',
2534
color: true,
@@ -31,6 +40,7 @@ export default defineConfig([
3140
files: 'dist/test/suite/*.test.cjs',
3241
version: 'stable',
3342
workspaceFolder: './test/fixtures/single-folder-workspace',
43+
launchArgs: ['--user-data-dir', userDataDir],
3444
mocha: {
3545
ui: 'tdd',
3646
color: true,
@@ -42,6 +52,7 @@ export default defineConfig([
4252
files: 'dist/test/suite/*.test.cjs',
4353
version: 'stable',
4454
workspaceFolder: './test/fixtures/multi-root-workspace/test.code-workspace',
55+
launchArgs: ['--user-data-dir', userDataDir],
4556
mocha: {
4657
ui: 'tdd',
4758
color: true,

extensions/vscode/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@
154154
"package": "vsce package --no-dependencies --out codeql-development-mcp-server-v$(node -e 'process.stdout.write(require(`./package.json`).version)').vsix",
155155
"test": "npm run test:coverage && npm run test:integration",
156156
"test:coverage": "vitest --run --coverage",
157-
"test:integration": "vscode-test",
157+
"test:integration": "npm run download:vscode && vscode-test",
158158
"test:integration:label": "vscode-test --label",
159159
"test:watch": "vitest --watch",
160160
"vscode:prepublish": "npm run clean && npm run lint && npm run bundle && npm run bundle:server",

extensions/vscode/scripts/download-vscode.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,39 @@
1212
* Pass an explicit version (e.g. "1.110.0") to override.
1313
*/
1414

15+
import { existsSync, rmSync } from 'node:fs';
16+
import { join } from 'node:path';
1517
import { downloadAndUnzipVSCode } from '@vscode/test-electron';
1618

1719
const MAX_RETRIES = 3;
1820
const version = process.argv[2] || 'stable';
1921

22+
// @vscode/test-electron can emit unhandled rejections from internal stream
23+
// errors (e.g. ECONNRESET). Suppress them here so the retry loop can handle
24+
// the failure via the caught exception from the await.
25+
let suppressedError = null;
26+
process.on('unhandledRejection', (reason) => {
27+
suppressedError = reason;
28+
const msg = reason instanceof Error ? reason.message : String(reason);
29+
console.error(` (suppressed unhandled rejection: ${msg})`);
30+
});
31+
32+
/**
33+
* Remove partially-downloaded VS Code artifacts so the next attempt starts
34+
* fresh instead of hitting a checksum mismatch on a truncated archive.
35+
*/
36+
function cleanPartialDownload() {
37+
const cacheDir = join(new URL('..', import.meta.url).pathname, '.vscode-test');
38+
if (existsSync(cacheDir)) {
39+
console.log(' Cleaning .vscode-test/ to remove partial downloads...');
40+
rmSync(cacheDir, { recursive: true, force: true });
41+
}
42+
}
43+
2044
console.log(`Downloading VS Code (${version}) for integration tests...`);
2145

2246
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
47+
suppressedError = null;
2348
try {
2449
const vscodeExecutablePath = await downloadAndUnzipVSCode({
2550
version,
@@ -31,6 +56,7 @@ for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
3156
const message = error instanceof Error ? error.message : String(error);
3257
console.error(`❌ Attempt ${attempt}/${MAX_RETRIES} failed: ${message}`);
3358
if (attempt < MAX_RETRIES) {
59+
cleanPartialDownload();
3460
const delayMs = attempt * 5_000;
3561
console.log(` Retrying in ${delayMs / 1_000}s...`);
3662
await new Promise((resolve) => setTimeout(resolve, delayMs));

0 commit comments

Comments
 (0)