From 0f854d883e99591281137a5a508d547f6e573927 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Wed, 11 Nov 2026 16:05:52 -0800 Subject: [PATCH] test: characterize TypeScript client lifecycle boundaries Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e9d001d1-6e2b-442b-a159-3f0ebb10b36b --- typescript/tests/activation.test.ts | 357 ++++++++++++++++++++-------- typescript/tests/server.test.ts | 187 ++++++++++++++- 2 files changed, 433 insertions(+), 111 deletions(-) diff --git a/typescript/tests/activation.test.ts b/typescript/tests/activation.test.ts index a8c20a8..76fdb9c 100644 --- a/typescript/tests/activation.test.ts +++ b/typescript/tests/activation.test.ts @@ -28,6 +28,14 @@ import * as vscodeapi from '../src/vscodeapi'; // Helpers // --------------------------------------------------------------------------- +function deferred() { + let resolve!: (value: T) => void; + const promise = new Promise((res) => { + resolve = res; + }); + return { promise, resolve }; +} + function makeToolConfig(overrides?: Partial): ToolConfig { return { toolId: 'flake8', @@ -269,50 +277,117 @@ suite('createToolContext', () => { assert.isUndefined(ctx.lsClient, 'client should be cleared'); }); - test('dispose during in-flight restart prevents client assignment', async () => { - let resolveRestart: ((value: unknown) => void) | undefined; - (serverModule.restartServer as sinon.SinonStub).returns( - new Promise((resolve) => { - resolveRestart = resolve; - }), - ); - - const ctx = createToolContext(makeOptions()); - const runPromise = ctx.runServer(); - - // Dispose while restartServer is in-flight - ctx.dispose(); - - // Now resolve the restart — should NOT assign client - const mockClient = { stop: sandbox.stub().resolves(), dispose: sandbox.stub() }; - resolveRestart!({ client: mockClient, disposables: [] }); - await runPromise; - - assert.isUndefined(ctx.lsClient, 'should not assign client after dispose'); + test('dispose during in-flight restart cleans up late results and cancels queued retry', async () => { + const clock = sandbox.useFakeTimers(); + const entered = deferred(); + const finished = deferred(); + const restartStub = serverModule.restartServer as sinon.SinonStub; + restartStub.callsFake(() => { + entered.resolve(); + return finished.promise; + }); + const stop = sandbox.stub().resolves(); + const dispose = sandbox.stub(); + const listener = { dispose: sandbox.stub() }; + const result = { + client: { stop, dispose } as unknown as LanguageClient, + disposables: [listener], + }; + const ctx = createToolContext(makeOptions({ toolConfig: makeToolConfig({ restartDelay: 50 }) })); + const running = ctx.runServer(); + + try { + await entered.promise; + await ctx.runServer(); + assert.strictEqual(clock.countTimers(), 1, 'overlap should queue a retry'); + + ctx.dispose(); + assert.strictEqual(clock.countTimers(), 0, 'dispose should cancel the retry timer'); + finished.resolve(result); + await running; + + assert.isUndefined(ctx.lsClient, 'should not assign the late client'); + sinon.assert.calledOnce(stop); + sinon.assert.calledOnce(dispose); + sinon.assert.calledOnce(listener.dispose); + + await ctx.runServer(); + await clock.tickAsync(100); + sinon.assert.calledOnce(restartStub); + assert.isUndefined(ctx.lsClient, 'calls and timers must not resurrect the context'); + } finally { + ctx.dispose(); + finished.resolve(result); + await running; + } }); - test('runServer debounces rapid calls', async () => { + test('overlapping runServer calls acknowledge scheduling and reset a single trailing retry', async () => { const clock = sandbox.useFakeTimers(); - const restartServerStub = serverModule.restartServer as sinon.SinonStub; + const entered = deferred(); + const finished = deferred(); + const restartStub = serverModule.restartServer as sinon.SinonStub; + const firstClient = new LanguageClient('first', 'First', { command: 'python' }, {}); + const secondClient = new LanguageClient('second', 'Second', { command: 'python' }, {}); + const firstResult = { client: firstClient, disposables: [] }; + restartStub.onFirstCall().callsFake(() => { + entered.resolve(); + return finished.promise; + }); + restartStub.onSecondCall().resolves({ client: secondClient, disposables: [] }); const ctx = createToolContext(makeOptions({ toolConfig: makeToolConfig({ restartDelay: 50 }) })); + let firstSettled = false; + const first = ctx.runServer().then(() => { + firstSettled = true; + }); - // First call triggers. isRestarting is set synchronously. - const p1 = ctx.runServer(); - // Second call while first is in progress — hits isRestarting, schedules timer. - const p2 = ctx.runServer(); - - // p2 resolves immediately (debounced path returns early). - await p2; - - // Complete the first call (flushes all internal awaits). - await p1; - assert.isTrue(restartServerStub.calledOnce, 'first call should trigger restartServer'); - - // Tick past the debounce delay — the scheduled timer should fire. - await clock.tickAsync(50); - assert.isTrue(restartServerStub.calledTwice, 'debounced call should trigger restartServer after delay'); - - clock.restore(); + try { + await entered.promise; + await ctx.runServer(); + assert.isFalse(firstSettled, 'overlap resolves without waiting for the active restart'); + assert.isUndefined(ctx.lsClient); + sinon.assert.calledOnce(restartStub); + + await clock.tickAsync(30); + await ctx.runServer(); + assert.isFalse(firstSettled); + assert.strictEqual(clock.countTimers(), 1, 'overlaps should replace, not accumulate, timers'); + + await clock.tickAsync(20); + sinon.assert.calledOnce(restartStub); + await ctx.runServer(); + assert.strictEqual(clock.countTimers(), 1); + await clock.tickAsync(30); + sinon.assert.calledOnce(restartStub); + await clock.tickAsync(19); + sinon.assert.calledOnce(restartStub); + assert.isFalse(firstSettled, 'the active restart is still held at the reset trailing edge'); + + await clock.tickAsync(1); + sinon.assert.calledOnce(restartStub); + assert.strictEqual(clock.countTimers(), 1, 'a retry during an active restart should be rescheduled'); + assert.isFalse(firstSettled); + + finished.resolve(firstResult); + await first; + assert.strictEqual(ctx.lsClient, firstClient); + + await clock.tickAsync(49); + sinon.assert.calledOnce(restartStub); + await clock.tickAsync(1); + sinon.assert.calledTwice(restartStub); + assert.strictEqual(restartStub.secondCall.args[1], firstClient); + assert.strictEqual(ctx.lsClient, secondClient); + + await clock.tickAsync(100); + sinon.assert.calledTwice(restartStub); + } finally { + ctx.dispose(); + finished.resolve(firstResult); + await first; + firstClient.dispose(); + secondClient.dispose(); + } }); test('initialize starts server when interpreter is already set', async () => { @@ -425,52 +500,93 @@ suite('createToolContext', () => { ); }); - test('subscribes to package changes and restarts the server when refreshExtensionOnPackagesChange is enabled', async () => { + test('subscribes to package changes and debounces bursts at the reset trailing edge', async () => { + const clock = sandbox.useFakeTimers(); (utilities.getInterpreterFromSetting as sinon.SinonStub).returns(undefined); const provider = makeMockProvider(sandbox); - // Capture the handler the activation logic registers so we can fire it. let capturedHandler: (() => void) | undefined; - const disposeStub = sinon.stub(); + const disposeStub = sandbox.stub(); (provider.subscribeToPackageChanges as sinon.SinonStub).callsFake((handler: () => void) => { capturedHandler = handler; return Promise.resolve({ dispose: disposeStub }); }); - - // Resolve a deferred when restartServer is reached instead of counting - // macrotasks — decouples the test from runServer's internal await chain. - let signalRestart: () => void = () => undefined; - const restarted = new Promise((resolve) => { - signalRestart = resolve; - }); - (serverModule.restartServer as sinon.SinonStub).callsFake(() => { - signalRestart(); + const entered = deferred(); + const restartStub = serverModule.restartServer as sinon.SinonStub; + restartStub.callsFake(() => { + entered.resolve(); return Promise.resolve({ client: undefined, disposables: [] }); }); - const subscriptions: vscode.Disposable[] = []; const ctx = createToolContext( makeOptions({ pythonProvider: provider, - // restartDelay: 0 keeps the trailing-edge debounce on the next tick. - toolConfig: makeToolConfig({ refreshExtensionOnPackagesChange: true, restartDelay: 0 }), + toolConfig: makeToolConfig({ refreshExtensionOnPackagesChange: true, restartDelay: 50 }), }), ); - await ctx.initialize(subscriptions); + const runSpy = sandbox.spy(ctx, 'runServer'); + + try { + await ctx.initialize(subscriptions); + sinon.assert.calledOnce(provider.subscribeToPackageChanges as sinon.SinonStub); + assert.isFunction(capturedHandler); + assert.lengthOf(subscriptions, 1, 'subscription belongs to the extension'); + + capturedHandler!(); + await clock.tickAsync(30); + capturedHandler!(); + await clock.tickAsync(20); + sinon.assert.notCalled(restartStub); + await clock.tickAsync(29); + sinon.assert.notCalled(restartStub); + + await clock.tickAsync(1); + await entered.promise; + await runSpy.firstCall.returnValue; + sinon.assert.calledOnce(restartStub); + + await clock.tickAsync(100); + sinon.assert.calledOnce(restartStub); + } finally { + ctx.dispose(); + subscriptions.forEach((subscription) => subscription.dispose()); + if (runSpy.called) { + await runSpy.firstCall.returnValue; + } + } + }); - assert.isTrue( - (provider.subscribeToPackageChanges as sinon.SinonStub).calledOnce, - 'should subscribe to package changes', + test('dispose cancels pending package refresh and ignores later package events', async () => { + const clock = sandbox.useFakeTimers(); + const provider = makeMockProvider(sandbox); + const restartStub = serverModule.restartServer as sinon.SinonStub; + const ctx = createToolContext( + makeOptions({ + pythonProvider: provider, + toolConfig: makeToolConfig({ refreshExtensionOnPackagesChange: true, restartDelay: 50 }), + }), ); - assert.isFunction(capturedHandler, 'should register a package-change handler'); - assert.lengthOf(subscriptions, 1, 'should register the subscription disposable'); - capturedHandler?.(); - await restarted; - assert.isTrue( - (serverModule.restartServer as sinon.SinonStub).called, - 'package-change handler should restart the server', - ); + try { + await ctx.initialize([]); + const handler = (provider.subscribeToPackageChanges as sinon.SinonStub) + .firstCall.args[0] as () => void; + sinon.assert.calledOnce(restartStub); + + handler(); + await clock.tickAsync(25); + assert.strictEqual(clock.countTimers(), 1); + ctx.dispose(); + assert.strictEqual(clock.countTimers(), 0, 'dispose should cancel package debounce'); + + await clock.tickAsync(100); + handler(); + assert.strictEqual(clock.countTimers(), 0, 'late package events must not schedule work'); + await clock.tickAsync(100); + sinon.assert.calledOnce(restartStub); + } finally { + ctx.dispose(); + } }); test('subscribes to package changes even when the interpreter is pinned via setting', async () => { @@ -626,8 +742,13 @@ suite('registerCommonSubscriptions', () => { // --------------------------------------------------------------------------- suite('deactivateServer', () => { - test('stops the client and disposes context', async () => { - const stop = sinon.stub().resolves(); + test('disposes context before awaiting client stop', async () => { + const entered = deferred(); + const finished = deferred(); + const stop = sinon.stub().callsFake(() => { + entered.resolve(); + return finished.promise; + }); const dispose = sinon.stub(); const client = { stop } as unknown as LanguageClient; const ctx: ToolExtensionContext = { @@ -636,9 +757,24 @@ suite('deactivateServer', () => { initialize: sinon.stub().resolves(), dispose, }; - await deactivateServer(ctx); - assert.isTrue(stop.calledOnce); - assert.isTrue(dispose.calledOnce); + let deactivated = false; + const pending = deactivateServer(ctx).then(() => { + deactivated = true; + }); + try { + await entered.promise; + sinon.assert.calledOnce(stop); + sinon.assert.calledOnce(dispose); + sinon.assert.callOrder(dispose, stop); + assert.isFalse(deactivated, 'deactivation must wait for the already-assigned client to stop'); + + finished.resolve(); + await pending; + assert.isTrue(deactivated); + } finally { + finished.resolve(); + await pending; + } }); test('handles undefined context', async () => { @@ -859,39 +995,62 @@ suite('createToolContext – NullFormatter lifecycle', () => { assert.isTrue(providerDispose.calledOnce, 'placeholder disposed on ctx.dispose()'); }); - // Test 7: extension-driven restart — second runServer() is called while - // the first client is still Running. The guard at the top of runServer - // skips re-registration to avoid the transient duplicate-formatter issue. - // The placeholder is re-registered only after restartServer returns the - // new (not-yet-Running) client. - test('re-registers placeholder on extension-driven restart (second runServer call)', async () => { + test('holds formatter fallback until replacement completes and ignores stale client events', async () => { const firstClient = makeMockClient(State.Stopped); const secondClient = makeMockClient(State.Stopped); + const entered = deferred(); + const finished = deferred(); + const replacement = { client: asLC(secondClient), disposables: [] }; const restartStub = sandbox.stub(serverModule, 'restartServer'); restartStub.onFirstCall().resolves({ client: asLC(firstClient), disposables: [] }); - restartStub.onSecondCall().resolves({ client: asLC(secondClient), disposables: [] }); - + restartStub.onSecondCall().callsFake(() => { + entered.resolve(); + return finished.promise; + }); const ctx = createToolContext(makeFormatterOptions()); - assert.strictEqual(registerFormattingProviderStub.callCount, 1, 'registered at activation'); - - // First run — transition to Running disposes placeholder - await ctx.runServer(); - firstClient.simulateStateChange(State.Running); - assert.strictEqual(providerDispose.callCount, 1, 'placeholder disposed after first Running'); - - // Second runServer() — simulates config/interpreter-driven restart. - // firstClient is still Running, so the guard at the top of runServer - // skips register (avoiding transient duplicate). After restartServer - // returns the new client (Stopped), the placeholder is re-registered. - await ctx.runServer(); - assert.strictEqual( - registerFormattingProviderStub.callCount, 2, - 'placeholder re-registered after restartServer returns non-Running client', - ); - - // Transition second client to Running — placeholder disposed again - secondClient.simulateStateChange(State.Running); - assert.strictEqual(providerDispose.callCount, 2, 'placeholder disposed after second Running'); + let replacing: Promise | undefined; + + try { + await ctx.runServer(); + firstClient.simulateStateChange(State.Running); + sinon.assert.calledOnce(providerDispose); + + replacing = ctx.runServer(); + await entered.promise; + assert.strictEqual(firstClient.state, State.Running); + assert.strictEqual(ctx.lsClient, asLC(firstClient)); + sinon.assert.calledOnce(registerFormattingProviderStub); + sinon.assert.calledOnce(providerDispose); + + // The old listener is gone; fallback returns only at replacement completion. + firstClient.simulateStateChange(State.Stopped); + sinon.assert.calledOnce(registerFormattingProviderStub); + + finished.resolve(replacement); + await replacing; + assert.strictEqual(ctx.lsClient, asLC(secondClient)); + sinon.assert.calledTwice(registerFormattingProviderStub); + + firstClient.simulateStateChange(State.Running); + sinon.assert.calledOnce(providerDispose); + secondClient.simulateStateChange(State.Running); + sinon.assert.calledTwice(providerDispose); + firstClient.simulateStateChange(State.Stopped); + sinon.assert.calledTwice(registerFormattingProviderStub); + + ctx.dispose(); + firstClient.simulateStateChange(State.Starting); + secondClient.simulateStateChange(State.Stopped); + secondClient.simulateStateChange(State.Running); + sinon.assert.calledTwice(registerFormattingProviderStub); + sinon.assert.calledTwice(providerDispose); + } finally { + ctx.dispose(); + finished.resolve(replacement); + await replacing; + firstClient.dispose(); + secondClient.dispose(); + } }); // Test 8: restartServer returns no client — placeholder stays registered diff --git a/typescript/tests/server.test.ts b/typescript/tests/server.test.ts index 1f834b7..3369d70 100644 --- a/typescript/tests/server.test.ts +++ b/typescript/tests/server.test.ts @@ -4,6 +4,7 @@ import { assert } from 'chai'; import * as sinon from 'sinon'; import { LanguageStatusSeverity, LogOutputChannel, Uri, LogLevel, WorkspaceFolder } from 'vscode'; +import { State } from 'vscode-languageclient'; import { LanguageClient } from 'vscode-languageclient/node'; import { getServerCwd, createServer, restartServer, CreateServerOptions, RestartServerOptions, RestartServerResult } from '../src/server'; import * as envFileModule from '../src/envFile'; @@ -18,6 +19,14 @@ import { PythonEnvironmentsProvider } from '../src/python'; // Helpers // --------------------------------------------------------------------------- +function deferred() { + let resolve!: (value: T) => void; + const promise = new Promise((res) => { + resolve = res; + }); + return { promise, resolve }; +} + function makeSettings(overrides?: Partial): IBaseSettings { return { cwd: '/workspace', @@ -307,6 +316,11 @@ suite('restartServer', () => { }; } + function disposeResult(result?: RestartServerResult): void { + result?.disposables.forEach((disposable) => disposable.dispose()); + result?.client?.dispose(); + } + test('returns a result with client and disposables', async () => { const result = await restartServer(makeRestartOptions()); assert.isDefined(result.client); @@ -314,12 +328,36 @@ suite('restartServer', () => { assert.isAbove(result.disposables.length, 0); }); - test('stops old client before creating new one', async () => { - const oldStop = sandbox.stub().resolves(); + test('awaits old client stop before creating and starting the replacement', async () => { + const entered = deferred(); + const finished = deferred(); + const oldStop = sandbox.stub().callsFake(() => { + entered.resolve(); + return finished.promise; + }); const oldClient = { stop: oldStop } as unknown as LanguageClient; + const startSpy = sandbox.spy(LanguageClient.prototype, 'start'); + const options = makeRestartOptions(); + const restarting = restartServer(options, oldClient); + let result: RestartServerResult | undefined; - await restartServer(makeRestartOptions(), oldClient); - assert.isTrue(oldStop.calledOnce, 'old client stop() should be called'); + try { + await entered.promise; + sinon.assert.calledOnce(oldStop); + sinon.assert.notCalled(envFileModule.getEnvFileVars as sinon.SinonStub); + sinon.assert.notCalled(startSpy); + sinon.assert.notCalled(updateStatusStub); + + finished.resolve(); + result = await restarting; + assert.isDefined(result.client); + sinon.assert.calledOnce(startSpy); + sinon.assert.callOrder(oldStop, envFileModule.getEnvFileVars as sinon.SinonStub, startSpy); + } finally { + finished.resolve(); + result ??= await restarting; + disposeResult(result); + } }); test('handles old client stop failure gracefully', async () => { @@ -327,19 +365,144 @@ suite('restartServer', () => { stop: sandbox.stub().rejects(new Error('stop failed')), } as unknown as LanguageClient; - // Should not throw + const startSpy = sandbox.spy(LanguageClient.prototype, 'start'); const result = await restartServer(makeRestartOptions(), oldClient); - assert.isDefined(result.client); + try { + assert.isDefined(result.client); + sinon.assert.calledOnce(startSpy); + assert.deepEqual(updateStatusStub.lastCall.args, [ + undefined, LanguageStatusSeverity.Information, false, + ]); + } finally { + disposeResult(result); + } }); - test('updates status to busy during restart', async () => { - await restartServer(makeRestartOptions()); - assert.isTrue( - updateStatusStub.calledWith(undefined, LanguageStatusSeverity.Information, true), - 'updateStatus should be called with busy=true', - ); + test('awaits start and trace before clearing busy and installing the state listener', async () => { + const startEntered = deferred(); + const startFinished = deferred(); + const traceEntered = deferred(); + const traceFinished = deferred(); + const originalStart = LanguageClient.prototype.start; + const startStub = sandbox.stub(LanguageClient.prototype, 'start').callsFake(async function (this: LanguageClient) { + startEntered.resolve(this); + await startFinished.promise; + await originalStart.call(this); + }); + const traceStub = sandbox.stub(LanguageClient.prototype, 'setTrace').callsFake(() => { + traceEntered.resolve(); + return traceFinished.promise; + }); + const listenerSpy = sandbox.spy(LanguageClient.prototype, 'onDidChangeState'); + const restarting = restartServer(makeRestartOptions()); + let result: RestartServerResult | undefined; + + try { + const client = await startEntered.promise; + sinon.assert.calledOnceWithExactly(updateStatusStub, undefined, LanguageStatusSeverity.Information, true); + sinon.assert.notCalled(traceStub); + sinon.assert.notCalled(listenerSpy); + + startFinished.resolve(); + await traceEntered.promise; + assert.strictEqual(client.state, State.Running, 'mock start changes state without emitting an event'); + sinon.assert.calledOnceWithExactly(updateStatusStub, undefined, LanguageStatusSeverity.Information, true); + sinon.assert.notCalled(listenerSpy); + + traceFinished.resolve(); + result = await restarting; + assert.strictEqual(result.client, client); + assert.lengthOf(result.disposables, 1); + assert.strictEqual(updateStatusStub.callCount, 2, 'success must explicitly clear busy without a state event'); + assert.deepEqual(updateStatusStub.lastCall.args, [undefined, LanguageStatusSeverity.Information, false]); + sinon.assert.calledOnce(listenerSpy); + sinon.assert.callOrder(startStub, traceStub, listenerSpy); + assert.isTrue(updateStatusStub.lastCall.calledBefore(listenerSpy.firstCall)); + + const mockClient = client as unknown as import('./_languageclient_mock').LanguageClient; + mockClient.simulateStateChange(State.Stopped); + assert.strictEqual(updateStatusStub.callCount, 2); + mockClient.simulateStateChange(State.Running); + assert.strictEqual(updateStatusStub.callCount, 3, 'returned listener should report future Running events'); + assert.deepEqual(updateStatusStub.lastCall.args, [undefined, LanguageStatusSeverity.Information, false]); + + result.disposables.forEach((disposable) => disposable.dispose()); + mockClient.simulateStateChange(State.Stopped); + mockClient.simulateStateChange(State.Running); + assert.strictEqual(updateStatusStub.callCount, 3, 'disposed listener must no longer update status'); + } finally { + startFinished.resolve(); + traceFinished.resolve(); + result ??= await restarting; + disposeResult(result); + } }); + test('reports creation failure without starting a client or installing a listener', async () => { + const startSpy = sandbox.spy(LanguageClient.prototype, 'start'); + const traceSpy = sandbox.spy(LanguageClient.prototype, 'setTrace'); + const listenerSpy = sandbox.spy(LanguageClient.prototype, 'onDidChangeState'); + (envFileModule.getEnvFileVars as sinon.SinonStub).rejects(new Error('env file failed')); + const result = await restartServer(makeRestartOptions()); + + try { + assert.isUndefined(result.client); + assert.isEmpty(result.disposables); + assert.strictEqual(updateStatusStub.callCount, 2); + assert.deepEqual(updateStatusStub.firstCall.args, [undefined, LanguageStatusSeverity.Information, true]); + assert.strictEqual(updateStatusStub.lastCall.args[1], LanguageStatusSeverity.Error); + assert.isFalse(updateStatusStub.calledWith(undefined, LanguageStatusSeverity.Information, false)); + sinon.assert.notCalled(startSpy); + sinon.assert.notCalled(traceSpy); + sinon.assert.notCalled(listenerSpy); + } finally { + disposeResult(result); + } + }); + + for (const phase of ['start', 'setTrace'] as const) { + test(`reports ${phase} rejection and disposes the client without installing a listener`, async () => { + const entered = deferred(); + const finished = deferred(); + sandbox.stub(LanguageClient.prototype, phase).callsFake(async function (this: LanguageClient) { + entered.resolve(this); + await finished.promise; + throw new Error(`${phase} failed`); + }); + const traceSpy = phase === 'start' ? sandbox.spy(LanguageClient.prototype, 'setTrace') : undefined; + const disposeSpy = sandbox.spy(LanguageClient.prototype, 'dispose'); + const listenerSpy = sandbox.spy(LanguageClient.prototype, 'onDidChangeState'); + const restarting = restartServer(makeRestartOptions()); + let result: RestartServerResult | undefined; + let client: LanguageClient | undefined; + + try { + client = await entered.promise; + sinon.assert.calledOnceWithExactly(updateStatusStub, undefined, LanguageStatusSeverity.Information, true); + sinon.assert.notCalled(disposeSpy); + sinon.assert.notCalled(listenerSpy); + + finished.resolve(); + result = await restarting; + assert.isUndefined(result.client); + assert.isEmpty(result.disposables); + assert.strictEqual(updateStatusStub.lastCall.args[1], LanguageStatusSeverity.Error); + assert.isFalse(updateStatusStub.calledWith(undefined, LanguageStatusSeverity.Information, false)); + sinon.assert.calledOnce(disposeSpy); + sinon.assert.calledOn(disposeSpy, client); + sinon.assert.notCalled(listenerSpy); + if (traceSpy) { + sinon.assert.notCalled(traceSpy); + } + } finally { + finished.resolve(); + result ??= await restarting; + disposeResult(result); + client?.dispose(); + } + }); + } + test('uses supplied extension settings without resolving them again', async () => { const extensionSettings = [ makeSettings({ workspace: 'file:///workspace/project', interpreter: ['/project/python'] }),