feat(cli): implement atomic writes and safety checks for trusted folders (#18406)

This commit is contained in:
Gal Zahavi
2026-02-09 09:16:56 -08:00
committed by GitHub
parent 01906a9205
commit 81ccd80c6d
16 changed files with 549 additions and 971 deletions
@@ -67,7 +67,7 @@ describe('ConsentPrompt', () => {
unmount();
});
it('calls onConfirm with true when "Yes" is selected', () => {
it('calls onConfirm with true when "Yes" is selected', async () => {
const prompt = 'Are you sure?';
const { unmount } = render(
<ConsentPrompt
@@ -78,7 +78,7 @@ describe('ConsentPrompt', () => {
);
const onSelect = MockedRadioButtonSelect.mock.calls[0][0].onSelect;
act(() => {
await act(async () => {
onSelect(true);
});
@@ -86,7 +86,7 @@ describe('ConsentPrompt', () => {
unmount();
});
it('calls onConfirm with false when "No" is selected', () => {
it('calls onConfirm with false when "No" is selected', async () => {
const prompt = 'Are you sure?';
const { unmount } = render(
<ConsentPrompt
@@ -97,7 +97,7 @@ describe('ConsentPrompt', () => {
);
const onSelect = MockedRadioButtonSelect.mock.calls[0][0].onSelect;
act(() => {
await act(async () => {
onSelect(false);
});
@@ -46,22 +46,26 @@ describe('LogoutConfirmationDialog', () => {
expect(mockCall.isFocused).toBe(true);
});
it('should call onSelect with LOGIN when Login is selected', () => {
it('should call onSelect with LOGIN when Login is selected', async () => {
const onSelect = vi.fn();
renderWithProviders(<LogoutConfirmationDialog onSelect={onSelect} />);
const mockCall = vi.mocked(RadioButtonSelect).mock.calls[0][0];
mockCall.onSelect(LogoutChoice.LOGIN);
await act(async () => {
mockCall.onSelect(LogoutChoice.LOGIN);
});
expect(onSelect).toHaveBeenCalledWith(LogoutChoice.LOGIN);
});
it('should call onSelect with EXIT when Exit is selected', () => {
it('should call onSelect with EXIT when Exit is selected', async () => {
const onSelect = vi.fn();
renderWithProviders(<LogoutConfirmationDialog onSelect={onSelect} />);
const mockCall = vi.mocked(RadioButtonSelect).mock.calls[0][0];
mockCall.onSelect(LogoutChoice.EXIT);
await act(async () => {
mockCall.onSelect(LogoutChoice.EXIT);
});
expect(onSelect).toHaveBeenCalledWith(LogoutChoice.EXIT);
});
@@ -125,7 +125,10 @@ export const MultiFolderTrustDialog: React.FC<MultiFolderTrustDialogProps> = ({
try {
const expandedPath = path.resolve(expandHomeDir(dir));
if (choice === MultiFolderTrustChoice.YES_AND_REMEMBER) {
trustedFolders.setValue(expandedPath, TrustLevel.TRUST_FOLDER);
await trustedFolders.setValue(
expandedPath,
TrustLevel.TRUST_FOLDER,
);
}
workspaceContext.addDirectory(expandedPath);
added.push(dir);
@@ -69,13 +69,14 @@ export function PermissionsModifyTrustDialog({
return true;
}
if (needsRestart && key.name === 'r') {
const success = commitTrustLevelChange();
if (success) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
relaunchApp();
} else {
onExit();
}
void (async () => {
const success = await commitTrustLevelChange();
if (success) {
void relaunchApp();
} else {
onExit();
}
})();
return true;
}
return false;
@@ -149,7 +149,9 @@ describe('useFolderTrust', () => {
});
await act(async () => {
result.current.handleFolderTrustSelect(FolderTrustChoice.TRUST_FOLDER);
await result.current.handleFolderTrustSelect(
FolderTrustChoice.TRUST_FOLDER,
);
});
await waitFor(() => {
@@ -173,7 +175,9 @@ describe('useFolderTrust', () => {
);
await act(async () => {
result.current.handleFolderTrustSelect(FolderTrustChoice.TRUST_PARENT);
await result.current.handleFolderTrustSelect(
FolderTrustChoice.TRUST_PARENT,
);
});
await waitFor(() => {
@@ -197,7 +201,9 @@ describe('useFolderTrust', () => {
);
await act(async () => {
result.current.handleFolderTrustSelect(FolderTrustChoice.DO_NOT_TRUST);
await result.current.handleFolderTrustSelect(
FolderTrustChoice.DO_NOT_TRUST,
);
});
await waitFor(() => {
@@ -221,7 +227,7 @@ describe('useFolderTrust', () => {
);
await act(async () => {
result.current.handleFolderTrustSelect(
await result.current.handleFolderTrustSelect(
'invalid_choice' as FolderTrustChoice,
);
});
@@ -253,7 +259,9 @@ describe('useFolderTrust', () => {
});
await act(async () => {
result.current.handleFolderTrustSelect(FolderTrustChoice.TRUST_FOLDER);
await result.current.handleFolderTrustSelect(
FolderTrustChoice.TRUST_FOLDER,
);
});
await waitFor(() => {
@@ -272,7 +280,9 @@ describe('useFolderTrust', () => {
);
await act(async () => {
result.current.handleFolderTrustSelect(FolderTrustChoice.TRUST_FOLDER);
await result.current.handleFolderTrustSelect(
FolderTrustChoice.TRUST_FOLDER,
);
});
await waitFor(() => {
@@ -294,8 +304,10 @@ describe('useFolderTrust', () => {
useFolderTrust(mockSettings, onTrustChange, addItem),
);
act(() => {
result.current.handleFolderTrustSelect(FolderTrustChoice.TRUST_FOLDER);
await act(async () => {
await result.current.handleFolderTrustSelect(
FolderTrustChoice.TRUST_FOLDER,
);
});
await vi.runAllTimersAsync();
+2 -2
View File
@@ -48,7 +48,7 @@ export const useFolderTrust = (
}, [folderTrust, onTrustChange, settings.merged, addItem]);
const handleFolderTrustSelect = useCallback(
(choice: FolderTrustChoice) => {
async (choice: FolderTrustChoice) => {
const trustLevelMap: Record<FolderTrustChoice, TrustLevel> = {
[FolderTrustChoice.TRUST_FOLDER]: TrustLevel.TRUST_FOLDER,
[FolderTrustChoice.TRUST_PARENT]: TrustLevel.TRUST_PARENT,
@@ -62,7 +62,7 @@ export const useFolderTrust = (
const trustedFolders = loadTrustedFolders();
try {
trustedFolders.setValue(cwd, trustLevel);
await trustedFolders.setValue(cwd, trustLevel);
} catch (_e) {
coreEvents.emitFeedback(
'error',
@@ -142,7 +142,7 @@ describe('usePermissionsModifyTrust', () => {
expect(result.current.isInheritedTrustFromParent).toBe(false);
});
it('should set needsRestart but not save when trust changes', () => {
it('should set needsRestart but not save when trust changes', async () => {
const mockSetValue = vi.fn();
mockedLoadTrustedFolders.mockReturnValue({
user: { config: {} },
@@ -157,15 +157,15 @@ describe('usePermissionsModifyTrust', () => {
usePermissionsModifyTrust(mockOnExit, mockAddItem, mockedCwd()),
);
act(() => {
result.current.updateTrustLevel(TrustLevel.TRUST_FOLDER);
await act(async () => {
await result.current.updateTrustLevel(TrustLevel.TRUST_FOLDER);
});
expect(result.current.needsRestart).toBe(true);
expect(mockSetValue).not.toHaveBeenCalled();
});
it('should save immediately if trust does not change', () => {
it('should save immediately if trust does not change', async () => {
const mockSetValue = vi.fn();
mockedLoadTrustedFolders.mockReturnValue({
user: { config: {} },
@@ -181,8 +181,8 @@ describe('usePermissionsModifyTrust', () => {
usePermissionsModifyTrust(mockOnExit, mockAddItem, mockedCwd()),
);
act(() => {
result.current.updateTrustLevel(TrustLevel.TRUST_PARENT);
await act(async () => {
await result.current.updateTrustLevel(TrustLevel.TRUST_PARENT);
});
expect(result.current.needsRestart).toBe(false);
@@ -193,7 +193,7 @@ describe('usePermissionsModifyTrust', () => {
expect(mockOnExit).toHaveBeenCalled();
});
it('should commit the pending trust level change', () => {
it('should commit the pending trust level change', async () => {
const mockSetValue = vi.fn();
mockedLoadTrustedFolders.mockReturnValue({
user: { config: {} },
@@ -208,14 +208,14 @@ describe('usePermissionsModifyTrust', () => {
usePermissionsModifyTrust(mockOnExit, mockAddItem, mockedCwd()),
);
act(() => {
result.current.updateTrustLevel(TrustLevel.TRUST_FOLDER);
await act(async () => {
await result.current.updateTrustLevel(TrustLevel.TRUST_FOLDER);
});
expect(result.current.needsRestart).toBe(true);
act(() => {
result.current.commitTrustLevelChange();
await act(async () => {
await result.current.commitTrustLevelChange();
});
expect(mockSetValue).toHaveBeenCalledWith(
@@ -224,7 +224,7 @@ describe('usePermissionsModifyTrust', () => {
);
});
it('should add warning when setting DO_NOT_TRUST but still trusted by parent', () => {
it('should add warning when setting DO_NOT_TRUST but still trusted by parent', async () => {
mockedLoadTrustedFolders.mockReturnValue({
user: { config: {} },
setValue: vi.fn(),
@@ -238,8 +238,8 @@ describe('usePermissionsModifyTrust', () => {
usePermissionsModifyTrust(mockOnExit, mockAddItem, mockedCwd()),
);
act(() => {
result.current.updateTrustLevel(TrustLevel.DO_NOT_TRUST);
await act(async () => {
await result.current.updateTrustLevel(TrustLevel.DO_NOT_TRUST);
});
expect(mockAddItem).toHaveBeenCalledWith(
@@ -251,7 +251,7 @@ describe('usePermissionsModifyTrust', () => {
);
});
it('should add warning when setting DO_NOT_TRUST but still trusted by IDE', () => {
it('should add warning when setting DO_NOT_TRUST but still trusted by IDE', async () => {
mockedLoadTrustedFolders.mockReturnValue({
user: { config: {} },
setValue: vi.fn(),
@@ -265,8 +265,8 @@ describe('usePermissionsModifyTrust', () => {
usePermissionsModifyTrust(mockOnExit, mockAddItem, mockedCwd()),
);
act(() => {
result.current.updateTrustLevel(TrustLevel.DO_NOT_TRUST);
await act(async () => {
await result.current.updateTrustLevel(TrustLevel.DO_NOT_TRUST);
});
expect(mockAddItem).toHaveBeenCalledWith(
@@ -299,7 +299,7 @@ describe('usePermissionsModifyTrust', () => {
expect(result.current.isInheritedTrustFromIde).toBe(false);
});
it('should save immediately without needing a restart', () => {
it('should save immediately without needing a restart', async () => {
const mockSetValue = vi.fn();
mockedLoadTrustedFolders.mockReturnValue({
user: { config: {} },
@@ -314,8 +314,8 @@ describe('usePermissionsModifyTrust', () => {
usePermissionsModifyTrust(mockOnExit, mockAddItem, otherDirectory),
);
act(() => {
result.current.updateTrustLevel(TrustLevel.TRUST_FOLDER);
await act(async () => {
await result.current.updateTrustLevel(TrustLevel.TRUST_FOLDER);
});
expect(result.current.needsRestart).toBe(false);
@@ -326,7 +326,7 @@ describe('usePermissionsModifyTrust', () => {
expect(mockOnExit).toHaveBeenCalled();
});
it('should not add a warning when setting DO_NOT_TRUST', () => {
it('should not add a warning when setting DO_NOT_TRUST', async () => {
mockedLoadTrustedFolders.mockReturnValue({
user: { config: {} },
setValue: vi.fn(),
@@ -340,15 +340,15 @@ describe('usePermissionsModifyTrust', () => {
usePermissionsModifyTrust(mockOnExit, mockAddItem, otherDirectory),
);
act(() => {
result.current.updateTrustLevel(TrustLevel.DO_NOT_TRUST);
await act(async () => {
await result.current.updateTrustLevel(TrustLevel.DO_NOT_TRUST);
});
expect(mockAddItem).not.toHaveBeenCalled();
});
});
it('should emit feedback when setValue throws in updateTrustLevel', () => {
it('should emit feedback when setValue throws in updateTrustLevel', async () => {
const mockSetValue = vi.fn().mockImplementation(() => {
throw new Error('test error');
});
@@ -368,8 +368,8 @@ describe('usePermissionsModifyTrust', () => {
usePermissionsModifyTrust(mockOnExit, mockAddItem, mockedCwd()),
);
act(() => {
result.current.updateTrustLevel(TrustLevel.TRUST_PARENT);
await act(async () => {
await result.current.updateTrustLevel(TrustLevel.TRUST_PARENT);
});
expect(emitFeedbackSpy).toHaveBeenCalledWith(
@@ -379,7 +379,7 @@ describe('usePermissionsModifyTrust', () => {
expect(mockOnExit).toHaveBeenCalled();
});
it('should emit feedback when setValue throws in commitTrustLevelChange', () => {
it('should emit feedback when setValue throws in commitTrustLevelChange', async () => {
const mockSetValue = vi.fn().mockImplementation(() => {
throw new Error('test error');
});
@@ -398,12 +398,12 @@ describe('usePermissionsModifyTrust', () => {
usePermissionsModifyTrust(mockOnExit, mockAddItem, mockedCwd()),
);
act(() => {
result.current.updateTrustLevel(TrustLevel.TRUST_FOLDER);
await act(async () => {
await result.current.updateTrustLevel(TrustLevel.TRUST_FOLDER);
});
act(() => {
const success = result.current.commitTrustLevelChange();
await act(async () => {
const success = await result.current.commitTrustLevelChange();
expect(success).toBe(false);
});
@@ -92,12 +92,12 @@ export const usePermissionsModifyTrust = (
settings.merged.security.folderTrust.enabled ?? true;
const updateTrustLevel = useCallback(
(trustLevel: TrustLevel) => {
async (trustLevel: TrustLevel) => {
// If we are not editing the current workspace, the logic is simple:
// just save the setting and exit. No restart or warnings are needed.
if (!isCurrentWorkspace) {
const folders = loadTrustedFolders();
folders.setValue(cwd, trustLevel);
await folders.setValue(cwd, trustLevel);
onExit();
return;
}
@@ -140,7 +140,7 @@ export const usePermissionsModifyTrust = (
} else {
const folders = loadTrustedFolders();
try {
folders.setValue(cwd, trustLevel);
await folders.setValue(cwd, trustLevel);
} catch (_e) {
coreEvents.emitFeedback(
'error',
@@ -153,11 +153,11 @@ export const usePermissionsModifyTrust = (
[cwd, settings.merged, onExit, addItem, isCurrentWorkspace],
);
const commitTrustLevelChange = useCallback(() => {
const commitTrustLevelChange = useCallback(async () => {
if (pendingTrustLevel) {
const folders = loadTrustedFolders();
try {
folders.setValue(cwd, pendingTrustLevel);
await folders.setValue(cwd, pendingTrustLevel);
return true;
} catch (_e) {
coreEvents.emitFeedback(