diff --git a/src/vs/workbench/contrib/chat/browser/pluginAutoUpdate.ts b/src/vs/workbench/contrib/chat/browser/pluginAutoUpdate.ts index 712cb3c1d84954..a1c6e97a4244c4 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 ac3f0376b56078..cfd0b8d1515934 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 663ae6ba2af01e..bda92afd3df700 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 c3d08a30792784..432d075aa8614b 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, [