From ba94c06818f9ed191fb962aad2738e3a45daae08 Mon Sep 17 00:00:00 2001 From: Dmitriy Vasyura Date: Sun, 20 Sep 2026 08:16:13 -0700 Subject: [PATCH] chat: wait for native metered state before plugin updates Stack plugin marketplace checks and automatic plugin updates on the native metered service PR. Wait for initial state before checking or updating, and retain readiness and disposal regression coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../contrib/chat/browser/pluginAutoUpdate.ts | 1 + .../plugins/pluginMarketplaceService.ts | 14 +++++++- .../browser/plugins/pluginAutoUpdate.test.ts | 24 +++++++++++-- .../plugins/pluginMarketplaceService.test.ts | 36 +++++++++++++++++++ 4 files changed, 72 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/pluginAutoUpdate.ts b/src/vs/workbench/contrib/chat/browser/pluginAutoUpdate.ts index 712cb3c1d8495..a1c6e97a4244c 100644 --- a/src/vs/workbench/contrib/chat/browser/pluginAutoUpdate.ts +++ b/src/vs/workbench/contrib/chat/browser/pluginAutoUpdate.ts @@ -64,6 +64,7 @@ export class PluginAutoUpdate extends Disposable implements IWorkbenchContributi } private async _triggerAutoUpdate(marketplaceIds: ReadonlySet): Promise { + await this._meteredConnectionService.whenInitialized; if (this._store.isDisposed || this._updateInFlight || this._meteredConnectionService.isConnectionMetered) { return; } diff --git a/src/vs/workbench/contrib/chat/common/plugins/pluginMarketplaceService.ts b/src/vs/workbench/contrib/chat/common/plugins/pluginMarketplaceService.ts index ac3f0376b5607..cfd0b8d151593 100644 --- a/src/vs/workbench/contrib/chat/common/plugins/pluginMarketplaceService.ts +++ b/src/vs/workbench/contrib/chat/common/plugins/pluginMarketplaceService.ts @@ -410,7 +410,7 @@ export class PluginMarketplaceService extends Disposable implements IPluginMarke this._register(runWhenGlobalIdle(() => { this._updateChecksInitialized = true; - this._scheduleUpdateCheck(); + void this._initializeUpdateChecks(); this._register(Event.filter( _configurationService.onDidChangeConfiguration, e => e.affectsConfiguration(AutoUpdateConfigurationKey) @@ -442,6 +442,13 @@ export class PluginMarketplaceService extends Disposable implements IPluginMarke })); } + private async _initializeUpdateChecks(): Promise { + await this._meteredConnectionService.whenInitialized; + if (!this._store.isDisposed) { + this._scheduleUpdateCheck(); + } + } + clearUpdatesAvailable(marketplaceIds?: ReadonlySet): void { const remaining = marketplaceIds ? new Set([...this._marketplacesWithUpdates.get()].filter(id => !marketplaceIds.has(id))) @@ -878,6 +885,11 @@ export class PluginMarketplaceService extends Disposable implements IPluginMarke } private async _doRunUpdateCheck(): Promise { + await this._meteredConnectionService.whenInitialized; + if (this._store.isDisposed) { + return; + } + if (this._meteredConnectionService.isConnectionMetered) { return; } diff --git a/src/vs/workbench/contrib/chat/test/browser/plugins/pluginAutoUpdate.test.ts b/src/vs/workbench/contrib/chat/test/browser/plugins/pluginAutoUpdate.test.ts index 663ae6ba2af01..bda92afd3df70 100644 --- a/src/vs/workbench/contrib/chat/test/browser/plugins/pluginAutoUpdate.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/plugins/pluginAutoUpdate.test.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; +import { DeferredPromise } from '../../../../../../base/common/async.js'; import { CancellationToken } from '../../../../../../base/common/cancellation.js'; import { Emitter } from '../../../../../../base/common/event.js'; import { Disposable } from '../../../../../../base/common/lifecycle.js'; @@ -45,9 +46,13 @@ suite('PluginAutoUpdate', () => { clearUpdatesAvailableCalls: ReadonlySet[]; } - function createContribution(stateOverrides?: Partial, isConnectionMetered = false): { contribution: PluginAutoUpdate; state: MockState; meteredConnectionService: TestMeteredConnectionService } { + function createContribution( + stateOverrides?: Partial, + isConnectionMetered = false, + whenInitialized: Promise = Promise.resolve(), + ): { contribution: PluginAutoUpdate; state: MockState; meteredConnectionService: TestMeteredConnectionService } { const instantiationService = store.add(new TestInstantiationService()); - const meteredConnectionService = store.add(new TestMeteredConnectionService(isConnectionMetered)); + const meteredConnectionService = store.add(new TestMeteredConnectionService(isConnectionMetered, whenInitialized)); const state: MockState = { marketplacesWithUpdates: observableValue>('test.marketplacesWithUpdates', new Set()), @@ -104,6 +109,21 @@ suite('PluginAutoUpdate', () => { })), [{ silent: true, automatic: true, marketplaceIds: ['github:microsoft/plugins'] }]); }); + test('waits for connection state initialization before updating', async () => { + const initialized = new DeferredPromise(); + const { state } = createContribution(undefined, false, initialized.p); + + state.marketplacesWithUpdates.set(new Set(['github:microsoft/plugins']), undefined); + await flushMicrotasks(); + assert.strictEqual(state.updateAllCalls.length, 0); + + initialized.complete(); + await flushMicrotasks(); + await flushMicrotasks(); + + assert.deepStrictEqual(state.updateAllCalls.map(call => [...call.marketplaceIds ?? []]), [['github:microsoft/plugins']]); + }); + test('retains queued updates while metered and runs them when unmetered', async () => { const { state, meteredConnectionService } = createContribution(undefined, true); diff --git a/src/vs/workbench/contrib/chat/test/common/plugins/pluginMarketplaceService.test.ts b/src/vs/workbench/contrib/chat/test/common/plugins/pluginMarketplaceService.test.ts index c3d08a3079278..432d075aa8614 100644 --- a/src/vs/workbench/contrib/chat/test/common/plugins/pluginMarketplaceService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/plugins/pluginMarketplaceService.test.ts @@ -788,6 +788,41 @@ suite('PluginMarketplaceService - installed plugins lifecycle', () => { assert.strictEqual(fetchCount, 1); }); + test('periodic update checking waits for metered connection initialization', async () => { + let runIdle: ((idle: IdleDeadline) => void) | undefined; + store.add(installFakeRunWhenIdle((_target, runner) => { + runIdle = runner; + return Disposable.None; + })); + const initialized = new DeferredPromise(); + const meteredConnectionService = store.add(new TestMeteredConnectionService(false, initialized.p)); + let fetchCount = 0; + const service = createService({ + meteredConnectionService, + pluginRepositoryService: { + fetchRepository: async () => { + fetchCount++; + return false; + }, + }, + }); + service.addInstalledPlugin( + URI.file('/agent-plugins/github.com/microsoft/plugins/my-plugin'), + makePlugin('my-plugin', 'my-plugin'), + ); + + assert.ok(runIdle); + runIdle({ didTimeout: false, timeRemaining: () => 50 }); + await timeout(0); + assert.strictEqual(fetchCount, 0); + + initialized.complete(); + await timeout(0); + await timeout(0); + + assert.strictEqual(fetchCount, 1); + }); + test('defers an overdue check until queued updates are acknowledged', async () => { const updateCheckInterval = 24 * 60 * 60 * 1000; const clock = sinon.useFakeTimers({ now: updateCheckInterval + 1 }); @@ -993,6 +1028,7 @@ suite('PluginMarketplaceService - installed plugins lifecycle', () => { assert.ok(runIdle); runIdle({ didTimeout: false, timeRemaining: () => 50 }); await timeout(0); + await timeout(0); assert.deepStrictEqual(fetched, [deferredRef.canonicalId]); await configurationService.setUserConfiguration(ChatConfiguration.StrictMarketplaces, [