extensions: add delayed auto-update mode to extensions.autoUpdate (#318974)

* extensions: add delayed auto-update mode to extensions.autoUpdate

Folds a 6-hour delayed auto-update mode into the existing
`extensions.autoUpdate` setting, which is now a string enum
('on' | 'delayed' | 'off') and policy-controllable via the
ExtensionsAutoUpdate policy.

When set to 'delayed', an extension whose newer version was published
less than 6 hours ago is treated as outdated-but-delayed: it is excluded
from auto-update and the activity badge, and surfaces an info status
explaining when it will be auto updated. Manual updates still work.

Includes startup migration from legacy values (true/'all'/
'onlyEnabledExtensions' -> 'on'; false/'none'/'onlySelectedExtensions'
-> 'off') plus defensive read-time normalization, and unit tests for the
new logic.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* update policy file

* extensions: address code review feedback for delayed auto-update

- Type IExtensionsConfiguration.autoUpdate as AutoUpdateConfigurationValue
- Keep ExtensionStatusAction.update() returning void; add recomputeStatus()
  for callers that need to await the computed status
- Preserve the higher-priority warning CSS class when the delayed info status
  is also applied to an outdated extension
- Hover awaits recomputeStatus() instead of update()
- Update pluginAutoUpdate doc comment to the on/delayed/off enum values

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* update policies data

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Sandeep Somavarapu
2026-05-29 18:37:38 +02:00
committed by GitHub
parent 52cc23534f
commit da03e4ef04
12 changed files with 330 additions and 80 deletions
+34
View File
@@ -312,6 +312,40 @@
"default": true,
"included": true
},
{
"key": "extensions.autoUpdate",
"name": "ExtensionsAutoUpdate",
"category": "Extensions",
"minimumVersion": "1.122",
"localization": {
"description": {
"key": "extensions.autoUpdate",
"value": "Controls the automatic update behavior of extensions. The updates are fetched from a Microsoft online service."
},
"enumDescriptions": [
{
"key": "extensions.autoUpdate.on",
"value": "Enabled extensions are updated automatically."
},
{
"key": "extensions.autoUpdate.delayed",
"value": "Enabled extensions are updated automatically, but only 6 hours after a new version has been published."
},
{
"key": "extensions.autoUpdate.off",
"value": "Extensions are not updated automatically."
}
]
},
"type": "string",
"default": "on",
"enum": [
"on",
"delayed",
"off"
],
"included": true
},
{
"key": "chat.tools.terminal.enableAutoApprove",
"name": "ChatToolsTerminalEnableAutoApprove",
@@ -6,10 +6,9 @@
import { CancellationToken } from '../../../../base/common/cancellation.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { autorun } from '../../../../base/common/observable.js';
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
import { ILogService } from '../../../../platform/log/common/log.js';
import { IWorkbenchContribution } from '../../../common/contributions.js';
import { AutoUpdateConfigurationKey, AutoUpdateConfigurationValue } from '../../extensions/common/extensions.js';
import { IExtensionsWorkbenchService } from '../../extensions/common/extensions.js';
import { IPluginInstallService } from '../common/plugins/pluginInstallService.js';
import { IPluginMarketplaceService } from '../common/plugins/pluginMarketplaceService.js';
@@ -23,11 +22,10 @@ import { IPluginMarketplaceService } from '../common/plugins/pluginMarketplaceSe
* Without this contribution, that signal was never consumed and plugins
* were never auto-updated (see microsoft/vscode#308563).
*
* When the signal becomes `true` and `extensions.autoUpdate === true`, we
* silently update all installed plugins. Other auto-update modes
* (`'onlyEnabledExtensions'`, `'onlySelectedExtensions'`) gate updates on a
* per-extension opt-in that has no plugin equivalent, so they are treated
* the same as `false` for plugins.
* When the signal becomes `true` and `extensions.autoUpdate` is set to update
* extensions (`'on'` or `'delayed'`), we silently update all installed plugins.
* The `'off'` mode gates updates on a per-extension opt-in that has no plugin
* equivalent, so it is treated the same as disabled for plugins.
*
* The flag is cleared after every attempt — including failures — so the
* next periodic check's `false → true` transition can always re-trigger the
@@ -43,7 +41,7 @@ export class PluginAutoUpdate extends Disposable implements IWorkbenchContributi
constructor(
@IPluginMarketplaceService private readonly _pluginMarketplaceService: IPluginMarketplaceService,
@IPluginInstallService private readonly _pluginInstallService: IPluginInstallService,
@IConfigurationService private readonly _configurationService: IConfigurationService,
@IExtensionsWorkbenchService private readonly _extensionsWorkbenchService: IExtensionsWorkbenchService,
@ILogService private readonly _logService: ILogService,
) {
super();
@@ -61,8 +59,8 @@ export class PluginAutoUpdate extends Disposable implements IWorkbenchContributi
return;
}
const autoUpdate = this._configurationService.getValue<AutoUpdateConfigurationValue>(AutoUpdateConfigurationKey);
if (autoUpdate !== true) {
const autoUpdate = this._extensionsWorkbenchService.getAutoUpdateValue();
if (autoUpdate === 'off') {
return;
}
@@ -22,7 +22,7 @@ import { ObservableMemento, observableMemento } from '../../../../../platform/ob
import { asJson, IRequestService } from '../../../../../platform/request/common/request.js';
import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js';
import type { Dto } from '../../../../services/extensions/common/proxyIdentifier.js';
import { AutoUpdateConfigurationKey, AutoUpdateConfigurationValue } from '../../../extensions/common/extensions.js';
import { AutoUpdateConfigurationKey, IExtensionsWorkbenchService } from '../../../extensions/common/extensions.js';
import { ChatConfiguration } from '../constants.js';
import { IAgentPluginRepositoryService } from './agentPluginRepositoryService.js';
import { FileBackedInstalledPluginsStore, IStoredInstalledPlugin } from './fileBackedInstalledPluginsStore.js';
@@ -304,6 +304,7 @@ export class PluginMarketplaceService extends Disposable implements IPluginMarke
@IStorageService private readonly _storageService: IStorageService,
@IWorkspacePluginSettingsService private readonly _workspacePluginSettingsService: IWorkspacePluginSettingsService,
@IWorkspaceTrustManagementService private readonly _workspaceTrustService: IWorkspaceTrustManagementService,
@IExtensionsWorkbenchService private readonly _extensionsWorkbenchService: IExtensionsWorkbenchService,
) {
super();
@@ -723,8 +724,8 @@ export class PluginMarketplaceService extends Disposable implements IPluginMarke
// --- Periodic update check ------------------------------------------------
private _isAutoUpdateEnabled(): AutoUpdateConfigurationValue {
return this._configurationService.getValue<AutoUpdateConfigurationValue>(AutoUpdateConfigurationKey);
private _isAutoUpdateEnabled(): boolean {
return this._extensionsWorkbenchService.getAutoUpdateValue() !== 'off';
}
/**
@@ -7,11 +7,9 @@ import assert from 'assert';
import { CancellationToken } from '../../../../../../base/common/cancellation.js';
import { observableValue } from '../../../../../../base/common/observable.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js';
import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js';
import { TestConfigurationService } from '../../../../../../platform/configuration/test/common/testConfigurationService.js';
import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
import { ILogService, NullLogService } from '../../../../../../platform/log/common/log.js';
import { AutoUpdateConfigurationKey } from '../../../../extensions/common/extensions.js';
import { AutoUpdateConfigurationValue, IExtensionsWorkbenchService } from '../../../../extensions/common/extensions.js';
import { PluginAutoUpdate } from '../../../browser/pluginAutoUpdate.js';
import { IPluginInstallService, IUpdateAllPluginsOptions, IUpdateAllPluginsResult } from '../../../common/plugins/pluginInstallService.js';
import { IPluginMarketplaceService } from '../../../common/plugins/pluginMarketplaceService.js';
@@ -26,7 +24,7 @@ suite('PluginAutoUpdate', () => {
clearUpdatesAvailableCalls: number;
}
function createContribution(autoUpdate: unknown, stateOverrides?: Partial<MockState>): { contribution: PluginAutoUpdate; state: MockState } {
function createContribution(autoUpdate: AutoUpdateConfigurationValue, stateOverrides?: Partial<MockState>): { contribution: PluginAutoUpdate; state: MockState } {
const instantiationService = store.add(new TestInstantiationService());
const state: MockState = {
@@ -52,9 +50,9 @@ suite('PluginAutoUpdate', () => {
},
} as Partial<IPluginInstallService> as IPluginInstallService);
const configService = new TestConfigurationService();
configService.setUserConfiguration(AutoUpdateConfigurationKey, autoUpdate);
instantiationService.stub(IConfigurationService, configService);
instantiationService.stub(IExtensionsWorkbenchService, {
getAutoUpdateValue: () => autoUpdate,
} as Partial<IExtensionsWorkbenchService> as IExtensionsWorkbenchService);
instantiationService.stub(ILogService, new NullLogService());
@@ -67,14 +65,14 @@ suite('PluginAutoUpdate', () => {
return new Promise(resolve => queueMicrotask(resolve));
}
test('does not trigger update on construction when flag is false', async () => {
const { state } = createContribution(true);
test('does not trigger update on construction', async () => {
const { state } = createContribution('on');
await flushMicrotasks();
assert.deepStrictEqual(state.updateAllCalls, []);
});
test('triggers silent updateAllPlugins when hasUpdatesAvailable becomes true', async () => {
const { state } = createContribution(true);
const { state } = createContribution('on');
state.hasUpdatesAvailable.set(true, undefined);
await flushMicrotasks();
@@ -82,8 +80,8 @@ suite('PluginAutoUpdate', () => {
assert.deepStrictEqual(state.updateAllCalls, [{ silent: true }]);
});
test('does not trigger update when extensions.autoUpdate is false', async () => {
const { state } = createContribution(false);
test('does not trigger update when extensions.autoUpdate is off', async () => {
const { state } = createContribution('off');
state.hasUpdatesAvailable.set(true, undefined);
await flushMicrotasks();
@@ -91,17 +89,13 @@ suite('PluginAutoUpdate', () => {
assert.deepStrictEqual(state.updateAllCalls, []);
});
test('does not trigger update for non-true auto-update values like onlyEnabledExtensions', async () => {
// Plugins have no per-item opt-in equivalent to extensions, so only
// `true` (update everything) opts plugins into auto-update.
for (const value of ['onlyEnabledExtensions', 'onlySelectedExtensions']) {
const { state } = createContribution(value);
test('triggers update for the delayed auto-update mode', async () => {
const { state } = createContribution('delayed');
state.hasUpdatesAvailable.set(true, undefined);
await flushMicrotasks();
state.hasUpdatesAvailable.set(true, undefined);
await flushMicrotasks();
assert.deepStrictEqual(state.updateAllCalls, [], `expected no update for autoUpdate=${value}`);
}
assert.deepStrictEqual(state.updateAllCalls, [{ silent: true }]);
});
test('does not run a second update concurrently with one in flight', async () => {
@@ -109,7 +103,7 @@ suite('PluginAutoUpdate', () => {
const pendingUpdate = new Promise<IUpdateAllPluginsResult>(resolve => {
resolveUpdate = () => resolve({ updatedNames: [], failedNames: [] });
});
const { state } = createContribution(true, {
const { state } = createContribution('on', {
updateAllImpl: () => pendingUpdate,
});
@@ -129,7 +123,7 @@ suite('PluginAutoUpdate', () => {
});
test('continues running on subsequent cycles after the previous update finished', async () => {
const { state } = createContribution(true);
const { state } = createContribution('on');
state.hasUpdatesAvailable.set(true, undefined);
await flushMicrotasks();
@@ -146,7 +140,7 @@ suite('PluginAutoUpdate', () => {
});
test('swallows errors from updateAllPlugins', async () => {
const { state } = createContribution(true, {
const { state } = createContribution('on', {
updateAllImpl: async () => { throw new Error('boom'); },
});
@@ -169,7 +163,7 @@ suite('PluginAutoUpdate', () => {
// path in `PluginInstallService.updateAllPlugins`). Without our own
// clear in `finally`, the observable would stay stuck at `true` and
// the next periodic check's `set(true)` would not notify subscribers.
const { state } = createContribution(true, {
const { state } = createContribution('on', {
updateAllImpl: async () => ({ updatedNames: [], failedNames: ['plugin-a'] }),
});
@@ -190,7 +184,7 @@ suite('PluginAutoUpdate', () => {
});
test('clears the flag even when updateAllPlugins throws', async () => {
const { state } = createContribution(true, {
const { state } = createContribution('on', {
updateAllImpl: async () => { throw new Error('boom'); },
});
@@ -20,6 +20,7 @@ import { IRequestService } from '../../../../../../platform/request/common/reque
import { IStorageService, InMemoryStorageService, StorageScope, StorageTarget } from '../../../../../../platform/storage/common/storage.js';
import { IWorkspaceTrustManagementService } from '../../../../../../platform/workspace/common/workspaceTrust.js';
import { IEnvironmentService } from '../../../../../../platform/environment/common/environment.js';
import { IExtensionsWorkbenchService } from '../../../../extensions/common/extensions.js';
import { ChatConfiguration } from '../../../common/constants.js';
import { IAgentPluginRepositoryService } from '../../../common/plugins/agentPluginRepositoryService.js';
import { IMarketplacePlugin, IMarketplaceReference, IPluginSourceDescriptor, MarketplaceReferenceKind, MarketplaceType, PluginMarketplaceService, PluginSourceKind, getPluginSourceLabel, parseMarketplaceReference, parseMarketplaceReferences, parsePluginSource } from '../../../common/plugins/pluginMarketplaceService.js';
@@ -257,6 +258,9 @@ suite('PluginMarketplaceService - GitHub marketplace refs', () => {
isWorkspaceTrusted: () => true,
onDidChangeTrust: Event.None,
} as Partial<IWorkspaceTrustManagementService> as IWorkspaceTrustManagementService);
instantiationService.stub(IExtensionsWorkbenchService, {
getAutoUpdateValue: () => 'on',
} as Partial<IExtensionsWorkbenchService> as IExtensionsWorkbenchService);
const service = store.add(instantiationService.createInstance(PluginMarketplaceService));
await service.fetchMarketplacePlugins(CancellationToken.None);
@@ -293,6 +297,9 @@ suite('PluginMarketplaceService - getMarketplacePluginMetadata', () => {
isWorkspaceTrusted: () => true,
onDidChangeTrust: Event.None,
} as Partial<IWorkspaceTrustManagementService> as IWorkspaceTrustManagementService);
instantiationService.stub(IExtensionsWorkbenchService, {
getAutoUpdateValue: () => 'on',
} as Partial<IExtensionsWorkbenchService> as IExtensionsWorkbenchService);
return store.add(instantiationService.createInstance(PluginMarketplaceService));
}
@@ -369,6 +376,9 @@ suite('PluginMarketplaceService - installed plugins lifecycle', () => {
isWorkspaceTrusted: () => true,
onDidChangeTrust: Event.None,
} as Partial<IWorkspaceTrustManagementService> as IWorkspaceTrustManagementService);
instantiationService.stub(IExtensionsWorkbenchService, {
getAutoUpdateValue: () => 'on',
} as Partial<IExtensionsWorkbenchService> as IExtensionsWorkbenchService);
return store.add(instantiationService.createInstance(PluginMarketplaceService));
}
@@ -586,6 +596,9 @@ suite('PluginMarketplaceService - hydration after restart', () => {
isWorkspaceTrusted: () => true,
onDidChangeTrust: Event.None,
} as Partial<IWorkspaceTrustManagementService> as IWorkspaceTrustManagementService);
instantiationService.stub(IExtensionsWorkbenchService, {
getAutoUpdateValue: () => 'on',
} as Partial<IExtensionsWorkbenchService> as IExtensionsWorkbenchService);
const service = store.add(instantiationService.createInstance(PluginMarketplaceService));
@@ -636,6 +649,9 @@ suite('PluginMarketplaceService - hydration after restart', () => {
isWorkspaceTrusted: () => true,
onDidChangeTrust: Event.None,
} as Partial<IWorkspaceTrustManagementService> as IWorkspaceTrustManagementService);
instantiationService.stub(IExtensionsWorkbenchService, {
getAutoUpdateValue: () => 'on',
} as Partial<IExtensionsWorkbenchService> as IExtensionsWorkbenchService);
return store.add(instantiationService.createInstance(PluginMarketplaceService));
}
@@ -23,7 +23,8 @@ import { Action2, IAction2Options, IMenuItem, MenuId, MenuRegistry, registerActi
import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js';
import { CommandsRegistry, ICommandService } from '../../../../platform/commands/common/commands.js';
import { Extensions as ConfigurationExtensions, ConfigurationScope, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js';
import { ContextKeyExpr, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';
import { IDialogService, IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js';
import { ExtensionGalleryManifestStatus, ExtensionGalleryResourceType, ExtensionGalleryServiceUrlConfigKey, getExtensionGalleryManifestResourceUri, IExtensionGalleryManifest, IExtensionGalleryManifestService } from '../../../../platform/extensionManagement/common/extensionGalleryManifest.js';
import { EXTENSION_INSTALL_SOURCE_CONTEXT, ExtensionInstallSource, ExtensionRequestsTimeoutConfigKey, ExtensionsLocalizedLabel, FilterType, IExtensionGalleryService, IExtensionManagementService, PreferencesLocalizedLabel, SortBy, VerifyExtensionSignatureConfigKey } from '../../../../platform/extensionManagement/common/extensionManagement.js';
@@ -137,21 +138,38 @@ Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration)
type: 'object',
properties: {
'extensions.autoUpdate': {
enum: [true, 'onlyEnabledExtensions', false,],
type: 'string',
enum: ['on', 'delayed', 'off'],
enumItemLabels: [
localize('all', "All Extensions"),
localize('enabled', "Only Enabled Extensions"),
localize('none', "None"),
localize('on', "On"),
localize('delayed', "Delayed"),
localize('off', "Off"),
],
enumDescriptions: [
localize('extensions.autoUpdate.true', 'Download and install updates automatically for all extensions.'),
localize('extensions.autoUpdate.enabled', 'Download and install updates automatically only for enabled extensions.'),
localize('extensions.autoUpdate.false', 'Extensions are not automatically updated.'),
localize('extensions.autoUpdate.on', 'Enabled extensions are updated automatically.'),
localize('extensions.autoUpdate.delayed', 'Enabled extensions are updated automatically, but only 6 hours after a new version has been published.'),
localize('extensions.autoUpdate.off', 'Extensions are not updated automatically.'),
],
description: localize('extensions.autoUpdate', "Controls the automatic update behavior of extensions. The updates are fetched from a Microsoft online service."),
default: true,
default: 'on',
scope: ConfigurationScope.APPLICATION,
tags: ['usesOnlineServices']
tags: ['usesOnlineServices'],
policy: {
name: 'ExtensionsAutoUpdate',
category: PolicyCategory.Extensions,
minimumVersion: '1.122',
localization: {
description: {
key: 'extensions.autoUpdate',
value: localize('extensions.autoUpdate', "Controls the automatic update behavior of extensions. The updates are fetched from a Microsoft online service."),
},
enumDescriptions: [
{ key: 'extensions.autoUpdate.on', value: localize('extensions.autoUpdate.on', 'Enabled extensions are updated automatically.') },
{ key: 'extensions.autoUpdate.delayed', value: localize('extensions.autoUpdate.delayed', 'Enabled extensions are updated automatically, but only 6 hours after a new version has been published.') },
{ key: 'extensions.autoUpdate.off', value: localize('extensions.autoUpdate.off', 'Extensions are not updated automatically.') },
]
}
},
},
'extensions.autoCheckUpdates': {
type: 'boolean',
@@ -547,6 +565,7 @@ const CONTEXT_GALLERY_FILTER_CAPABILITIES = new RawContextKey<string>('galleryFi
const CONTEXT_GALLERY_ALL_PUBLIC_REPOSITORY_SIGNED = new RawContextKey<boolean>('galleryAllPublicRepositorySigned', false);
const CONTEXT_GALLERY_ALL_PRIVATE_REPOSITORY_SIGNED = new RawContextKey<boolean>('galleryAllPrivateRepositorySigned', false);
const CONTEXT_GALLERY_HAS_EXTENSION_LINK = new RawContextKey<boolean>('galleryHasExtensionLink', false);
const CONTEXT_EXTENSIONS_AUTO_UPDATE_POLICY = new RawContextKey<boolean>('extensionsAutoUpdatePolicy', false);
async function runAction(action: IAction): Promise<void> {
try {
@@ -565,6 +584,8 @@ type IExtensionActionOptions = IAction2Options & {
class ExtensionsContributions extends Disposable implements IWorkbenchContribution {
private readonly autoUpdatePolicyContext: IContextKey<boolean>;
constructor(
@IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService,
@IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService,
@@ -578,6 +599,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi
@ICommandService private readonly commandService: ICommandService,
@IProductService private readonly productService: IProductService,
@IPluginInstallService private readonly pluginInstallService: IPluginInstallService,
@IConfigurationService private readonly configurationService: IConfigurationService,
) {
super();
const hasLocalServerContext = CONTEXT_HAS_LOCAL_SERVER.bindTo(contextKeyService);
@@ -597,6 +619,14 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi
this.updateExtensionGalleryStatusContexts();
this._register(extensionGalleryManifestService.onDidChangeExtensionGalleryManifestStatus(() => this.updateExtensionGalleryStatusContexts()));
this.autoUpdatePolicyContext = CONTEXT_EXTENSIONS_AUTO_UPDATE_POLICY.bindTo(contextKeyService);
this.updateAutoUpdatePolicyContext();
this._register(this.configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration(AutoUpdateConfigurationKey)) {
this.updateAutoUpdatePolicyContext();
}
}));
extensionGalleryManifestService.getExtensionGalleryManifest()
.then(extensionGalleryManifest => {
this.updateGalleryCapabilitiesContexts(extensionGalleryManifest);
@@ -607,6 +637,10 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi
this.registerQuickAccessProvider();
}
private updateAutoUpdatePolicyContext(): void {
this.autoUpdatePolicyContext.set(this.configurationService.inspect(AutoUpdateConfigurationKey).policyValue !== undefined);
}
private async updateExtensionGalleryStatusContexts(): Promise<void> {
CONTEXT_HAS_GALLERY.bindTo(this.contextKeyService).set(this.extensionGalleryManifestService.extensionGalleryManifestStatus === ExtensionGalleryManifestStatus.Available);
CONTEXT_EXTENSIONS_GALLERY_STATUS.bindTo(this.contextKeyService).set(this.extensionGalleryManifestService.extensionGalleryManifestStatus);
@@ -733,7 +767,8 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi
}
});
const enableAutoUpdateWhenCondition = ContextKeyExpr.equals(`config.${AutoUpdateConfigurationKey}`, false);
const notPolicyControlledCondition = CONTEXT_EXTENSIONS_AUTO_UPDATE_POLICY.toNegated();
const enableAutoUpdateWhenCondition = ContextKeyExpr.and(ContextKeyExpr.equals(`config.${AutoUpdateConfigurationKey}`, 'off'), notPolicyControlledCondition);
this.registerExtensionAction({
id: 'workbench.extensions.action.enableAutoUpdate',
title: localize2('enableAutoUpdate', 'Enable Auto Update for All Extensions'),
@@ -750,7 +785,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi
run: (accessor: ServicesAccessor) => accessor.get(IExtensionsWorkbenchService).updateAutoUpdateForAllExtensions(true)
});
const disableAutoUpdateWhenCondition = ContextKeyExpr.notEquals(`config.${AutoUpdateConfigurationKey}`, false);
const disableAutoUpdateWhenCondition = ContextKeyExpr.and(ContextKeyExpr.notEquals(`config.${AutoUpdateConfigurationKey}`, 'off'), notPolicyControlledCondition);
this.registerExtensionAction({
id: 'workbench.extensions.action.disableAutoUpdate',
title: localize2('disableAutoUpdate', 'Disable Auto Update for All Extensions'),
@@ -778,7 +813,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi
when: ContextKeyExpr.and(CONTEXT_HAS_GALLERY, ContextKeyExpr.or(CONTEXT_HAS_LOCAL_SERVER, CONTEXT_HAS_REMOTE_SERVER, CONTEXT_HAS_WEB_SERVER))
}, {
id: MenuId.ViewContainerTitle,
when: ContextKeyExpr.and(ContextKeyExpr.equals('viewContainer', VIEWLET_ID), ContextKeyExpr.or(ContextKeyExpr.has(`config.${AutoUpdateConfigurationKey}`).negate(), ContextKeyExpr.equals(`config.${AutoUpdateConfigurationKey}`, 'onlyEnabledExtensions'))),
when: ContextKeyExpr.equals('viewContainer', VIEWLET_ID),
group: '1_updates',
order: 2
}, {
@@ -1445,7 +1480,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi
id: ToggleAutoUpdateForExtensionAction.ID,
title: ToggleAutoUpdateForExtensionAction.LABEL,
category: ExtensionsLocalizedLabel,
precondition: ContextKeyExpr.and(ContextKeyExpr.or(ContextKeyExpr.notEquals(`config.${AutoUpdateConfigurationKey}`, 'onlyEnabledExtensions'), ContextKeyExpr.equals('isExtensionEnabled', true)), ContextKeyExpr.not('extensionDisallowInstall'), ContextKeyExpr.has('isExtensionAllowed')),
precondition: ContextKeyExpr.and(ContextKeyExpr.or(ContextKeyExpr.equals(`config.${AutoUpdateConfigurationKey}`, 'off'), ContextKeyExpr.equals('isExtensionEnabled', true)), ContextKeyExpr.not('extensionDisallowInstall'), ContextKeyExpr.has('isExtensionAllowed')),
menu: {
id: MenuId.ExtensionContext,
group: UPDATE_ACTIONS_GROUP,
@@ -1472,7 +1507,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi
id: ToggleAutoUpdatesForPublisherAction.ID,
title: { value: ToggleAutoUpdatesForPublisherAction.LABEL, original: 'Auto Update (Publisher)' },
category: ExtensionsLocalizedLabel,
precondition: ContextKeyExpr.equals(`config.${AutoUpdateConfigurationKey}`, false),
precondition: ContextKeyExpr.equals(`config.${AutoUpdateConfigurationKey}`, 'off'),
menu: {
id: MenuId.ExtensionContext,
group: UPDATE_ACTIONS_GROUP,
@@ -2101,8 +2136,11 @@ Registry.as<IConfigurationMigrationRegistry>(ConfigurationMigrationExtensions.Co
.registerConfigurationMigrations([{
key: AutoUpdateConfigurationKey,
migrateFn: (value, accessor) => {
if (value === 'onlySelectedExtensions') {
return { value: false };
if (value === true || value === 'all' || value === 'onlyEnabledExtensions') {
return { value: 'on' };
}
if (value === false || value === 'none' || value === 'onlySelectedExtensions') {
return { value: 'off' };
}
return [];
}
@@ -1087,7 +1087,7 @@ export class ToggleAutoUpdateForExtensionAction extends ExtensionAction {
if (extension && this.allowedExtensionsService.isAllowed(extension) !== true) {
return;
}
if (this.extensionsWorkbenchService.getAutoUpdateValue() === 'onlyEnabledExtensions' && !this.extensionEnablementService.isEnabledEnablementState(this.extension.enablementState)) {
if (this.extensionsWorkbenchService.getAutoUpdateValue() !== 'off' && !this.extensionEnablementService.isEnabledEnablementState(this.extension.enablementState)) {
return;
}
this.enabled = true;
@@ -2771,17 +2771,32 @@ export class ExtensionStatusAction extends ExtensionAction {
@IWorkbenchExtensionEnablementService private readonly workbenchExtensionEnablementService: IWorkbenchExtensionEnablementService,
@IExtensionFeaturesManagementService private readonly extensionFeaturesManagementService: IExtensionFeaturesManagementService,
@IExtensionGalleryManifestService private readonly extensionGalleryManifestService: IExtensionGalleryManifestService,
@IConfigurationService private readonly configurationService: IConfigurationService,
) {
super('extensions.status', '', `${ExtensionStatusAction.CLASS} hide`, false);
this._register(this.labelService.onDidChangeFormatters(() => this.update(), this));
this._register(this.extensionService.onDidChangeExtensions(() => this.update()));
this._register(this.extensionFeaturesManagementService.onDidChangeAccessData(() => this.update()));
this._register(allowedExtensionsService.onDidChangeAllowedExtensionsConfigValue(() => this.update()));
this._register(this.configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration(AutoUpdateConfigurationKey)) {
this.update();
}
}));
this.update();
}
update(): void {
this.updateThrottler.queue(() => this.computeAndUpdateStatus());
this.recomputeStatus();
}
/**
* Recomputes the status and returns a promise that resolves when the
* computation is done. Use this when callers need to await time-sensitive
* status content (e.g. the delayed auto-update message) before reading it.
*/
recomputeStatus(): Promise<void> {
return this.updateThrottler.queue(() => this.computeAndUpdateStatus());
}
private async computeAndUpdateStatus(): Promise<void> {
@@ -2829,8 +2844,10 @@ export class ExtensionStatusAction extends ExtensionAction {
}
if (this.extension.outdated) {
let hasConsentWarning = false;
const message = await this.extensionsWorkbenchService.shouldRequireConsentToUpdate(this.extension);
if (message) {
hasConsentWarning = true;
const markdown = new MarkdownString();
markdown.appendMarkdown(`${message} `);
markdown.appendMarkdown(
@@ -2843,6 +2860,11 @@ export class ExtensionStatusAction extends ExtensionAction {
));
this.updateStatus({ icon: warningIcon, message: markdown }, true);
}
if (this.extensionsWorkbenchService.isAutoUpdateDelayed(this.extension)) {
const updateAt = fromNow(Date.now() + this.extensionsWorkbenchService.getAutoUpdateDelayRemaining(this.extension), false, true);
// Do not override the higher-priority warning class with the info class.
this.updateStatus({ icon: infoIcon, message: new MarkdownString(localize('autoUpdateDelayed', "This extension is not updated yet because extension auto updates are configured to be delayed by 6 hours. It will be auto updated {0}.", updateAt)) }, !hasConsentWarning);
}
}
if (this.extension.gallery && this.extension.state === ExtensionState.Uninstalled) {
@@ -1011,7 +1011,7 @@ export class StatusUpdater extends Disposable implements IWorkbenchContribution
if (!badge) {
const actionRequired = this.configurationService.getValue(AutoRestartConfigurationKey) === true ? [] : this.extensionsWorkbenchService.installed.filter(e => e.runtimeState !== undefined);
const outdated = this.extensionsWorkbenchService.outdated.reduce((r, e) => r + (this.extensionEnablementService.isEnabled(e.local!) && !actionRequired.includes(e) ? 1 : 0), 0);
const outdated = this.extensionsWorkbenchService.outdated.reduce((r, e) => r + (this.extensionEnablementService.isEnabled(e.local!) && !actionRequired.includes(e) && !this.extensionsWorkbenchService.isAutoUpdateDelayed(e) ? 1 : 0), 0);
const newBadgeNumber = outdated + actionRequired.length;
if (newBadgeNumber > 0) {
let msg = '';
@@ -825,7 +825,16 @@ export class ExtensionHoverWidget extends ExtensionWidget {
},
this.options.target,
{
markdown: () => Promise.resolve(this.getHoverMarkdown()),
markdown: async () => {
// Recompute the status so any time-sensitive content (e.g. the
// delayed auto-update message) reflects the current time on each hover.
try {
await this.extensionStatusAction.recomputeStatus();
} catch (error) {
// Ignore: fall back to the last computed status.
}
return this.getHoverMarkdown();
},
markdownNotSupportedFallback: undefined
},
{
@@ -7,7 +7,7 @@ import * as nls from '../../../../nls.js';
import * as semver from '../../../../base/common/semver/semver.js';
import { Event, Emitter } from '../../../../base/common/event.js';
import { index } from '../../../../base/common/arrays.js';
import { CancelablePromise, Promises, ThrottledDelayer, createCancelablePromise } from '../../../../base/common/async.js';
import { CancelablePromise, Promises, ThrottledDelayer, createCancelablePromise, disposableTimeout } from '../../../../base/common/async.js';
import { CancellationError, getErrorMessage, isCancellationError } from '../../../../base/common/errors.js';
import { Disposable, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
import { IPager, singlePagePager } from '../../../../base/common/paging.js';
@@ -52,7 +52,7 @@ import { FileAccess } from '../../../../base/common/network.js';
import { IIgnoredExtensionsManagementService } from '../../../../platform/userDataSync/common/ignoredExtensions.js';
import { IUserDataAutoSyncService, IUserDataSyncEnablementService, SyncResource } from '../../../../platform/userDataSync/common/userDataSync.js';
import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
import { isBoolean, isDefined, isString, isUndefined } from '../../../../base/common/types.js';
import { isDefined, isString, isUndefined } from '../../../../base/common/types.js';
import { IExtensionManifestPropertiesService } from '../../../services/extensions/common/extensionManifestPropertiesService.js';
import { IExtensionService, IExtensionsStatus as IExtensionRuntimeStatus, toExtension, toExtensionDescription } from '../../../services/extensions/common/extensions.js';
import { isWeb, language } from '../../../../base/common/platform.js';
@@ -80,6 +80,8 @@ interface IExtensionStateProvider<T> {
(extension: Extension): T;
}
const DELAYED_AUTO_UPDATE_PERIOD = 6 * 60 * 60 * 1000; // 6 hours in milliseconds
interface InstalledExtensionsEvent {
readonly extensionIds: TelemetryTrustedValue<string>;
readonly count: number;
@@ -998,6 +1000,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
private installing: IExtension[] = [];
private tasksInProgress: CancelablePromise<any>[] = [];
private readonly delayedAutoUpdateCheckTimer = this._register(new MutableDisposable());
readonly whenInitialized: Promise<void>;
@@ -1119,9 +1122,15 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
// Register listeners for auto updates
this._register(this.configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration(AutoUpdateConfigurationKey)) {
if (this.getAutoUpdateValue() !== 'delayed') {
// No longer delaying — cancel any pending delayed re-check
this.delayedAutoUpdateCheckTimer.value = undefined;
}
if (this.isAutoUpdateEnabled()) {
this.eventuallyAutoUpdateExtensions();
}
// The auto update value affects whether an extension is shown as delayed
this._onChange.fire(undefined);
}
if (e.affectsConfiguration(AutoCheckUpdatesConfigurationKey)) {
if (this.isAutoCheckUpdatesEnabled()) {
@@ -1130,7 +1139,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
}
}));
this._register(this.extensionEnablementService.onEnablementChanged(platformExtensions => {
if (this.isAutoCheckUpdatesEnabled() && this.getAutoUpdateValue() === 'onlyEnabledExtensions' && platformExtensions.some(e => this.extensionEnablementService.isEnabled(e))) {
if (this.isAutoCheckUpdatesEnabled() && this.getAutoUpdateValue() !== 'off' && platformExtensions.some(e => this.extensionEnablementService.isEnabled(e))) {
this.checkForUpdates('Extension enablement changed');
}
}));
@@ -1188,16 +1197,45 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
if (this.meteredConnectionService.isConnectionMetered) {
return false;
}
return this.getAutoUpdateValue() !== false;
return this.getAutoUpdateValue() !== 'off';
}
getAutoUpdateValue(): AutoUpdateConfigurationValue {
const autoUpdate = this.configurationService.getValue<AutoUpdateConfigurationValue>(AutoUpdateConfigurationKey);
// eslint-disable-next-line local/code-no-any-casts
if (<any>autoUpdate === 'onlySelectedExtensions') {
const autoUpdate = this.configurationService.getValue<AutoUpdateConfigurationValue | boolean | 'all' | 'none' | 'onlyEnabledExtensions' | 'onlySelectedExtensions'>(AutoUpdateConfigurationKey);
// Normalize legacy values
if (autoUpdate === true || autoUpdate === 'on' || autoUpdate === 'all' || autoUpdate === 'onlyEnabledExtensions') {
return 'on';
}
if (autoUpdate === false || autoUpdate === 'off' || autoUpdate === 'none' || autoUpdate === 'onlySelectedExtensions') {
return 'off';
}
if (autoUpdate === 'delayed') {
return 'delayed';
}
return 'on';
}
isAutoUpdateDelayed(extension: IExtension): boolean {
if (!extension.outdated) {
return false;
}
return isBoolean(autoUpdate) || autoUpdate === 'onlyEnabledExtensions' ? autoUpdate : true;
if (this.getAutoUpdateValue() !== 'delayed') {
return false;
}
return this.getAutoUpdateDelayRemaining(extension) > 0;
}
getAutoUpdateDelayRemaining(extension: IExtension): number {
const lastUpdated = extension.gallery?.lastUpdated;
if (!Number.isFinite(lastUpdated) || !lastUpdated) {
return 0;
}
const elapsed = Date.now() - lastUpdated;
if (elapsed < 0) {
// Future timestamp (clock skew) — treat as not delayed
return 0;
}
return Math.max(0, DELAYED_AUTO_UPDATE_PERIOD - elapsed);
}
async updateAutoUpdateForAllExtensions(isAutoUpdateEnabled: boolean): Promise<void> {
@@ -1220,7 +1258,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
// Reset extensions enabled for auto update first to prevent them from being updated
this.setEnabledAutoUpdateExtensions([]);
await this.configurationService.updateValue(AutoUpdateConfigurationKey, isAutoUpdateEnabled);
await this.configurationService.updateValue(AutoUpdateConfigurationKey, isAutoUpdateEnabled ? 'on' : 'off');
this.setDisabledAutoUpdateExtensions([]);
await this.updateExtensionsPinnedState(!isAutoUpdateEnabled);
@@ -2139,7 +2177,11 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
}
private getUpdatesCheckInterval(): number {
if (this.productService.quality === 'insider' && this.getProductUpdateVersion()) {
if (
this.productService.quality === 'insider'
&& this.getProductUpdateVersion()
&& this.getAutoUpdateValue() !== 'delayed'
) {
return 1000 * 60 * 60 * 1; // 1 hour
}
return ExtensionsWorkbenchService.UpdatesCheckInterval;
@@ -2183,11 +2225,21 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
const toUpdate: IExtension[] = [];
const disabledAutoUpdate = [];
const consentRequired = [];
let soonestDelayRemaining = Number.MAX_SAFE_INTEGER;
const isDelayed = this.getAutoUpdateValue() === 'delayed';
for (const extension of this.outdated) {
if (!this.shouldAutoUpdateExtension(extension)) {
disabledAutoUpdate.push(extension.identifier.id);
continue;
}
if (isDelayed && !extension.local?.forceAutoUpdate) {
const delayRemaining = this.getAutoUpdateDelayRemaining(extension);
if (delayRemaining > 0) {
this.logService.trace('Auto update delayed for extension', extension.identifier.id);
soonestDelayRemaining = Math.min(soonestDelayRemaining, delayRemaining);
continue;
}
}
if (await this.shouldRequireConsentToUpdate(extension)) {
consentRequired.push(extension.identifier.id);
continue;
@@ -2195,6 +2247,12 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
toUpdate.push(extension);
}
if (soonestDelayRemaining < Number.MAX_SAFE_INTEGER) {
this.delayedAutoUpdateCheckTimer.value = disposableTimeout(() => this.eventuallyCheckForUpdates(true), soonestDelayRemaining);
} else {
this.delayedAutoUpdateCheckTimer.value = undefined;
}
if (disabledAutoUpdate.length) {
this.logService.trace('Auto update disabled for extensions', disabledAutoUpdate.join(', '));
}
@@ -2246,7 +2304,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
const autoUpdateValue = this.getAutoUpdateValue();
if (autoUpdateValue === false) {
if (autoUpdateValue === 'off') {
const extensionsToAutoUpdate = this.getEnabledAutoUpdateExtensions();
const extensionId = extension.identifier.id.toLowerCase();
if (extensionsToAutoUpdate.includes(extensionId)) {
@@ -2267,11 +2325,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
return false;
}
if (autoUpdateValue === true) {
return true;
}
if (autoUpdateValue === 'onlyEnabledExtensions') {
if (autoUpdateValue === 'on' || autoUpdateValue === 'delayed') {
return extension.enablementState !== EnablementState.DisabledGlobally && extension.enablementState !== EnablementState.DisabledWorkspace;
}
@@ -161,6 +161,8 @@ export interface IExtensionsWorkbenchService {
open(extension: IExtension | string, options?: IExtensionEditorOptions): Promise<void>;
openSearch(searchValue: string, focus?: boolean): Promise<void>;
getAutoUpdateValue(): AutoUpdateConfigurationValue;
isAutoUpdateDelayed(extension: IExtension): boolean;
getAutoUpdateDelayRemaining(extension: IExtension): number;
checkForUpdates(): Promise<void>;
getExtensionRuntimeStatus(extension: IExtension): IExtensionRuntimeStatus | undefined;
updateAll(): Promise<InstallExtensionResult[]>;
@@ -189,10 +191,10 @@ export const AutoCheckUpdatesConfigurationKey = 'extensions.autoCheckUpdates';
export const CloseExtensionDetailsOnViewChangeKey = 'extensions.closeExtensionDetailsOnViewChange';
export const AutoRestartConfigurationKey = 'extensions.autoRestart';
export type AutoUpdateConfigurationValue = boolean | 'onlyEnabledExtensions' | 'onlySelectedExtensions';
export type AutoUpdateConfigurationValue = 'on' | 'delayed' | 'off';
export interface IExtensionsConfiguration {
autoUpdate: boolean;
autoUpdate: AutoUpdateConfigurationValue;
autoCheckUpdates: boolean;
ignoreRecommendations: boolean;
closeExtensionDetailsOnViewChange: boolean;
@@ -424,6 +424,76 @@ suite('ExtensionsWorkbenchServiceTest', () => {
assert.ok(!testObject.local[0].outdated);
});
test('test isAutoUpdateDelayed returns false when auto update is not set to delayed', async () => {
stubConfiguration('on', true);
testObject = await anOutdatedExtensionWorkbenchService(Date.now());
assert.strictEqual(testObject.local[0].outdated, true);
assert.strictEqual(testObject.isAutoUpdateDelayed(testObject.local[0]), false);
});
test('test isAutoUpdateDelayed returns true for recently published version when auto update is set to delayed', async () => {
stubConfiguration('delayed', true);
testObject = await anOutdatedExtensionWorkbenchService(Date.now() - (1000 * 60 * 60) /* 1 hour ago */);
assert.strictEqual(testObject.local[0].outdated, true);
assert.strictEqual(testObject.isAutoUpdateDelayed(testObject.local[0]), true);
});
test('test isAutoUpdateDelayed returns false for version published more than 6 hours ago', async () => {
stubConfiguration('delayed', true);
testObject = await anOutdatedExtensionWorkbenchService(Date.now() - (1000 * 60 * 60 * 7) /* 7 hours ago */);
assert.strictEqual(testObject.local[0].outdated, true);
assert.strictEqual(testObject.isAutoUpdateDelayed(testObject.local[0]), false);
});
test('test isAutoUpdateDelayed returns false for version with a future published timestamp', async () => {
stubConfiguration('delayed', true);
testObject = await anOutdatedExtensionWorkbenchService(Date.now() + (1000 * 60 * 60) /* 1 hour in the future */);
assert.strictEqual(testObject.local[0].outdated, true);
assert.strictEqual(testObject.isAutoUpdateDelayed(testObject.local[0]), false);
});
test('test getAutoUpdateValue normalizes configured and legacy values', async () => {
const result: Record<string, string> = {};
for (const value of [true, false, 'on', 'off', 'all', 'onlyEnabledExtensions', 'onlySelectedExtensions', 'delayed', 'none', 'unknown']) {
stubConfiguration(value);
testObject = await aWorkbenchService();
result[String(value)] = testObject.getAutoUpdateValue();
}
assert.deepStrictEqual(result, {
'true': 'on',
'false': 'off',
'on': 'on',
'off': 'off',
'all': 'on',
'onlyEnabledExtensions': 'on',
'onlySelectedExtensions': 'off',
'delayed': 'delayed',
'none': 'off',
'unknown': 'on',
});
});
test('test getAutoUpdateDelayRemaining returns remaining time within the delay window', async () => {
stubConfiguration('delayed', true);
testObject = await anOutdatedExtensionWorkbenchService(Date.now() - (1000 * 60 * 60 * 2) /* 2 hours ago */);
const remaining = testObject.getAutoUpdateDelayRemaining(testObject.local[0]);
// Published 2 hours ago, so ~4 hours of the 6 hour window remain.
assert.ok(remaining > (1000 * 60 * 60 * 3) && remaining <= (1000 * 60 * 60 * 4), `unexpected remaining time ${remaining}`);
});
test('test getAutoUpdateDelayRemaining returns 0 when the published timestamp is missing', async () => {
stubConfiguration('delayed', true);
testObject = await anOutdatedExtensionWorkbenchService(undefined);
assert.strictEqual(testObject.getAutoUpdateDelayRemaining(testObject.local[0]), 0);
});
test('test canInstall returns false for extensions with out gallery', async () => {
const local = aLocalExtension('a', { version: '1.0.1' }, { type: ExtensionType.System });
instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]);
@@ -1516,7 +1586,7 @@ suite('ExtensionsWorkbenchServiceTest', () => {
});
test('Test disable autoupdate for extension when auto update is enabled for enabled extensions', async () => {
stubConfiguration('onlyEnabledExtensions');
stubConfiguration('on');
const extension1 = aLocalExtension('a');
const extension2 = aLocalExtension('b');
@@ -1695,9 +1765,21 @@ suite('ExtensionsWorkbenchServiceTest', () => {
return workbenchService;
}
async function anOutdatedExtensionWorkbenchService(lastUpdated: number | undefined): Promise<ExtensionsWorkbenchService> {
const local = aLocalExtension('a', { version: '1.0.1' });
const gallery = aGalleryExtension(local.manifest.name, { identifier: local.identifier, version: '1.0.2', lastUpdated });
instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]);
instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(gallery));
instantiationService.stubPromise(IExtensionGalleryService, 'getCompatibleExtension', gallery);
instantiationService.stubPromise(IExtensionGalleryService, 'getExtensions', [gallery]);
const workbenchService = await aWorkbenchService();
await workbenchService.checkForUpdates();
return workbenchService;
}
function stubConfiguration(autoUpdateValue?: any, autoCheckUpdatesValue?: any): void {
const values: any = {
[AutoUpdateConfigurationKey]: autoUpdateValue ?? true,
[AutoUpdateConfigurationKey]: autoUpdateValue ?? 'on',
[AutoCheckUpdatesConfigurationKey]: autoCheckUpdatesValue ?? true
};
const emitter = disposableStore.add(new Emitter<IConfigurationChangeEvent>());