@google/gemini-cli 0.28.0-preview.5 → 0.28.0-preview.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/google-gemini-cli-0.28.0-preview.6.tgz +0 -0
  2. package/dist/package.json +3 -2
  3. package/dist/src/config/extension-manager.js +1 -1
  4. package/dist/src/config/extension-manager.js.map +1 -1
  5. package/dist/src/config/extensions/extensionUpdates.test.js +98 -132
  6. package/dist/src/config/extensions/extensionUpdates.test.js.map +1 -1
  7. package/dist/src/config/trustedFolders.d.ts +1 -1
  8. package/dist/src/config/trustedFolders.js +80 -15
  9. package/dist/src/config/trustedFolders.js.map +1 -1
  10. package/dist/src/config/trustedFolders.test.js +211 -558
  11. package/dist/src/config/trustedFolders.test.js.map +1 -1
  12. package/dist/src/generated/git-commit.d.ts +2 -2
  13. package/dist/src/generated/git-commit.js +2 -2
  14. package/dist/src/ui/components/ConsentPrompt.test.js +4 -4
  15. package/dist/src/ui/components/ConsentPrompt.test.js.map +1 -1
  16. package/dist/src/ui/components/LogoutConfirmationDialog.test.js +8 -4
  17. package/dist/src/ui/components/LogoutConfirmationDialog.test.js.map +1 -1
  18. package/dist/src/ui/components/MultiFolderTrustDialog.js +1 -1
  19. package/dist/src/ui/components/MultiFolderTrustDialog.js.map +1 -1
  20. package/dist/src/ui/components/PermissionsModifyTrustDialog.js +9 -8
  21. package/dist/src/ui/components/PermissionsModifyTrustDialog.js.map +1 -1
  22. package/dist/src/ui/hooks/useFolderTrust.d.ts +1 -1
  23. package/dist/src/ui/hooks/useFolderTrust.js +2 -2
  24. package/dist/src/ui/hooks/useFolderTrust.js.map +1 -1
  25. package/dist/src/ui/hooks/useFolderTrust.test.js +8 -8
  26. package/dist/src/ui/hooks/useFolderTrust.test.js.map +1 -1
  27. package/dist/src/ui/hooks/usePermissionsModifyTrust.d.ts +2 -2
  28. package/dist/src/ui/hooks/usePermissionsModifyTrust.js +5 -5
  29. package/dist/src/ui/hooks/usePermissionsModifyTrust.js.map +1 -1
  30. package/dist/src/ui/hooks/usePermissionsModifyTrust.test.js +31 -31
  31. package/dist/src/ui/hooks/usePermissionsModifyTrust.test.js.map +1 -1
  32. package/dist/tsconfig.tsbuildinfo +1 -1
  33. package/package.json +4 -3
  34. package/dist/google-gemini-cli-0.28.0-preview.4.tgz +0 -0
@@ -3,93 +3,140 @@
3
3
  * Copyright 2025 Google LLC
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
- import * as osActual from 'node:os';
7
- import { FatalConfigError, ideContextStore, AuthType, } from '@google/gemini-cli-core';
8
- import { describe, it, expect, vi, beforeEach, afterEach, } from 'vitest';
6
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
9
7
  import * as fs from 'node:fs';
10
- import stripJsonComments from 'strip-json-comments';
11
8
  import * as path from 'node:path';
12
- import { loadTrustedFolders, getTrustedFoldersPath, TrustLevel, isWorkspaceTrusted, resetTrustedFoldersForTesting, } from './trustedFolders.js';
13
- import { loadEnvironment, getSettingsSchema } from './settings.js';
9
+ import * as os from 'node:os';
10
+ import { FatalConfigError, ideContextStore, coreEvents, } from '@google/gemini-cli-core';
11
+ import { loadTrustedFolders, TrustLevel, isWorkspaceTrusted, resetTrustedFoldersForTesting, } from './trustedFolders.js';
12
+ import { loadEnvironment } from './settings.js';
14
13
  import { createMockSettings } from '../test-utils/settings.js';
15
- import { validateAuthMethod } from './auth.js';
16
- vi.mock('os', async (importOriginal) => {
17
- const actualOs = await importOriginal();
18
- return {
19
- ...actualOs,
20
- homedir: vi.fn(() => '/mock/home/user'),
21
- platform: vi.fn(() => 'linux'),
22
- };
23
- });
14
+ // We explicitly do NOT mock 'fs' or 'proper-lockfile' here to ensure
15
+ // we are testing the actual behavior on the real file system.
24
16
  vi.mock('@google/gemini-cli-core', async (importOriginal) => {
25
17
  const actual = await importOriginal();
26
18
  return {
27
19
  ...actual,
28
20
  homedir: () => '/mock/home/user',
21
+ coreEvents: {
22
+ emitFeedback: vi.fn(),
23
+ },
29
24
  };
30
25
  });
31
- vi.mock('fs', async (importOriginal) => {
32
- const actualFs = await importOriginal();
33
- return {
34
- ...actualFs,
35
- existsSync: vi.fn(),
36
- readFileSync: vi.fn(),
37
- writeFileSync: vi.fn(),
38
- mkdirSync: vi.fn(),
39
- realpathSync: vi.fn().mockImplementation((p) => p),
40
- };
41
- });
42
- vi.mock('strip-json-comments', () => ({
43
- default: vi.fn((content) => content),
44
- }));
45
- describe('Trusted Folders Loading', () => {
46
- let mockStripJsonComments;
47
- let mockFsWriteFileSync;
26
+ describe('Trusted Folders', () => {
27
+ let tempDir;
28
+ let trustedFoldersPath;
48
29
  beforeEach(() => {
30
+ // Create a temporary directory for each test
31
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gemini-cli-test-'));
32
+ trustedFoldersPath = path.join(tempDir, 'trustedFolders.json');
33
+ // Set the environment variable to point to the temp file
34
+ vi.stubEnv('GEMINI_CLI_TRUSTED_FOLDERS_PATH', trustedFoldersPath);
35
+ // Reset the internal state
49
36
  resetTrustedFoldersForTesting();
50
- vi.resetAllMocks();
51
- mockStripJsonComments = vi.mocked(stripJsonComments);
52
- mockFsWriteFileSync = vi.mocked(fs.writeFileSync);
53
- vi.mocked(osActual.homedir).mockReturnValue('/mock/home/user');
54
- mockStripJsonComments.mockImplementation((jsonString) => jsonString);
55
- vi.mocked(fs.existsSync).mockReturnValue(false);
56
- vi.mocked(fs.readFileSync).mockReturnValue('{}');
57
- vi.mocked(fs.realpathSync).mockImplementation((p) => p.toString());
37
+ vi.clearAllMocks();
58
38
  });
59
39
  afterEach(() => {
60
- vi.restoreAllMocks();
40
+ // Clean up the temporary directory
41
+ fs.rmSync(tempDir, { recursive: true, force: true });
42
+ vi.unstubAllEnvs();
61
43
  });
62
- it('should load empty rules if no files exist', () => {
63
- const { rules, errors } = loadTrustedFolders();
64
- expect(rules).toEqual([]);
65
- expect(errors).toEqual([]);
44
+ describe('Locking & Concurrency', () => {
45
+ it('setValue should handle concurrent calls correctly using real lockfile', async () => {
46
+ // Initialize the file
47
+ fs.writeFileSync(trustedFoldersPath, '{}', 'utf-8');
48
+ const loadedFolders = loadTrustedFolders();
49
+ // Start two concurrent calls
50
+ // These will race to acquire the lock on the real file system
51
+ const p1 = loadedFolders.setValue('/path1', TrustLevel.TRUST_FOLDER);
52
+ const p2 = loadedFolders.setValue('/path2', TrustLevel.TRUST_FOLDER);
53
+ await Promise.all([p1, p2]);
54
+ // Verify final state in the file
55
+ const content = fs.readFileSync(trustedFoldersPath, 'utf-8');
56
+ const config = JSON.parse(content);
57
+ expect(config).toEqual({
58
+ '/path1': TrustLevel.TRUST_FOLDER,
59
+ '/path2': TrustLevel.TRUST_FOLDER,
60
+ });
61
+ });
62
+ });
63
+ describe('Loading & Parsing', () => {
64
+ it('should load empty rules if no files exist', () => {
65
+ const { rules, errors } = loadTrustedFolders();
66
+ expect(rules).toEqual([]);
67
+ expect(errors).toEqual([]);
68
+ });
69
+ it('should load rules from the configuration file', () => {
70
+ const config = {
71
+ '/user/folder': TrustLevel.TRUST_FOLDER,
72
+ };
73
+ fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
74
+ const { rules, errors } = loadTrustedFolders();
75
+ expect(rules).toEqual([
76
+ { path: '/user/folder', trustLevel: TrustLevel.TRUST_FOLDER },
77
+ ]);
78
+ expect(errors).toEqual([]);
79
+ });
80
+ it('should handle JSON parsing errors gracefully', () => {
81
+ fs.writeFileSync(trustedFoldersPath, 'invalid json', 'utf-8');
82
+ const { rules, errors } = loadTrustedFolders();
83
+ expect(rules).toEqual([]);
84
+ expect(errors.length).toBe(1);
85
+ expect(errors[0].path).toBe(trustedFoldersPath);
86
+ expect(errors[0].message).toContain('Unexpected token');
87
+ });
88
+ it('should handle non-object JSON gracefully', () => {
89
+ fs.writeFileSync(trustedFoldersPath, 'null', 'utf-8');
90
+ const { rules, errors } = loadTrustedFolders();
91
+ expect(rules).toEqual([]);
92
+ expect(errors.length).toBe(1);
93
+ expect(errors[0].message).toContain('not a valid JSON object');
94
+ });
95
+ it('should handle invalid trust levels gracefully', () => {
96
+ const config = {
97
+ '/path': 'INVALID_LEVEL',
98
+ };
99
+ fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
100
+ const { rules, errors } = loadTrustedFolders();
101
+ expect(rules).toEqual([]);
102
+ expect(errors.length).toBe(1);
103
+ expect(errors[0].message).toContain('Invalid trust level "INVALID_LEVEL"');
104
+ });
105
+ it('should support JSON with comments', () => {
106
+ const content = `
107
+ {
108
+ // This is a comment
109
+ "/path": "TRUST_FOLDER"
110
+ }
111
+ `;
112
+ fs.writeFileSync(trustedFoldersPath, content, 'utf-8');
113
+ const { rules, errors } = loadTrustedFolders();
114
+ expect(rules).toEqual([
115
+ { path: '/path', trustLevel: TrustLevel.TRUST_FOLDER },
116
+ ]);
117
+ expect(errors).toEqual([]);
118
+ });
66
119
  });
67
120
  describe('isPathTrusted', () => {
68
- function setup({ config = {} } = {}) {
69
- vi.mocked(fs.existsSync).mockImplementation((p) => p.toString() === getTrustedFoldersPath());
70
- vi.mocked(fs.readFileSync).mockImplementation((p) => {
71
- if (p.toString() === getTrustedFoldersPath())
72
- return JSON.stringify(config);
73
- return '{}';
74
- });
75
- const folders = loadTrustedFolders();
76
- return { folders };
121
+ function setup(config) {
122
+ fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
123
+ return loadTrustedFolders();
77
124
  }
78
125
  it('provides a method to determine if a path is trusted', () => {
79
- const { folders } = setup({
80
- config: {
81
- './myfolder': TrustLevel.TRUST_FOLDER,
82
- '/trustedparent/trustme': TrustLevel.TRUST_PARENT,
83
- '/user/folder': TrustLevel.TRUST_FOLDER,
84
- '/secret': TrustLevel.DO_NOT_TRUST,
85
- '/secret/publickeys': TrustLevel.TRUST_FOLDER,
86
- },
126
+ const folders = setup({
127
+ './myfolder': TrustLevel.TRUST_FOLDER,
128
+ '/trustedparent/trustme': TrustLevel.TRUST_PARENT,
129
+ '/user/folder': TrustLevel.TRUST_FOLDER,
130
+ '/secret': TrustLevel.DO_NOT_TRUST,
131
+ '/secret/publickeys': TrustLevel.TRUST_FOLDER,
87
132
  });
133
+ // We need to resolve relative paths for comparison since the implementation uses realpath
134
+ const resolvedMyFolder = path.resolve('./myfolder');
88
135
  expect(folders.isPathTrusted('/secret')).toBe(false);
89
136
  expect(folders.isPathTrusted('/user/folder')).toBe(true);
90
137
  expect(folders.isPathTrusted('/secret/publickeys/public.pem')).toBe(true);
91
138
  expect(folders.isPathTrusted('/user/folder/harhar')).toBe(true);
92
- expect(folders.isPathTrusted('myfolder/somefile.jpg')).toBe(true);
139
+ expect(folders.isPathTrusted(path.join(resolvedMyFolder, 'somefile.jpg'))).toBe(true);
93
140
  expect(folders.isPathTrusted('/trustedparent/someotherfolder')).toBe(true);
94
141
  expect(folders.isPathTrusted('/trustedparent/trustme')).toBe(true);
95
142
  // No explicit rule covers this file
@@ -98,335 +145,51 @@ describe('Trusted Folders Loading', () => {
98
145
  expect(folders.isPathTrusted('/user/someotherfolder')).toBe(undefined);
99
146
  });
100
147
  it('prioritizes the longest matching path (precedence)', () => {
101
- const { folders } = setup({
102
- config: {
103
- '/a': TrustLevel.TRUST_FOLDER,
104
- '/a/b': TrustLevel.DO_NOT_TRUST,
105
- '/a/b/c': TrustLevel.TRUST_FOLDER,
106
- '/parent/trustme': TrustLevel.TRUST_PARENT, // effective path is /parent
107
- '/parent/trustme/butnotthis': TrustLevel.DO_NOT_TRUST,
108
- },
148
+ const folders = setup({
149
+ '/a': TrustLevel.TRUST_FOLDER,
150
+ '/a/b': TrustLevel.DO_NOT_TRUST,
151
+ '/a/b/c': TrustLevel.TRUST_FOLDER,
152
+ '/parent/trustme': TrustLevel.TRUST_PARENT,
153
+ '/parent/trustme/butnotthis': TrustLevel.DO_NOT_TRUST,
109
154
  });
110
- // /a/b/c/d matches /a (len 2), /a/b (len 4), /a/b/c (len 6).
111
- // /a/b/c wins (TRUST_FOLDER).
112
155
  expect(folders.isPathTrusted('/a/b/c/d')).toBe(true);
113
- // /a/b/x matches /a (len 2), /a/b (len 4).
114
- // /a/b wins (DO_NOT_TRUST).
115
156
  expect(folders.isPathTrusted('/a/b/x')).toBe(false);
116
- // /a/x matches /a (len 2).
117
- // /a wins (TRUST_FOLDER).
118
157
  expect(folders.isPathTrusted('/a/x')).toBe(true);
119
- // Overlap with TRUST_PARENT
120
- // /parent/trustme/butnotthis/file matches:
121
- // - /parent/trustme (len 15, TRUST_PARENT -> effective /parent)
122
- // - /parent/trustme/butnotthis (len 26, DO_NOT_TRUST)
123
- // /parent/trustme/butnotthis wins.
124
158
  expect(folders.isPathTrusted('/parent/trustme/butnotthis/file')).toBe(false);
125
- // /parent/other matches /parent/trustme (len 15, effective /parent)
126
159
  expect(folders.isPathTrusted('/parent/other')).toBe(true);
127
160
  });
128
161
  });
129
- it('should load user rules if only user file exists', () => {
130
- const userPath = getTrustedFoldersPath();
131
- vi.mocked(fs.existsSync).mockImplementation((p) => p.toString() === userPath);
132
- const userContent = {
133
- '/user/folder': TrustLevel.TRUST_FOLDER,
134
- };
135
- vi.mocked(fs.readFileSync).mockImplementation((p) => {
136
- if (p.toString() === userPath)
137
- return JSON.stringify(userContent);
138
- return '{}';
162
+ describe('setValue', () => {
163
+ it('should update the user config and save it atomically', async () => {
164
+ fs.writeFileSync(trustedFoldersPath, '{}', 'utf-8');
165
+ const loadedFolders = loadTrustedFolders();
166
+ await loadedFolders.setValue('/new/path', TrustLevel.TRUST_FOLDER);
167
+ expect(loadedFolders.user.config['/new/path']).toBe(TrustLevel.TRUST_FOLDER);
168
+ const content = fs.readFileSync(trustedFoldersPath, 'utf-8');
169
+ const config = JSON.parse(content);
170
+ expect(config['/new/path']).toBe(TrustLevel.TRUST_FOLDER);
139
171
  });
140
- const { rules, errors } = loadTrustedFolders();
141
- expect(rules).toEqual([
142
- { path: '/user/folder', trustLevel: TrustLevel.TRUST_FOLDER },
143
- ]);
144
- expect(errors).toEqual([]);
145
- });
146
- it('should handle JSON parsing errors gracefully', () => {
147
- const userPath = getTrustedFoldersPath();
148
- vi.mocked(fs.existsSync).mockImplementation((p) => p.toString() === userPath);
149
- vi.mocked(fs.readFileSync).mockImplementation((p) => {
150
- if (p.toString() === userPath)
151
- return 'invalid json';
152
- return '{}';
172
+ it('should throw FatalConfigError if there were load errors', async () => {
173
+ fs.writeFileSync(trustedFoldersPath, 'invalid json', 'utf-8');
174
+ const loadedFolders = loadTrustedFolders();
175
+ expect(loadedFolders.errors.length).toBe(1);
176
+ await expect(loadedFolders.setValue('/some/path', TrustLevel.TRUST_FOLDER)).rejects.toThrow(FatalConfigError);
153
177
  });
154
- const { rules, errors } = loadTrustedFolders();
155
- expect(rules).toEqual([]);
156
- expect(errors.length).toBe(1);
157
- expect(errors[0].path).toBe(userPath);
158
- expect(errors[0].message).toContain('Unexpected token');
159
- });
160
- it('should use GEMINI_CLI_TRUSTED_FOLDERS_PATH env var if set', () => {
161
- const customPath = '/custom/path/to/trusted_folders.json';
162
- process.env['GEMINI_CLI_TRUSTED_FOLDERS_PATH'] = customPath;
163
- vi.mocked(fs.existsSync).mockImplementation((p) => p.toString() === customPath);
164
- const userContent = {
165
- '/user/folder/from/env': TrustLevel.TRUST_FOLDER,
166
- };
167
- vi.mocked(fs.readFileSync).mockImplementation((p) => {
168
- if (p.toString() === customPath)
169
- return JSON.stringify(userContent);
170
- return '{}';
171
- });
172
- const { rules, errors } = loadTrustedFolders();
173
- expect(rules).toEqual([
174
- {
175
- path: '/user/folder/from/env',
176
- trustLevel: TrustLevel.TRUST_FOLDER,
177
- },
178
- ]);
179
- expect(errors).toEqual([]);
180
- delete process.env['GEMINI_CLI_TRUSTED_FOLDERS_PATH'];
181
- });
182
- it('setValue should update the user config and save it', () => {
183
- const loadedFolders = loadTrustedFolders();
184
- loadedFolders.setValue('/new/path', TrustLevel.TRUST_FOLDER);
185
- expect(loadedFolders.user.config['/new/path']).toBe(TrustLevel.TRUST_FOLDER);
186
- expect(mockFsWriteFileSync).toHaveBeenCalledWith(getTrustedFoldersPath(), JSON.stringify({ '/new/path': TrustLevel.TRUST_FOLDER }, null, 2), { encoding: 'utf-8', mode: 0o600 });
187
- });
188
- });
189
- describe('isWorkspaceTrusted', () => {
190
- let mockCwd;
191
- const mockRules = {};
192
- const mockSettings = {
193
- security: {
194
- folderTrust: {
195
- enabled: true,
196
- },
197
- },
198
- };
199
- beforeEach(() => {
200
- resetTrustedFoldersForTesting();
201
- vi.spyOn(process, 'cwd').mockImplementation(() => mockCwd);
202
- vi.spyOn(fs, 'readFileSync').mockImplementation((p) => {
203
- if (p.toString() === getTrustedFoldersPath()) {
204
- return JSON.stringify(mockRules);
205
- }
206
- return '{}';
178
+ it('should report corrupted config via coreEvents.emitFeedback and still succeed', async () => {
179
+ // Initialize with valid JSON
180
+ fs.writeFileSync(trustedFoldersPath, '{}', 'utf-8');
181
+ const loadedFolders = loadTrustedFolders();
182
+ // Corrupt the file after initial load
183
+ fs.writeFileSync(trustedFoldersPath, 'invalid json', 'utf-8');
184
+ await loadedFolders.setValue('/new/path', TrustLevel.TRUST_FOLDER);
185
+ expect(coreEvents.emitFeedback).toHaveBeenCalledWith('error', expect.stringContaining('may be corrupted'), expect.any(Error));
186
+ // Should have overwritten the corrupted file with new valid config
187
+ const content = fs.readFileSync(trustedFoldersPath, 'utf-8');
188
+ const config = JSON.parse(content);
189
+ expect(config).toEqual({ '/new/path': TrustLevel.TRUST_FOLDER });
207
190
  });
208
- vi.spyOn(fs, 'existsSync').mockImplementation((p) => p.toString() === getTrustedFoldersPath());
209
191
  });
210
- afterEach(() => {
211
- vi.restoreAllMocks();
212
- // Clear the object
213
- Object.keys(mockRules).forEach((key) => delete mockRules[key]);
214
- });
215
- it('should throw a fatal error if the config is malformed', () => {
216
- mockCwd = '/home/user/projectA';
217
- // This mock needs to be specific to this test to override the one in beforeEach
218
- vi.spyOn(fs, 'readFileSync').mockImplementation((p) => {
219
- if (p.toString() === getTrustedFoldersPath()) {
220
- return '{"foo": "bar",}'; // Malformed JSON with trailing comma
221
- }
222
- return '{}';
223
- });
224
- expect(() => isWorkspaceTrusted(mockSettings)).toThrow(FatalConfigError);
225
- expect(() => isWorkspaceTrusted(mockSettings)).toThrow(/Please fix the configuration file/);
226
- });
227
- it('should throw a fatal error if the config is not a JSON object', () => {
228
- mockCwd = '/home/user/projectA';
229
- vi.spyOn(fs, 'readFileSync').mockImplementation((p) => {
230
- if (p.toString() === getTrustedFoldersPath()) {
231
- return 'null';
232
- }
233
- return '{}';
234
- });
235
- expect(() => isWorkspaceTrusted(mockSettings)).toThrow(FatalConfigError);
236
- expect(() => isWorkspaceTrusted(mockSettings)).toThrow(/not a valid JSON object/);
237
- });
238
- it('should return true for a directly trusted folder', () => {
239
- mockCwd = '/home/user/projectA';
240
- mockRules['/home/user/projectA'] = TrustLevel.TRUST_FOLDER;
241
- expect(isWorkspaceTrusted(mockSettings)).toEqual({
242
- isTrusted: true,
243
- source: 'file',
244
- });
245
- });
246
- it('should return true for a child of a trusted folder', () => {
247
- mockCwd = '/home/user/projectA/src';
248
- mockRules['/home/user/projectA'] = TrustLevel.TRUST_FOLDER;
249
- expect(isWorkspaceTrusted(mockSettings)).toEqual({
250
- isTrusted: true,
251
- source: 'file',
252
- });
253
- });
254
- it('should return true for a child of a trusted parent folder', () => {
255
- mockCwd = '/home/user/projectB';
256
- mockRules['/home/user/projectB/somefile.txt'] = TrustLevel.TRUST_PARENT;
257
- expect(isWorkspaceTrusted(mockSettings)).toEqual({
258
- isTrusted: true,
259
- source: 'file',
260
- });
261
- });
262
- it('should return false for a directly untrusted folder', () => {
263
- mockCwd = '/home/user/untrusted';
264
- mockRules['/home/user/untrusted'] = TrustLevel.DO_NOT_TRUST;
265
- expect(isWorkspaceTrusted(mockSettings)).toEqual({
266
- isTrusted: false,
267
- source: 'file',
268
- });
269
- });
270
- it('should return false for a child of an untrusted folder', () => {
271
- mockCwd = '/home/user/untrusted/src';
272
- mockRules['/home/user/untrusted'] = TrustLevel.DO_NOT_TRUST;
273
- expect(isWorkspaceTrusted(mockSettings).isTrusted).toBe(false);
274
- });
275
- it('should return undefined when no rules match', () => {
276
- mockCwd = '/home/user/other';
277
- mockRules['/home/user/projectA'] = TrustLevel.TRUST_FOLDER;
278
- mockRules['/home/user/untrusted'] = TrustLevel.DO_NOT_TRUST;
279
- expect(isWorkspaceTrusted(mockSettings).isTrusted).toBeUndefined();
280
- });
281
- it('should prioritize specific distrust over parent trust', () => {
282
- mockCwd = '/home/user/projectA/untrusted';
283
- mockRules['/home/user/projectA'] = TrustLevel.TRUST_FOLDER;
284
- mockRules['/home/user/projectA/untrusted'] = TrustLevel.DO_NOT_TRUST;
285
- expect(isWorkspaceTrusted(mockSettings)).toEqual({
286
- isTrusted: false,
287
- source: 'file',
288
- });
289
- });
290
- it('should use workspaceDir instead of process.cwd() when provided', () => {
291
- mockCwd = '/home/user/untrusted';
292
- const workspaceDir = '/home/user/projectA';
293
- mockRules['/home/user/projectA'] = TrustLevel.TRUST_FOLDER;
294
- mockRules['/home/user/untrusted'] = TrustLevel.DO_NOT_TRUST;
295
- // process.cwd() is untrusted, but workspaceDir is trusted
296
- expect(isWorkspaceTrusted(mockSettings, workspaceDir)).toEqual({
297
- isTrusted: true,
298
- source: 'file',
299
- });
300
- });
301
- it('should handle path normalization', () => {
302
- mockCwd = '/home/user/projectA';
303
- mockRules[`/home/user/../user/${path.basename('/home/user/projectA')}`] =
304
- TrustLevel.TRUST_FOLDER;
305
- expect(isWorkspaceTrusted(mockSettings)).toEqual({
306
- isTrusted: true,
307
- source: 'file',
308
- });
309
- });
310
- });
311
- describe('isWorkspaceTrusted with IDE override', () => {
312
- const mockCwd = '/home/user/projectA';
313
- beforeEach(() => {
314
- resetTrustedFoldersForTesting();
315
- vi.spyOn(process, 'cwd').mockImplementation(() => mockCwd);
316
- vi.spyOn(fs, 'realpathSync').mockImplementation((p) => p.toString());
317
- vi.spyOn(fs, 'existsSync').mockImplementation((p) => p.toString().endsWith('trustedFolders.json') ? false : true);
318
- });
319
- afterEach(() => {
320
- vi.clearAllMocks();
321
- ideContextStore.clear();
322
- resetTrustedFoldersForTesting();
323
- });
324
- const mockSettings = {
325
- security: {
326
- folderTrust: {
327
- enabled: true,
328
- },
329
- },
330
- };
331
- it('should return true when ideTrust is true, ignoring config', () => {
332
- ideContextStore.set({ workspaceState: { isTrusted: true } });
333
- // Even if config says don't trust, ideTrust should win.
334
- vi.spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify({ [process.cwd()]: TrustLevel.DO_NOT_TRUST }));
335
- expect(isWorkspaceTrusted(mockSettings)).toEqual({
336
- isTrusted: true,
337
- source: 'ide',
338
- });
339
- });
340
- it('should return false when ideTrust is false, ignoring config', () => {
341
- ideContextStore.set({ workspaceState: { isTrusted: false } });
342
- // Even if config says trust, ideTrust should win.
343
- vi.spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify({ [process.cwd()]: TrustLevel.TRUST_FOLDER }));
344
- expect(isWorkspaceTrusted(mockSettings)).toEqual({
345
- isTrusted: false,
346
- source: 'ide',
347
- });
348
- });
349
- it('should fall back to config when ideTrust is undefined', () => {
350
- vi.spyOn(fs, 'existsSync').mockImplementation((p) => p === getTrustedFoldersPath() || p === mockCwd ? true : false);
351
- vi.spyOn(fs, 'readFileSync').mockImplementation((p) => {
352
- if (p === getTrustedFoldersPath()) {
353
- return JSON.stringify({ [mockCwd]: TrustLevel.TRUST_FOLDER });
354
- }
355
- return '{}';
356
- });
357
- expect(isWorkspaceTrusted(mockSettings)).toEqual({
358
- isTrusted: true,
359
- source: 'file',
360
- });
361
- });
362
- it('should always return true if folderTrust setting is disabled', () => {
363
- const settings = {
364
- security: {
365
- folderTrust: {
366
- enabled: false,
367
- },
368
- },
369
- };
370
- ideContextStore.set({ workspaceState: { isTrusted: false } });
371
- expect(isWorkspaceTrusted(settings)).toEqual({
372
- isTrusted: true,
373
- source: undefined,
374
- });
375
- });
376
- });
377
- describe('Trusted Folders Caching', () => {
378
- beforeEach(() => {
379
- resetTrustedFoldersForTesting();
380
- vi.spyOn(fs, 'existsSync').mockReturnValue(true);
381
- vi.spyOn(fs, 'readFileSync').mockReturnValue('{}');
382
- vi.spyOn(fs, 'realpathSync').mockImplementation((p) => p.toString());
383
- });
384
- afterEach(() => {
385
- vi.restoreAllMocks();
386
- });
387
- it('should cache the loaded folders object', () => {
388
- const readSpy = vi.spyOn(fs, 'readFileSync');
389
- // First call should read the file
390
- loadTrustedFolders();
391
- expect(readSpy).toHaveBeenCalledTimes(1);
392
- // Second call should use the cache
393
- loadTrustedFolders();
394
- expect(readSpy).toHaveBeenCalledTimes(1);
395
- // Resetting should clear the cache
396
- resetTrustedFoldersForTesting();
397
- // Third call should read the file again
398
- loadTrustedFolders();
399
- expect(readSpy).toHaveBeenCalledTimes(2);
400
- });
401
- });
402
- describe('invalid trust levels', () => {
403
- const mockCwd = '/user/folder';
404
- const mockRules = {};
405
- beforeEach(() => {
406
- resetTrustedFoldersForTesting();
407
- vi.spyOn(process, 'cwd').mockImplementation(() => mockCwd);
408
- vi.spyOn(fs, 'realpathSync').mockImplementation((p) => p.toString());
409
- vi.spyOn(fs, 'readFileSync').mockImplementation((p) => {
410
- if (p.toString() === getTrustedFoldersPath()) {
411
- return JSON.stringify(mockRules);
412
- }
413
- return '{}';
414
- });
415
- vi.spyOn(fs, 'existsSync').mockImplementation((p) => p.toString() === getTrustedFoldersPath() || p.toString() === mockCwd);
416
- });
417
- afterEach(() => {
418
- vi.restoreAllMocks();
419
- // Clear the object
420
- Object.keys(mockRules).forEach((key) => delete mockRules[key]);
421
- });
422
- it('should create a comprehensive error message for invalid trust level', () => {
423
- mockRules[mockCwd] = 'INVALID_TRUST_LEVEL';
424
- const { errors } = loadTrustedFolders();
425
- const possibleValues = Object.values(TrustLevel).join(', ');
426
- expect(errors.length).toBe(1);
427
- expect(errors[0].message).toBe(`Invalid trust level "INVALID_TRUST_LEVEL" for path "${mockCwd}". Possible values are: ${possibleValues}.`);
428
- });
429
- it('should throw a fatal error for invalid trust level', () => {
192
+ describe('isWorkspaceTrusted Integration', () => {
430
193
  const mockSettings = {
431
194
  security: {
432
195
  folderTrust: {
@@ -434,193 +197,83 @@ describe('invalid trust levels', () => {
434
197
  },
435
198
  },
436
199
  };
437
- mockRules[mockCwd] = 'INVALID_TRUST_LEVEL';
438
- expect(() => isWorkspaceTrusted(mockSettings)).toThrow(FatalConfigError);
439
- });
440
- });
441
- describe('Verification: Auth and Trust Interaction', () => {
442
- let mockCwd;
443
- const mockRules = {};
444
- beforeEach(() => {
445
- vi.stubEnv('GEMINI_API_KEY', '');
446
- resetTrustedFoldersForTesting();
447
- vi.spyOn(process, 'cwd').mockImplementation(() => mockCwd);
448
- vi.spyOn(fs, 'readFileSync').mockImplementation((p) => {
449
- if (p === getTrustedFoldersPath()) {
450
- return JSON.stringify(mockRules);
451
- }
452
- if (p === path.resolve(mockCwd, '.env')) {
453
- return 'GEMINI_API_KEY=shhh-secret';
454
- }
455
- return '{}';
200
+ it('should return true for a directly trusted folder', () => {
201
+ const config = { '/projectA': TrustLevel.TRUST_FOLDER };
202
+ fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
203
+ expect(isWorkspaceTrusted(mockSettings, '/projectA')).toEqual({
204
+ isTrusted: true,
205
+ source: 'file',
206
+ });
456
207
  });
457
- vi.spyOn(fs, 'existsSync').mockImplementation((p) => p === getTrustedFoldersPath() || p === path.resolve(mockCwd, '.env'));
458
- });
459
- afterEach(() => {
460
- vi.unstubAllEnvs();
461
- Object.keys(mockRules).forEach((key) => delete mockRules[key]);
462
- });
463
- it('should verify loadEnvironment returns early and validateAuthMethod fails when untrusted', () => {
464
- // 1. Mock untrusted workspace
465
- mockCwd = '/home/user/untrusted';
466
- mockRules[mockCwd] = TrustLevel.DO_NOT_TRUST;
467
- // 2. Load environment (should return early)
468
- const settings = createMockSettings({
469
- security: { folderTrust: { enabled: true } },
208
+ it('should return false for a directly untrusted folder', () => {
209
+ const config = { '/untrusted': TrustLevel.DO_NOT_TRUST };
210
+ fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
211
+ expect(isWorkspaceTrusted(mockSettings, '/untrusted')).toEqual({
212
+ isTrusted: false,
213
+ source: 'file',
214
+ });
470
215
  });
471
- loadEnvironment(settings.merged, mockCwd);
472
- // 3. Verify env var NOT loaded
473
- expect(process.env['GEMINI_API_KEY']).toBe('');
474
- // 4. Verify validateAuthMethod fails
475
- const result = validateAuthMethod(AuthType.USE_GEMINI);
476
- expect(result).toContain('you must specify the GEMINI_API_KEY environment variable');
477
- });
478
- it('should identify if sandbox flag is available in Settings', () => {
479
- const schema = getSettingsSchema();
480
- expect(schema.tools.properties).toBeDefined();
481
- expect('sandbox' in schema.tools.properties).toBe(true);
482
- });
483
- });
484
- describe('Trusted Folders realpath caching', () => {
485
- beforeEach(() => {
486
- resetTrustedFoldersForTesting();
487
- vi.resetAllMocks();
488
- vi.spyOn(fs, 'realpathSync').mockImplementation((p) => p.toString());
489
- });
490
- afterEach(() => {
491
- vi.restoreAllMocks();
492
- });
493
- it('should only call fs.realpathSync once for the same path', () => {
494
- const mockPath = '/some/path';
495
- const mockRealPath = '/real/path';
496
- vi.spyOn(fs, 'existsSync').mockReturnValue(true);
497
- const realpathSpy = vi
498
- .spyOn(fs, 'realpathSync')
499
- .mockReturnValue(mockRealPath);
500
- vi.spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify({
501
- [mockPath]: TrustLevel.TRUST_FOLDER,
502
- '/another/path': TrustLevel.TRUST_FOLDER,
503
- }));
504
- const folders = loadTrustedFolders();
505
- // Call isPathTrusted multiple times with the same path
506
- folders.isPathTrusted(mockPath);
507
- folders.isPathTrusted(mockPath);
508
- folders.isPathTrusted(mockPath);
509
- // fs.realpathSync should only be called once for mockPath (at the start of isPathTrusted)
510
- // And once for each rule in the config (if they are different)
511
- // Let's check calls for mockPath
512
- const mockPathCalls = realpathSpy.mock.calls.filter((call) => call[0] === mockPath);
513
- expect(mockPathCalls.length).toBe(1);
514
- });
515
- it('should cache results for rule paths in the loop', () => {
516
- const rulePath = '/rule/path';
517
- const locationPath = '/location/path';
518
- vi.spyOn(fs, 'existsSync').mockReturnValue(true);
519
- const realpathSpy = vi
520
- .spyOn(fs, 'realpathSync')
521
- .mockImplementation((p) => p.toString()); // identity for simplicity
522
- vi.spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify({
523
- [rulePath]: TrustLevel.TRUST_FOLDER,
524
- }));
525
- const folders = loadTrustedFolders();
526
- // First call
527
- folders.isPathTrusted(locationPath);
528
- const firstCallCount = realpathSpy.mock.calls.length;
529
- expect(firstCallCount).toBe(2); // locationPath and rulePath
530
- // Second call with same location and same config
531
- folders.isPathTrusted(locationPath);
532
- const secondCallCount = realpathSpy.mock.calls.length;
533
- // Should still be 2 because both were cached
534
- expect(secondCallCount).toBe(2);
535
- });
536
- });
537
- describe('isWorkspaceTrusted with Symlinks', () => {
538
- const mockSettings = {
539
- security: {
540
- folderTrust: {
541
- enabled: true,
542
- },
543
- },
544
- };
545
- beforeEach(() => {
546
- resetTrustedFoldersForTesting();
547
- vi.resetAllMocks();
548
- vi.spyOn(fs, 'realpathSync').mockImplementation((p) => p.toString());
549
- });
550
- afterEach(() => {
551
- vi.restoreAllMocks();
552
- });
553
- it('should trust a folder even if CWD is a symlink and rule is realpath', () => {
554
- const symlinkPath = '/var/folders/project';
555
- const realPath = '/private/var/folders/project';
556
- vi.spyOn(process, 'cwd').mockReturnValue(symlinkPath);
557
- // Mock fs.existsSync to return true for trust config and both paths
558
- vi.spyOn(fs, 'existsSync').mockImplementation((p) => {
559
- const pathStr = p.toString();
560
- if (pathStr === getTrustedFoldersPath())
561
- return true;
562
- if (pathStr === symlinkPath)
563
- return true;
564
- if (pathStr === realPath)
565
- return true;
566
- return false;
216
+ it('should return undefined when no rules match', () => {
217
+ fs.writeFileSync(trustedFoldersPath, '{}', 'utf-8');
218
+ expect(isWorkspaceTrusted(mockSettings, '/other').isTrusted).toBeUndefined();
567
219
  });
568
- // Mock realpathSync to resolve symlink to realpath
569
- vi.spyOn(fs, 'realpathSync').mockImplementation((p) => {
570
- const pathStr = p.toString();
571
- if (pathStr === symlinkPath)
572
- return realPath;
573
- if (pathStr === realPath)
574
- return realPath;
575
- return pathStr;
220
+ it('should prioritize IDE override over file config', () => {
221
+ const config = { '/projectA': TrustLevel.DO_NOT_TRUST };
222
+ fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
223
+ ideContextStore.set({ workspaceState: { isTrusted: true } });
224
+ try {
225
+ expect(isWorkspaceTrusted(mockSettings, '/projectA')).toEqual({
226
+ isTrusted: true,
227
+ source: 'ide',
228
+ });
229
+ }
230
+ finally {
231
+ ideContextStore.clear();
232
+ }
576
233
  });
577
- // Rule is saved with realpath
578
- const mockRules = {
579
- [realPath]: TrustLevel.TRUST_FOLDER,
580
- };
581
- vi.spyOn(fs, 'readFileSync').mockImplementation((p) => {
582
- if (p.toString() === getTrustedFoldersPath())
583
- return JSON.stringify(mockRules);
584
- return '{}';
234
+ it('should always return true if folderTrust setting is disabled', () => {
235
+ const disabledSettings = {
236
+ security: { folderTrust: { enabled: false } },
237
+ };
238
+ expect(isWorkspaceTrusted(disabledSettings, '/any')).toEqual({
239
+ isTrusted: true,
240
+ source: undefined,
241
+ });
585
242
  });
586
- // Should be trusted because both resolve to the same realpath
587
- expect(isWorkspaceTrusted(mockSettings).isTrusted).toBe(true);
588
243
  });
589
- it('should trust a folder even if CWD is realpath and rule is a symlink', () => {
590
- const symlinkPath = '/var/folders/project';
591
- const realPath = '/private/var/folders/project';
592
- vi.spyOn(process, 'cwd').mockReturnValue(realPath);
593
- // Mock fs.existsSync
594
- vi.spyOn(fs, 'existsSync').mockImplementation((p) => {
595
- const pathStr = p.toString();
596
- if (pathStr === getTrustedFoldersPath())
597
- return true;
598
- if (pathStr === symlinkPath)
599
- return true;
600
- if (pathStr === realPath)
601
- return true;
602
- return false;
603
- });
604
- // Mock realpathSync
605
- vi.spyOn(fs, 'realpathSync').mockImplementation((p) => {
606
- const pathStr = p.toString();
607
- if (pathStr === symlinkPath)
608
- return realPath;
609
- if (pathStr === realPath)
610
- return realPath;
611
- return pathStr;
244
+ describe('Symlinks Support', () => {
245
+ it('should trust a folder if the rule matches the realpath', () => {
246
+ // Create a real directory and a symlink
247
+ const realDir = path.join(tempDir, 'real');
248
+ const symlinkDir = path.join(tempDir, 'symlink');
249
+ fs.mkdirSync(realDir);
250
+ fs.symlinkSync(realDir, symlinkDir);
251
+ // Rule uses realpath
252
+ const config = { [realDir]: TrustLevel.TRUST_FOLDER };
253
+ fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
254
+ // Check against symlink path
255
+ expect(isWorkspaceTrusted(mockSettings, symlinkDir).isTrusted).toBe(true);
612
256
  });
613
- // Rule is saved with symlink path
614
- const mockRules = {
615
- [symlinkPath]: TrustLevel.TRUST_FOLDER,
257
+ const mockSettings = {
258
+ security: { folderTrust: { enabled: true } },
616
259
  };
617
- vi.spyOn(fs, 'readFileSync').mockImplementation((p) => {
618
- if (p.toString() === getTrustedFoldersPath())
619
- return JSON.stringify(mockRules);
620
- return '{}';
260
+ });
261
+ describe('Verification: Auth and Trust Interaction', () => {
262
+ it('should verify loadEnvironment returns early when untrusted', () => {
263
+ const untrustedDir = path.join(tempDir, 'untrusted');
264
+ fs.mkdirSync(untrustedDir);
265
+ const config = { [untrustedDir]: TrustLevel.DO_NOT_TRUST };
266
+ fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8');
267
+ const envPath = path.join(untrustedDir, '.env');
268
+ fs.writeFileSync(envPath, 'GEMINI_API_KEY=secret', 'utf-8');
269
+ vi.stubEnv('GEMINI_API_KEY', '');
270
+ const settings = createMockSettings({
271
+ security: { folderTrust: { enabled: true } },
272
+ });
273
+ loadEnvironment(settings.merged, untrustedDir);
274
+ expect(process.env['GEMINI_API_KEY']).toBe('');
275
+ vi.unstubAllEnvs();
621
276
  });
622
- // Should be trusted because both resolve to the same realpath
623
- expect(isWorkspaceTrusted(mockSettings).isTrusted).toBe(true);
624
277
  });
625
278
  });
626
279
  //# sourceMappingURL=trustedFolders.test.js.map