Re-request consent if necessary when updating extensions (#9517)

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
This commit is contained in:
Jacob MacDonald
2025-09-25 10:57:59 -07:00
committed by GitHub
parent e209724789
commit a0c8e3bf2b
14 changed files with 279 additions and 74 deletions
+106 -38
View File
@@ -16,8 +16,10 @@ import {
enableExtension,
installExtension,
loadExtension,
loadExtensionConfig,
loadExtensions,
performWorkspaceExtensionMigration,
requestConsentNonInteractive,
uninstallExtension,
type Extension,
} from './extension.js';
@@ -258,10 +260,13 @@ describe('extension tests', () => {
});
fs.writeFileSync(path.join(sourceExtDir, 'context.md'), 'linked context');
const extensionName = await installExtension({
source: sourceExtDir,
type: 'link',
});
const extensionName = await installExtension(
{
source: sourceExtDir,
type: 'link',
},
async (_) => true,
);
expect(extensionName).toEqual('my-linked-extension');
const extensions = loadExtensions();
@@ -627,7 +632,10 @@ describe('extension tests', () => {
const targetExtDir = path.join(userExtensionsDir, 'my-local-extension');
const metadataPath = path.join(targetExtDir, INSTALL_METADATA_FILENAME);
await installExtension({ source: sourceExtDir, type: 'local' });
await installExtension(
{ source: sourceExtDir, type: 'local' },
async (_) => true,
);
expect(fs.existsSync(targetExtDir)).toBe(true);
expect(fs.existsSync(metadataPath)).toBe(true);
@@ -645,9 +653,15 @@ describe('extension tests', () => {
name: 'my-local-extension',
version: '1.0.0',
});
await installExtension({ source: sourceExtDir, type: 'local' });
await installExtension(
{ source: sourceExtDir, type: 'local' },
async (_) => true,
);
await expect(
installExtension({ source: sourceExtDir, type: 'local' }),
installExtension(
{ source: sourceExtDir, type: 'local' },
async (_) => true,
),
).rejects.toThrow(
'Extension "my-local-extension" is already installed. Please uninstall it first.',
);
@@ -659,7 +673,10 @@ describe('extension tests', () => {
const configPath = path.join(sourceExtDir, EXTENSIONS_CONFIG_FILENAME);
await expect(
installExtension({ source: sourceExtDir, type: 'local' }),
installExtension(
{ source: sourceExtDir, type: 'local' },
async (_) => true,
),
).rejects.toThrow(`Configuration file not found at ${configPath}`);
const targetExtDir = path.join(userExtensionsDir, 'bad-extension');
@@ -673,7 +690,10 @@ describe('extension tests', () => {
fs.writeFileSync(configPath, '{ "name": "bad-json", "version": "1.0.0"'); // Malformed JSON
await expect(
installExtension({ source: sourceExtDir, type: 'local' }),
installExtension(
{ source: sourceExtDir, type: 'local' },
async (_) => true,
),
).rejects.toThrow(
new RegExp(
`^Failed to load extension config from ${configPath.replace(
@@ -695,7 +715,10 @@ describe('extension tests', () => {
fs.writeFileSync(configPath, JSON.stringify({ version: '1.0.0' }));
await expect(
installExtension({ source: sourceExtDir, type: 'local' }),
installExtension(
{ source: sourceExtDir, type: 'local' },
async (_) => true,
),
).rejects.toThrow(
`Invalid configuration in ${configPath}: missing "name"`,
);
@@ -718,7 +741,10 @@ describe('extension tests', () => {
});
mockGit.getRemotes.mockResolvedValue([{ name: 'origin' }]);
await installExtension({ source: gitUrl, type: 'git' });
await installExtension(
{ source: gitUrl, type: 'git' },
async (_) => true,
);
expect(fs.existsSync(targetExtDir)).toBe(true);
expect(fs.existsSync(metadataPath)).toBe(true);
@@ -740,7 +766,10 @@ describe('extension tests', () => {
const metadataPath = path.join(targetExtDir, INSTALL_METADATA_FILENAME);
const configPath = path.join(targetExtDir, EXTENSIONS_CONFIG_FILENAME);
await installExtension({ source: sourceExtDir, type: 'link' });
await installExtension(
{ source: sourceExtDir, type: 'link' },
async (_) => true,
);
expect(fs.existsSync(targetExtDir)).toBe(true);
expect(fs.existsSync(metadataPath)).toBe(true);
@@ -762,7 +791,10 @@ describe('extension tests', () => {
version: '1.0.0',
});
await installExtension({ source: sourceExtDir, type: 'local' });
await installExtension(
{ source: sourceExtDir, type: 'local' },
async (_) => true,
);
expect(mockLogExtensionInstallEvent).toHaveBeenCalled();
});
@@ -789,7 +821,10 @@ describe('extension tests', () => {
mockQuestion.mockImplementation((_query, callback) => callback('y'));
await expect(
installExtension({ source: sourceExtDir, type: 'local' }, true),
installExtension(
{ source: sourceExtDir, type: 'local' },
requestConsentNonInteractive,
),
).resolves.toBe('my-local-extension');
expect(consoleInfoSpy).toHaveBeenCalledWith(
@@ -817,7 +852,10 @@ This extension will run the following MCP servers:
mockQuestion.mockImplementation((_query, callback) => callback('y'));
await expect(
installExtension({ source: sourceExtDir, type: 'local' }, true),
installExtension(
{ source: sourceExtDir, type: 'local' },
requestConsentNonInteractive,
),
).resolves.toBe('my-local-extension');
expect(mockQuestion).toHaveBeenCalledWith(
@@ -842,8 +880,11 @@ This extension will run the following MCP servers:
mockQuestion.mockImplementation((_query, callback) => callback('n'));
await expect(
installExtension({ source: sourceExtDir, type: 'local' }, true),
).rejects.toThrow('Installation cancelled by user.');
installExtension(
{ source: sourceExtDir, type: 'local' },
requestConsentNonInteractive,
),
).rejects.toThrow('Installation cancelled.');
expect(mockQuestion).toHaveBeenCalledWith(
expect.stringContaining('Do you want to continue? [Y/n]: '),
@@ -860,11 +901,14 @@ This extension will run the following MCP servers:
const targetExtDir = path.join(userExtensionsDir, 'my-local-extension');
const metadataPath = path.join(targetExtDir, INSTALL_METADATA_FILENAME);
await installExtension({
source: sourceExtDir,
type: 'local',
autoUpdate: true,
});
await installExtension(
{
source: sourceExtDir,
type: 'local',
autoUpdate: true,
},
async (_) => true,
);
expect(fs.existsSync(targetExtDir)).toBe(true);
expect(fs.existsSync(metadataPath)).toBe(true);
@@ -890,9 +934,22 @@ This extension will run the following MCP servers:
},
});
const mockRequestConsent = vi.fn();
await expect(
installExtension({ source: sourceExtDir, type: 'local' }, false),
installExtension(
{ source: sourceExtDir, type: 'local' },
mockRequestConsent,
process.cwd(),
// Provide its own existing config as the previous config.
await loadExtensionConfig({
extensionDir: sourceExtDir,
workspaceDir: process.cwd(),
}),
),
).resolves.toBe('my-local-extension');
expect(mockRequestConsent).not.toHaveBeenCalled();
});
});
@@ -1028,12 +1085,15 @@ This extension will run the following MCP servers:
version: '1.0.0',
});
await performWorkspaceExtensionMigration([
loadExtension({
extensionDir: ext1Path,
workspaceDir: tempWorkspaceDir,
})!,
]);
await performWorkspaceExtensionMigration(
[
loadExtension({
extensionDir: ext1Path,
workspaceDir: tempWorkspaceDir,
})!,
],
async (_) => true,
);
const userExtensionsDir = path.join(
tempHomeDir,
@@ -1051,12 +1111,15 @@ This extension will run the following MCP servers:
version: '1.0.0',
});
await performWorkspaceExtensionMigration([
loadExtension({
extensionDir: ext1Path,
workspaceDir: tempWorkspaceDir,
})!,
]);
await performWorkspaceExtensionMigration(
[
loadExtension({
extensionDir: ext1Path,
workspaceDir: tempWorkspaceDir,
})!,
],
async (_) => true,
);
const extensions = loadExtensions();
expect(extensions).toEqual([]);
@@ -1084,8 +1147,10 @@ This extension will run the following MCP servers:
workspaceDir: tempWorkspaceDir,
})!,
];
const failed =
await performWorkspaceExtensionMigration(extensionsToMigrate);
const failed = await performWorkspaceExtensionMigration(
extensionsToMigrate,
async (_) => true,
);
expect(failed).toEqual([]);
@@ -1126,7 +1191,10 @@ This extension will run the following MCP servers:
},
];
const failed = await performWorkspaceExtensionMigration(extensions);
const failed = await performWorkspaceExtensionMigration(
extensions,
async (_) => true,
);
expect(failed).toEqual(['ext2']);
});
});