Skip to content

Commit fc03891

Browse files
authored
fix(extensions): revert broken extension removal behavior (#23317)
1 parent 974d291 commit fc03891

File tree

3 files changed

+22
-81
lines changed

3 files changed

+22
-81
lines changed

packages/cli/src/config/extension-manager.test.ts

Lines changed: 0 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -637,64 +637,4 @@ describe('ExtensionManager', () => {
637637
);
638638
});
639639
});
640-
641-
describe('orphaned extension cleanup', () => {
642-
it('should remove broken extension metadata on startup to allow re-installation', async () => {
643-
const extName = 'orphaned-ext';
644-
const sourceDir = path.join(tempHomeDir, 'valid-source');
645-
fs.mkdirSync(sourceDir, { recursive: true });
646-
fs.writeFileSync(
647-
path.join(sourceDir, 'gemini-extension.json'),
648-
JSON.stringify({ name: extName, version: '1.0.0' }),
649-
);
650-
651-
// Link an extension successfully.
652-
await extensionManager.loadExtensions();
653-
await extensionManager.installOrUpdateExtension({
654-
source: sourceDir,
655-
type: 'link',
656-
});
657-
658-
const destinationPath = path.join(userExtensionsDir, extName);
659-
const metadataPath = path.join(
660-
destinationPath,
661-
'.gemini-extension-install.json',
662-
);
663-
expect(fs.existsSync(metadataPath)).toBe(true);
664-
665-
// Simulate metadata corruption (e.g., pointing to a non-existent source).
666-
fs.writeFileSync(
667-
metadataPath,
668-
JSON.stringify({ source: '/NON_EXISTENT_PATH', type: 'link' }),
669-
);
670-
671-
// Simulate CLI startup. The manager should detect the broken link
672-
// and proactively delete the orphaned metadata directory.
673-
const newManager = new ExtensionManager({
674-
settings: createTestMergedSettings(),
675-
workspaceDir: tempWorkspaceDir,
676-
requestConsent: vi.fn().mockResolvedValue(true),
677-
requestSetting: null,
678-
integrityManager: mockIntegrityManager,
679-
});
680-
681-
await newManager.loadExtensions();
682-
683-
// Verify the extension failed to load and was proactively cleaned up.
684-
expect(newManager.getExtensions().some((e) => e.name === extName)).toBe(
685-
false,
686-
);
687-
expect(fs.existsSync(destinationPath)).toBe(false);
688-
689-
// Verify the system is self-healed and allows re-linking to the valid source.
690-
await newManager.installOrUpdateExtension({
691-
source: sourceDir,
692-
type: 'link',
693-
});
694-
695-
expect(newManager.getExtensions().some((e) => e.name === extName)).toBe(
696-
true,
697-
);
698-
});
699-
});
700640
});

packages/cli/src/config/extension-manager.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -982,18 +982,11 @@ Would you like to attempt to install via "git clone" instead?`,
982982
plan: config.plan,
983983
};
984984
} catch (e) {
985-
const extName = path.basename(extensionDir);
986-
debugLogger.warn(
987-
`Warning: Removing broken extension ${extName}: ${getErrorMessage(e)}`,
985+
debugLogger.error(
986+
`Warning: Skipping extension in ${effectiveExtensionPath}: ${getErrorMessage(
987+
e,
988+
)}`,
988989
);
989-
try {
990-
await fs.promises.rm(extensionDir, { recursive: true, force: true });
991-
} catch (rmError) {
992-
debugLogger.error(
993-
`Failed to remove broken extension directory ${extensionDir}:`,
994-
rmError,
995-
);
996-
}
997990
return null;
998991
}
999992
}

packages/cli/src/config/extension.test.ts

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -249,8 +249,10 @@ describe('extension tests', () => {
249249
expect(extensions[0].name).toBe('test-extension');
250250
});
251251

252-
it('should log a warning and remove the extension if a context file path is outside the extension directory', async () => {
253-
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
252+
it('should skip the extension if a context file path is outside the extension directory and log an error', async () => {
253+
const consoleSpy = vi
254+
.spyOn(console, 'error')
255+
.mockImplementation(() => {});
254256
createExtension({
255257
extensionsDir: userExtensionsDir,
256258
name: 'traversal-extension',
@@ -660,8 +662,10 @@ name = "yolo-checker"
660662
expect(serverConfig.env!['MISSING_VAR_BRACES']).toBe('${ALSO_UNDEFINED}');
661663
});
662664

663-
it('should remove an extension with invalid JSON config and log a warning', async () => {
664-
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
665+
it('should skip an extension with invalid JSON config and log an error', async () => {
666+
const consoleSpy = vi
667+
.spyOn(console, 'error')
668+
.mockImplementation(() => {});
665669

666670
// Good extension
667671
createExtension({
@@ -682,15 +686,17 @@ name = "yolo-checker"
682686
expect(extensions[0].name).toBe('good-ext');
683687
expect(consoleSpy).toHaveBeenCalledWith(
684688
expect.stringContaining(
685-
`Warning: Removing broken extension bad-ext: Failed to load extension config from ${badConfigPath}`,
689+
`Warning: Skipping extension in ${badExtDir}: Failed to load extension config from ${badConfigPath}`,
686690
),
687691
);
688692

689693
consoleSpy.mockRestore();
690694
});
691695

692-
it('should remove an extension with missing "name" in config and log a warning', async () => {
693-
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
696+
it('should skip an extension with missing "name" in config and log an error', async () => {
697+
const consoleSpy = vi
698+
.spyOn(console, 'error')
699+
.mockImplementation(() => {});
694700

695701
// Good extension
696702
createExtension({
@@ -711,7 +717,7 @@ name = "yolo-checker"
711717
expect(extensions[0].name).toBe('good-ext');
712718
expect(consoleSpy).toHaveBeenCalledWith(
713719
expect.stringContaining(
714-
`Warning: Removing broken extension bad-ext-no-name: Failed to load extension config from ${badConfigPath}: Invalid configuration in ${badConfigPath}: missing "name"`,
720+
`Warning: Skipping extension in ${badExtDir}: Failed to load extension config from ${badConfigPath}: Invalid configuration in ${badConfigPath}: missing "name"`,
715721
),
716722
);
717723

@@ -737,8 +743,10 @@ name = "yolo-checker"
737743
expect(extensions[0].mcpServers?.['test-server'].trust).toBeUndefined();
738744
});
739745

740-
it('should log a warning for invalid extension names during loading', async () => {
741-
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
746+
it('should log an error for invalid extension names during loading', async () => {
747+
const consoleSpy = vi
748+
.spyOn(console, 'error')
749+
.mockImplementation(() => {});
742750
createExtension({
743751
extensionsDir: userExtensionsDir,
744752
name: 'bad_name',

0 commit comments

Comments
 (0)