fix(cli): Exit CLI when trust save unsuccessful during launch (#11968)

This commit is contained in:
shrutip90
2025-11-14 11:56:39 -08:00
committed by GitHub
parent 6d83d3440c
commit d683e1c0db
7 changed files with 174 additions and 30 deletions
+21 -11
View File
@@ -76,11 +76,17 @@ export class LoadedTrustedFolders {
* @param location path * @param location path
* @returns * @returns
*/ */
isPathTrusted(location: string): boolean | undefined { isPathTrusted(
location: string,
config?: Record<string, TrustLevel>,
): boolean | undefined {
const configToUse = config ?? this.user.config;
const trustedPaths: string[] = []; const trustedPaths: string[] = [];
const untrustedPaths: string[] = []; const untrustedPaths: string[] = [];
for (const rule of this.rules) { for (const rule of Object.entries(configToUse).map(
([path, trustLevel]) => ({ path, trustLevel }),
)) {
switch (rule.trustLevel) { switch (rule.trustLevel) {
case TrustLevel.TRUST_FOLDER: case TrustLevel.TRUST_FOLDER:
trustedPaths.push(rule.path); trustedPaths.push(rule.path);
@@ -113,8 +119,19 @@ export class LoadedTrustedFolders {
} }
setValue(path: string, trustLevel: TrustLevel): void { setValue(path: string, trustLevel: TrustLevel): void {
const originalTrustLevel = this.user.config[path];
this.user.config[path] = trustLevel; this.user.config[path] = trustLevel;
try {
saveTrustedFolders(this.user); saveTrustedFolders(this.user);
} catch (e) {
// Revert the in-memory change if the save failed.
if (originalTrustLevel === undefined) {
delete this.user.config[path];
} else {
this.user.config[path] = originalTrustLevel;
}
throw e;
}
} }
} }
@@ -174,7 +191,6 @@ export function loadTrustedFolders(): LoadedTrustedFolders {
export function saveTrustedFolders( export function saveTrustedFolders(
trustedFoldersFile: TrustedFoldersFile, trustedFoldersFile: TrustedFoldersFile,
): void { ): void {
try {
// Ensure the directory exists // Ensure the directory exists
const dirPath = path.dirname(trustedFoldersFile.path); const dirPath = path.dirname(trustedFoldersFile.path);
if (!fs.existsSync(dirPath)) { if (!fs.existsSync(dirPath)) {
@@ -186,9 +202,6 @@ export function saveTrustedFolders(
JSON.stringify(trustedFoldersFile.config, null, 2), JSON.stringify(trustedFoldersFile.config, null, 2),
{ encoding: 'utf-8', mode: 0o600 }, { encoding: 'utf-8', mode: 0o600 },
); );
} catch (error) {
console.error('Error saving trusted folders file:', error);
}
} }
/** Is folder trust feature enabled per the current applied settings */ /** Is folder trust feature enabled per the current applied settings */
@@ -201,10 +214,7 @@ function getWorkspaceTrustFromLocalConfig(
trustConfig?: Record<string, TrustLevel>, trustConfig?: Record<string, TrustLevel>,
): TrustResult { ): TrustResult {
const folders = loadTrustedFolders(); const folders = loadTrustedFolders();
const configToUse = trustConfig ?? folders.user.config;
if (trustConfig) {
folders.user.config = trustConfig;
}
if (folders.errors.length > 0) { if (folders.errors.length > 0) {
const errorMessages = folders.errors.map( const errorMessages = folders.errors.map(
@@ -215,7 +225,7 @@ function getWorkspaceTrustFromLocalConfig(
); );
} }
const isTrusted = folders.isPathTrusted(process.cwd()); const isTrusted = folders.isPathTrusted(process.cwd(), configToUse);
return { return {
isTrusted, isTrusted,
source: isTrusted !== undefined ? 'file' : undefined, source: isTrusted !== undefined ? 'file' : undefined,
@@ -148,10 +148,11 @@ describe('PermissionsModifyTrustDialog', () => {
}); });
}); });
it('should commit, restart, and exit on `r` keypress', async () => { it('should commit and restart `r` keypress', async () => {
const mockRelaunchApp = vi const mockRelaunchApp = vi
.spyOn(processUtils, 'relaunchApp') .spyOn(processUtils, 'relaunchApp')
.mockResolvedValue(undefined); .mockResolvedValue(undefined);
mockCommitTrustLevelChange.mockReturnValue(true);
vi.mocked(usePermissionsModifyTrust).mockReturnValue({ vi.mocked(usePermissionsModifyTrust).mockReturnValue({
cwd: '/test/dir', cwd: '/test/dir',
currentTrustLevel: TrustLevel.DO_NOT_TRUST, currentTrustLevel: TrustLevel.DO_NOT_TRUST,
@@ -175,7 +176,6 @@ describe('PermissionsModifyTrustDialog', () => {
await waitFor(() => { await waitFor(() => {
expect(mockCommitTrustLevelChange).toHaveBeenCalled(); expect(mockCommitTrustLevelChange).toHaveBeenCalled();
expect(mockRelaunchApp).toHaveBeenCalled(); expect(mockRelaunchApp).toHaveBeenCalled();
expect(onExit).toHaveBeenCalled();
}); });
mockRelaunchApp.mockRestore(); mockRelaunchApp.mockRestore();
@@ -62,10 +62,13 @@ export function PermissionsModifyTrustDialog({
onExit(); onExit();
} }
if (needsRestart && key.name === 'r') { if (needsRestart && key.name === 'r') {
commitTrustLevelChange(); const success = commitTrustLevelChange();
if (success) {
relaunchApp(); relaunchApp();
} else {
onExit(); onExit();
} }
}
}, },
{ isActive: true }, { isActive: true },
); );
@@ -14,8 +14,10 @@ import { FolderTrustChoice } from '../components/FolderTrustDialog.js';
import type { LoadedTrustedFolders } from '../../config/trustedFolders.js'; import type { LoadedTrustedFolders } from '../../config/trustedFolders.js';
import { TrustLevel } from '../../config/trustedFolders.js'; import { TrustLevel } from '../../config/trustedFolders.js';
import * as trustedFolders from '../../config/trustedFolders.js'; import * as trustedFolders from '../../config/trustedFolders.js';
import { coreEvents } from '@google/gemini-cli-core';
const mockedCwd = vi.hoisted(() => vi.fn()); const mockedCwd = vi.hoisted(() => vi.fn());
const mockedExit = vi.hoisted(() => vi.fn());
vi.mock('node:process', async () => { vi.mock('node:process', async () => {
const actual = const actual =
@@ -23,6 +25,7 @@ vi.mock('node:process', async () => {
return { return {
...actual, ...actual,
cwd: mockedCwd, cwd: mockedCwd,
exit: mockedExit,
platform: 'linux', platform: 'linux',
}; };
}); });
@@ -35,6 +38,7 @@ describe('useFolderTrust', () => {
let addItem: Mock; let addItem: Mock;
beforeEach(() => { beforeEach(() => {
vi.useFakeTimers();
mockSettings = { mockSettings = {
merged: { merged: {
security: { security: {
@@ -60,6 +64,7 @@ describe('useFolderTrust', () => {
}); });
afterEach(() => { afterEach(() => {
vi.useRealTimers();
vi.clearAllMocks(); vi.clearAllMocks();
}); });
@@ -260,4 +265,30 @@ describe('useFolderTrust', () => {
expect(result.current.isRestarting).toBe(false); expect(result.current.isRestarting).toBe(false);
expect(result.current.isFolderTrustDialogOpen).toBe(false); // Dialog should close expect(result.current.isFolderTrustDialogOpen).toBe(false); // Dialog should close
}); });
it('should emit feedback on failure to set value', () => {
isWorkspaceTrustedSpy.mockReturnValue({
isTrusted: undefined,
source: undefined,
});
(mockTrustedFolders.setValue as Mock).mockImplementation(() => {
throw new Error('test error');
});
const emitFeedbackSpy = vi.spyOn(coreEvents, 'emitFeedback');
const { result } = renderHook(() =>
useFolderTrust(mockSettings, onTrustChange, addItem),
);
act(() => {
result.current.handleFolderTrustSelect(FolderTrustChoice.TRUST_FOLDER);
});
vi.runAllTimers();
expect(emitFeedbackSpy).toHaveBeenCalledWith(
'error',
'Failed to save trust settings. Exiting Gemini CLI.',
);
expect(mockedExit).toHaveBeenCalledWith(1);
});
}); });
@@ -14,6 +14,7 @@ import {
} from '../../config/trustedFolders.js'; } from '../../config/trustedFolders.js';
import * as process from 'node:process'; import * as process from 'node:process';
import { type HistoryItemWithoutId, MessageType } from '../types.js'; import { type HistoryItemWithoutId, MessageType } from '../types.js';
import { coreEvents } from '@google/gemini-cli-core';
export const useFolderTrust = ( export const useFolderTrust = (
settings: LoadedSettings, settings: LoadedSettings,
@@ -67,7 +68,19 @@ export const useFolderTrust = (
return; return;
} }
try {
trustedFolders.setValue(cwd, trustLevel); trustedFolders.setValue(cwd, trustLevel);
} catch (_e) {
coreEvents.emitFeedback(
'error',
'Failed to save trust settings. Exiting Gemini CLI.',
);
setTimeout(() => {
process.exit(1);
}, 100);
return;
}
const currentIsTrusted = const currentIsTrusted =
trustLevel === TrustLevel.TRUST_FOLDER || trustLevel === TrustLevel.TRUST_FOLDER ||
trustLevel === TrustLevel.TRUST_PARENT; trustLevel === TrustLevel.TRUST_PARENT;
@@ -19,6 +19,7 @@ import { usePermissionsModifyTrust } from './usePermissionsModifyTrust.js';
import { TrustLevel } from '../../config/trustedFolders.js'; import { TrustLevel } from '../../config/trustedFolders.js';
import type { LoadedSettings } from '../../config/settings.js'; import type { LoadedSettings } from '../../config/settings.js';
import type { LoadedTrustedFolders } from '../../config/trustedFolders.js'; import type { LoadedTrustedFolders } from '../../config/trustedFolders.js';
import { coreEvents } from '@google/gemini-cli-core';
// Hoist mocks // Hoist mocks
const mockedCwd = vi.hoisted(() => vi.fn()); const mockedCwd = vi.hoisted(() => vi.fn());
@@ -259,4 +260,70 @@ describe('usePermissionsModifyTrust', () => {
expect.any(Number), expect.any(Number),
); );
}); });
it('should emit feedback when setValue throws in updateTrustLevel', () => {
const mockSetValue = vi.fn().mockImplementation(() => {
throw new Error('test error');
});
mockedLoadTrustedFolders.mockReturnValue({
user: { config: {} },
setValue: mockSetValue,
} as unknown as LoadedTrustedFolders);
mockedIsWorkspaceTrusted.mockReturnValue({
isTrusted: true,
source: 'file',
});
const emitFeedbackSpy = vi.spyOn(coreEvents, 'emitFeedback');
const { result } = renderHook(() =>
usePermissionsModifyTrust(mockOnExit, mockAddItem),
);
act(() => {
result.current.updateTrustLevel(TrustLevel.TRUST_PARENT);
});
expect(emitFeedbackSpy).toHaveBeenCalledWith(
'error',
'Failed to save trust settings. Your changes may not persist.',
);
expect(mockOnExit).toHaveBeenCalled();
});
it('should emit feedback when setValue throws in commitTrustLevelChange', () => {
const mockSetValue = vi.fn().mockImplementation(() => {
throw new Error('test error');
});
mockedLoadTrustedFolders.mockReturnValue({
user: { config: {} },
setValue: mockSetValue,
} as unknown as LoadedTrustedFolders);
mockedIsWorkspaceTrusted
.mockReturnValueOnce({ isTrusted: false, source: 'file' })
.mockReturnValueOnce({ isTrusted: true, source: 'file' });
const emitFeedbackSpy = vi.spyOn(coreEvents, 'emitFeedback');
const { result } = renderHook(() =>
usePermissionsModifyTrust(mockOnExit, mockAddItem),
);
act(() => {
result.current.updateTrustLevel(TrustLevel.TRUST_FOLDER);
});
act(() => {
const success = result.current.commitTrustLevelChange();
expect(success).toBe(false);
});
expect(emitFeedbackSpy).toHaveBeenCalledWith(
'error',
'Failed to save trust settings. Your changes may not persist.',
);
expect(result.current.needsRestart).toBe(false);
});
}); });
@@ -16,6 +16,7 @@ import { useSettings } from '../contexts/SettingsContext.js';
import { MessageType } from '../types.js'; import { MessageType } from '../types.js';
import { type UseHistoryManagerReturn } from './useHistoryManager.js'; import { type UseHistoryManagerReturn } from './useHistoryManager.js';
import type { LoadedSettings } from '../../config/settings.js'; import type { LoadedSettings } from '../../config/settings.js';
import { coreEvents } from '@google/gemini-cli-core';
interface TrustState { interface TrustState {
currentTrustLevel: TrustLevel | undefined; currentTrustLevel: TrustLevel | undefined;
@@ -101,7 +102,14 @@ export const usePermissionsModifyTrust = (
setNeedsRestart(true); setNeedsRestart(true);
} else { } else {
const folders = loadTrustedFolders(); const folders = loadTrustedFolders();
try {
folders.setValue(cwd, trustLevel); folders.setValue(cwd, trustLevel);
} catch (_e) {
coreEvents.emitFeedback(
'error',
'Failed to save trust settings. Your changes may not persist.',
);
}
onExit(); onExit();
} }
}, },
@@ -111,8 +119,20 @@ export const usePermissionsModifyTrust = (
const commitTrustLevelChange = useCallback(() => { const commitTrustLevelChange = useCallback(() => {
if (pendingTrustLevel) { if (pendingTrustLevel) {
const folders = loadTrustedFolders(); const folders = loadTrustedFolders();
try {
folders.setValue(cwd, pendingTrustLevel); folders.setValue(cwd, pendingTrustLevel);
return true;
} catch (_e) {
coreEvents.emitFeedback(
'error',
'Failed to save trust settings. Your changes may not persist.',
);
setNeedsRestart(false);
setPendingTrustLevel(undefined);
return false;
} }
}
return true;
}, [cwd, pendingTrustLevel]); }, [cwd, pendingTrustLevel]);
return { return {