From 58ff0b503ebdee5f60ae5cd587653be445b9ce37 Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Fri, 27 Feb 2026 12:48:33 -0800 Subject: [PATCH] fix: create independence between manager registration --- .vscode/settings.json | 3 +- src/common/utils/asyncUtils.ts | 14 +++++ src/extension.ts | 23 ++++++--- src/test/common/asyncUtils.unit.test.ts | 69 +++++++++++++++++++++++++ 4 files changed, 102 insertions(+), 7 deletions(-) create mode 100644 src/test/common/asyncUtils.unit.test.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 031b2d19..a777efbd 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -31,6 +31,7 @@ "git.branchProtectionPrompt": "alwaysCommitToNewBranch", "chat.tools.terminal.autoApprove": { "npx tsc": true, - "mkdir": true + "mkdir": true, + "npx mocha": true } } diff --git a/src/common/utils/asyncUtils.ts b/src/common/utils/asyncUtils.ts index 02bb265c..b0de59e4 100644 --- a/src/common/utils/asyncUtils.ts +++ b/src/common/utils/asyncUtils.ts @@ -1,3 +1,17 @@ +import { traceError } from '../logging'; + export async function timeout(milliseconds: number): Promise { return new Promise((resolve) => setTimeout(resolve, milliseconds)); } + +/** + * Wraps a promise so that rejection is caught and logged instead of propagated. + * Use with `Promise.all` to run tasks independently — one failure won't block the others. + */ +export async function safeRegister(name: string, task: Promise): Promise { + try { + await task; + } catch (error) { + traceError(`Failed to register ${name} features:`, error); + } +} diff --git a/src/extension.ts b/src/extension.ts index b03c5132..11aa94be 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -19,6 +19,7 @@ import { StopWatch } from './common/stopWatch'; import { EventNames } from './common/telemetry/constants'; import { sendManagerSelectionTelemetry, sendProjectStructureTelemetry } from './common/telemetry/helpers'; import { sendTelemetryEvent } from './common/telemetry/sender'; +import { safeRegister } from './common/utils/asyncUtils'; import { createDeferred } from './common/utils/deferred'; import { @@ -522,13 +523,23 @@ export async function activate(context: ExtensionContext): Promise { + let traceErrorStub: sinon.SinonStub; + + setup(() => { + traceErrorStub = sinon.stub(logging, 'traceError'); + }); + + teardown(() => { + sinon.restore(); + }); + + test('resolves when the task succeeds', async () => { + await safeRegister('test-manager', Promise.resolve()); + assert.ok(traceErrorStub.notCalled, 'traceError should not be called on success'); + }); + + test('resolves (not rejects) when the task fails', async () => { + const failing = Promise.reject(new Error('boom')); + // safeRegister must not propagate the rejection + await safeRegister('failing-manager', failing); + // If we got here without throwing, the test passes + }); + + test('logs the manager name and error when the task fails', async () => { + const error = new Error('registration exploded'); + await safeRegister('conda', Promise.reject(error)); + + assert.ok(traceErrorStub.calledOnce, 'traceError should be called once'); + const [message, loggedError] = traceErrorStub.firstCall.args; + assert.ok(message.includes('conda'), 'log message should contain the manager name'); + assert.strictEqual(loggedError, error, 'original error should be passed through'); + }); + + test('independent tasks continue when one fails', async () => { + const results: string[] = []; + + await Promise.all([ + safeRegister('will-fail', Promise.reject(new Error('fail'))), + safeRegister( + 'will-succeed-1', + Promise.resolve().then(() => { + results.push('a'); + }), + ), + safeRegister( + 'will-succeed-2', + Promise.resolve().then(() => { + results.push('b'); + }), + ), + ]); + + assert.deepStrictEqual(results.sort(), ['a', 'b'], 'both successful tasks should complete'); + assert.ok(traceErrorStub.calledOnce, 'only the failing task should log an error'); + }); + + test('handles non-Error rejections', async () => { + await safeRegister('string-reject', Promise.reject('just a string')); + + assert.ok(traceErrorStub.calledOnce); + const [, loggedError] = traceErrorStub.firstCall.args; + assert.strictEqual(loggedError, 'just a string'); + }); +});