Download packages/cli/src/config/trustedFolders.test.ts from SaylorTwift/gemini-cli: direct link, hf CLI and curl.
- Browser
- Download file 18.5 kB
-
https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/cli/src/config/trustedFolders.test.ts
- Command line
-
hf download hf://SaylorTwift/gemini-cli/packages/cli/src/config/trustedFolders.test.ts
-
curl -L -o trustedFolders.test.ts https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/cli/src/config/trustedFolders.test.ts
18.5 kB
| /** | |
| * @license | |
| * Copyright 2025 Google LLC | |
| * SPDX-License-Identifier: Apache-2.0 | |
| */ | |
| import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; | |
| import * as fs from 'node:fs'; | |
| import * as path from 'node:path'; | |
| import * as os from 'node:os'; | |
| import { | |
| FatalConfigError, | |
| ideContextStore, | |
| normalizePath, | |
| } from '@google/gemini-cli-core'; | |
| import { | |
| loadTrustedFolders, | |
| TrustLevel, | |
| isWorkspaceTrusted, | |
| resetTrustedFoldersForTesting, | |
| } from './trustedFolders.js'; | |
| import { loadEnvironment, type Settings } from './settings.js'; | |
| import { createMockSettings } from '../test-utils/settings.js'; | |
| // We explicitly do NOT mock 'fs' or 'proper-lockfile' here to ensure | |
| // we are testing the actual behavior on the real file system. | |
| vi.mock('@google/gemini-cli-core', async (importOriginal) => { | |
| const actual = | |
| await importOriginal<typeof import('@google/gemini-cli-core')>(); | |
| return { | |
| ...actual, | |
| homedir: () => '/mock/home/user', | |
| isHeadlessMode: vi.fn(() => false), | |
| coreEvents: Object.assign( | |
| Object.create(Object.getPrototypeOf(actual.coreEvents)), | |
| actual.coreEvents, | |
| { | |
| emitFeedback: vi.fn(), | |
| }, | |
| ), | |
| FatalConfigError: actual.FatalConfigError, | |
| }; | |
| }); | |
| describe('Trusted Folders', () => { | |
| let tempDir: string; | |
| let trustedFoldersPath: string; | |
| beforeEach(() => { | |
| // Create a temporary directory for each test | |
| tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gemini-cli-test-')); | |
| trustedFoldersPath = path.join(tempDir, 'trustedFolders.json'); | |
| // Set the environment variable to point to the temp file | |
| vi.stubEnv('GEMINI_CLI_TRUSTED_FOLDERS_PATH', trustedFoldersPath); | |
| // Reset the internal state | |
| resetTrustedFoldersForTesting(); | |
| vi.clearAllMocks(); | |
| delete process.env['GEMINI_CLI_TRUST_WORKSPACE']; | |
| }); | |
| afterEach(() => { | |
| // Clean up the temporary directory | |
| fs.rmSync(tempDir, { recursive: true, force: true }); | |
| vi.unstubAllEnvs(); | |
| }); | |
| describe('Locking & Concurrency', () => { | |
| it('setValue should handle concurrent calls correctly using real lockfile', async () => { | |
| // Initialize the file | |
| fs.writeFileSync(trustedFoldersPath, '{}', 'utf-8'); | |
| const loadedFolders = loadTrustedFolders(); | |
| // Start two concurrent calls | |
| // These will race to acquire the lock on the real file system | |
| const p1 = loadedFolders.setValue( | |
| path.resolve('/path1'), | |
| TrustLevel.TRUST_FOLDER, | |
| ); | |
| const p2 = loadedFolders.setValue( | |
| path.resolve('/path2'), | |
| TrustLevel.TRUST_FOLDER, | |
| ); | |
| await Promise.all([p1, p2]); | |
| // Verify final state in the file | |
| const content = fs.readFileSync(trustedFoldersPath, 'utf-8'); | |
| const config = JSON.parse(content); | |
| expect(config).toEqual({ | |
| [normalizePath('/path1')]: TrustLevel.TRUST_FOLDER, | |
| [normalizePath('/path2')]: TrustLevel.TRUST_FOLDER, | |
| }); | |
| }); | |
| }); | |
| describe('Loading & Parsing', () => { | |
| it('should load empty rules if no files exist', () => { | |
| const { rules, errors } = loadTrustedFolders(); | |
| expect(rules).toEqual([]); | |
| expect(errors).toEqual([]); | |
| }); | |
| it('should load rules from the configuration file', () => { | |
| const config = { | |
| [normalizePath('/user/folder')]: TrustLevel.TRUST_FOLDER, | |
| }; | |
| fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8'); | |
| const { rules, errors } = loadTrustedFolders(); | |
| expect(rules).toEqual([ | |
| { | |
| path: normalizePath('/user/folder'), | |
| trustLevel: TrustLevel.TRUST_FOLDER, | |
| }, | |
| ]); | |
| expect(errors).toEqual([]); | |
| }); | |
| it('should handle JSON parsing errors gracefully', () => { | |
| fs.writeFileSync(trustedFoldersPath, 'invalid json', 'utf-8'); | |
| const { rules, errors } = loadTrustedFolders(); | |
| expect(rules).toEqual([]); | |
| expect(errors.length).toBe(1); | |
| expect(errors[0].path).toBe(trustedFoldersPath); | |
| expect(errors[0].message).toContain('Unexpected token'); | |
| }); | |
| it('should handle non-object JSON gracefully', () => { | |
| fs.writeFileSync(trustedFoldersPath, 'null', 'utf-8'); | |
| const { rules, errors } = loadTrustedFolders(); | |
| expect(rules).toEqual([]); | |
| expect(errors.length).toBe(1); | |
| expect(errors[0].message).toContain('not a valid JSON object'); | |
| }); | |
| it('should handle invalid trust levels gracefully', () => { | |
| const config = { | |
| '/path': 'INVALID_LEVEL', | |
| }; | |
| fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8'); | |
| const { rules, errors } = loadTrustedFolders(); | |
| expect(rules).toEqual([]); | |
| expect(errors.length).toBe(1); | |
| expect(errors[0].message).toContain( | |
| 'Invalid trust level "INVALID_LEVEL"', | |
| ); | |
| }); | |
| it('should support JSON with comments', () => { | |
| const content = ` | |
| { | |
| // This is a comment | |
| "${normalizePath('/path').replaceAll('\\', '\\\\')}": "TRUST_FOLDER" | |
| } | |
| `; | |
| fs.writeFileSync(trustedFoldersPath, content, 'utf-8'); | |
| const { rules, errors } = loadTrustedFolders(); | |
| expect(rules).toEqual([ | |
| { path: normalizePath('/path'), trustLevel: TrustLevel.TRUST_FOLDER }, | |
| ]); | |
| expect(errors).toEqual([]); | |
| }); | |
| }); | |
| describe('isPathTrusted', () => { | |
| function setup(config: Record<string, TrustLevel>) { | |
| fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8'); | |
| return loadTrustedFolders(); | |
| } | |
| it('provides a method to determine if a path is trusted', () => { | |
| const folders = setup({ | |
| './myfolder': TrustLevel.TRUST_FOLDER, | |
| '/trustedparent/trustme': TrustLevel.TRUST_PARENT, | |
| '/user/folder': TrustLevel.TRUST_FOLDER, | |
| '/secret': TrustLevel.DO_NOT_TRUST, | |
| '/secret/publickeys': TrustLevel.TRUST_FOLDER, | |
| }); | |
| // We need to resolve relative paths for comparison since the implementation uses realpath | |
| const resolvedMyFolder = path.resolve('./myfolder'); | |
| expect(folders.isPathTrusted('/secret')).toBe(false); | |
| expect(folders.isPathTrusted('/user/folder')).toBe(true); | |
| expect(folders.isPathTrusted('/secret/publickeys/public.pem')).toBe(true); | |
| expect(folders.isPathTrusted('/user/folder/harhar')).toBe(true); | |
| expect( | |
| folders.isPathTrusted(path.join(resolvedMyFolder, 'somefile.jpg')), | |
| ).toBe(true); | |
| expect(folders.isPathTrusted('/trustedparent/someotherfolder')).toBe( | |
| true, | |
| ); | |
| expect(folders.isPathTrusted('/trustedparent/trustme')).toBe(true); | |
| // No explicit rule covers this file | |
| expect(folders.isPathTrusted('/secret/bankaccounts.json')).toBe(false); | |
| expect(folders.isPathTrusted('/secret/mine/privatekey.pem')).toBe(false); | |
| expect(folders.isPathTrusted('/user/someotherfolder')).toBe(undefined); | |
| }); | |
| it('prioritizes the longest matching path (precedence)', () => { | |
| const folders = setup({ | |
| '/a': TrustLevel.TRUST_FOLDER, | |
| '/a/b': TrustLevel.DO_NOT_TRUST, | |
| '/a/b/c': TrustLevel.TRUST_FOLDER, | |
| '/parent/trustme': TrustLevel.TRUST_PARENT, | |
| '/parent/trustme/butnotthis': TrustLevel.DO_NOT_TRUST, | |
| }); | |
| expect(folders.isPathTrusted('/a/b/c/d')).toBe(true); | |
| expect(folders.isPathTrusted('/a/b/x')).toBe(false); | |
| expect(folders.isPathTrusted('/a/x')).toBe(true); | |
| expect(folders.isPathTrusted('/parent/trustme/butnotthis/file')).toBe( | |
| false, | |
| ); | |
| expect(folders.isPathTrusted('/parent/other')).toBe(true); | |
| }); | |
| }); | |
| describe('setValue', () => { | |
| it('should update the user config and save it atomically', async () => { | |
| fs.writeFileSync(trustedFoldersPath, '{}', 'utf-8'); | |
| const loadedFolders = loadTrustedFolders(); | |
| await loadedFolders.setValue( | |
| normalizePath('/new/path'), | |
| TrustLevel.TRUST_FOLDER, | |
| ); | |
| expect(loadedFolders.user.config[normalizePath('/new/path')]).toBe( | |
| TrustLevel.TRUST_FOLDER, | |
| ); | |
| const content = fs.readFileSync(trustedFoldersPath, 'utf-8'); | |
| const config = JSON.parse(content); | |
| expect(config[normalizePath('/new/path')]).toBe(TrustLevel.TRUST_FOLDER); | |
| }); | |
| it('should throw FatalConfigError if there were load errors', async () => { | |
| fs.writeFileSync(trustedFoldersPath, 'invalid json', 'utf-8'); | |
| const loadedFolders = loadTrustedFolders(); | |
| expect(loadedFolders.errors.length).toBe(1); | |
| await expect( | |
| loadedFolders.setValue('/some/path', TrustLevel.TRUST_FOLDER), | |
| ).rejects.toThrow(FatalConfigError); | |
| }); | |
| }); | |
| describe('isWorkspaceTrusted Integration', () => { | |
| const mockSettings: Settings = { | |
| security: { | |
| folderTrust: { | |
| enabled: true, | |
| }, | |
| }, | |
| }; | |
| it('should return true for a directly trusted folder', () => { | |
| const config = { '/projectA': TrustLevel.TRUST_FOLDER }; | |
| fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8'); | |
| expect(isWorkspaceTrusted(mockSettings, '/projectA')).toEqual({ | |
| isTrusted: true, | |
| source: 'file', | |
| }); | |
| }); | |
| it('should return true for a child of a trusted folder', () => { | |
| const config = { '/projectA': TrustLevel.TRUST_FOLDER }; | |
| fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8'); | |
| expect(isWorkspaceTrusted(mockSettings, '/projectA/src')).toEqual({ | |
| isTrusted: true, | |
| source: 'file', | |
| }); | |
| }); | |
| it('should return true for a child of a trusted parent folder', () => { | |
| const config = { '/projectB/somefile.txt': TrustLevel.TRUST_PARENT }; | |
| fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8'); | |
| expect(isWorkspaceTrusted(mockSettings, '/projectB')).toEqual({ | |
| isTrusted: true, | |
| source: 'file', | |
| }); | |
| }); | |
| it('should return false for a directly untrusted folder', () => { | |
| const config = { '/untrusted': TrustLevel.DO_NOT_TRUST }; | |
| fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8'); | |
| expect(isWorkspaceTrusted(mockSettings, '/untrusted')).toEqual({ | |
| isTrusted: false, | |
| source: 'file', | |
| }); | |
| }); | |
| it('should return false for a child of an untrusted folder', () => { | |
| const config = { '/untrusted': TrustLevel.DO_NOT_TRUST }; | |
| fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8'); | |
| expect(isWorkspaceTrusted(mockSettings, '/untrusted/src').isTrusted).toBe( | |
| false, | |
| ); | |
| }); | |
| it('should return undefined when no rules match', () => { | |
| fs.writeFileSync(trustedFoldersPath, '{}', 'utf-8'); | |
| expect( | |
| isWorkspaceTrusted(mockSettings, '/other').isTrusted, | |
| ).toBeUndefined(); | |
| }); | |
| it('should prioritize specific distrust over parent trust', () => { | |
| const config = { | |
| '/projectA': TrustLevel.TRUST_FOLDER, | |
| '/projectA/untrusted': TrustLevel.DO_NOT_TRUST, | |
| }; | |
| fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8'); | |
| expect(isWorkspaceTrusted(mockSettings, '/projectA/untrusted')).toEqual({ | |
| isTrusted: false, | |
| source: 'file', | |
| }); | |
| }); | |
| it('should use workspaceDir instead of process.cwd() when provided', () => { | |
| const config = { | |
| '/projectA': TrustLevel.TRUST_FOLDER, | |
| '/untrusted': TrustLevel.DO_NOT_TRUST, | |
| }; | |
| fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8'); | |
| vi.spyOn(process, 'cwd').mockImplementation(() => '/untrusted'); | |
| // process.cwd() is untrusted, but workspaceDir is trusted | |
| expect(isWorkspaceTrusted(mockSettings, '/projectA')).toEqual({ | |
| isTrusted: true, | |
| source: 'file', | |
| }); | |
| }); | |
| it('should handle path normalization', () => { | |
| const config = { '/home/user/projectA': TrustLevel.TRUST_FOLDER }; | |
| fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8'); | |
| expect( | |
| isWorkspaceTrusted(mockSettings, '/home/user/../user/projectA'), | |
| ).toEqual({ | |
| isTrusted: true, | |
| source: 'file', | |
| }); | |
| }); | |
| it('should prioritize IDE override over file config', () => { | |
| const config = { '/projectA': TrustLevel.DO_NOT_TRUST }; | |
| fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8'); | |
| ideContextStore.set({ workspaceState: { isTrusted: true } }); | |
| try { | |
| expect(isWorkspaceTrusted(mockSettings, '/projectA')).toEqual({ | |
| isTrusted: true, | |
| source: 'ide', | |
| }); | |
| } finally { | |
| ideContextStore.clear(); | |
| } | |
| }); | |
| it('should return false when IDE override is false', () => { | |
| const config = { '/projectA': TrustLevel.TRUST_FOLDER }; | |
| fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8'); | |
| ideContextStore.set({ workspaceState: { isTrusted: false } }); | |
| try { | |
| expect(isWorkspaceTrusted(mockSettings, '/projectA')).toEqual({ | |
| isTrusted: false, | |
| source: 'ide', | |
| }); | |
| } finally { | |
| ideContextStore.clear(); | |
| } | |
| }); | |
| it('should throw FatalConfigError when the config file is invalid', () => { | |
| fs.writeFileSync(trustedFoldersPath, 'invalid json', 'utf-8'); | |
| expect(() => isWorkspaceTrusted(mockSettings, '/any')).toThrow( | |
| FatalConfigError, | |
| ); | |
| }); | |
| it('should always return true if folderTrust setting is disabled', () => { | |
| const disabledSettings: Settings = { | |
| security: { folderTrust: { enabled: false } }, | |
| }; | |
| expect(isWorkspaceTrusted(disabledSettings, '/any')).toEqual({ | |
| isTrusted: true, | |
| source: undefined, | |
| }); | |
| }); | |
| }); | |
| describe('isWorkspaceTrusted headless mode', () => { | |
| const mockSettings: Settings = { | |
| security: { | |
| folderTrust: { | |
| enabled: true, | |
| }, | |
| }, | |
| }; | |
| it('should NOT return true when isHeadlessMode is true, ignoring config', async () => { | |
| const geminiCore = await import('@google/gemini-cli-core'); | |
| vi.spyOn(geminiCore, 'isHeadlessMode').mockReturnValue(true); | |
| expect(isWorkspaceTrusted(mockSettings)).toEqual({ | |
| isTrusted: undefined, | |
| source: undefined, | |
| }); | |
| }); | |
| it('should return true when GEMINI_CLI_TRUST_WORKSPACE is true', async () => { | |
| process.env['GEMINI_CLI_TRUST_WORKSPACE'] = 'true'; | |
| try { | |
| expect(isWorkspaceTrusted(mockSettings)).toEqual({ | |
| isTrusted: true, | |
| source: 'env', | |
| }); | |
| } finally { | |
| delete process.env['GEMINI_CLI_TRUST_WORKSPACE']; | |
| } | |
| }); | |
| it('should fall back to config when isHeadlessMode is false', async () => { | |
| const geminiCore = await import('@google/gemini-cli-core'); | |
| vi.spyOn(geminiCore, 'isHeadlessMode').mockReturnValue(false); | |
| const config = { '/projectA': TrustLevel.DO_NOT_TRUST }; | |
| fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8'); | |
| expect(isWorkspaceTrusted(mockSettings, '/projectA').isTrusted).toBe( | |
| false, | |
| ); | |
| }); | |
| it('should return undefined for isPathTrusted when isHeadlessMode is true', async () => { | |
| const geminiCore = await import('@google/gemini-cli-core'); | |
| vi.spyOn(geminiCore, 'isHeadlessMode').mockReturnValue(true); | |
| const folders = loadTrustedFolders(); | |
| expect(folders.isPathTrusted('/any-untrusted-path')).toBe(undefined); | |
| }); | |
| }); | |
| describe('Trusted Folders Caching', () => { | |
| it('should cache the loaded folders object', () => { | |
| // First call should load and cache | |
| const folders1 = loadTrustedFolders(); | |
| // Second call should return the same instance from cache | |
| const folders2 = loadTrustedFolders(); | |
| expect(folders1).toBe(folders2); | |
| // Resetting should clear the cache | |
| resetTrustedFoldersForTesting(); | |
| // Third call should return a new instance | |
| const folders3 = loadTrustedFolders(); | |
| expect(folders3).not.toBe(folders1); | |
| }); | |
| }); | |
| describe('invalid trust levels', () => { | |
| it('should create a comprehensive error message for invalid trust level', () => { | |
| const config = { '/user/folder': 'INVALID_TRUST_LEVEL' }; | |
| fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8'); | |
| const { errors } = loadTrustedFolders(); | |
| const possibleValues = Object.values(TrustLevel).join(', '); | |
| expect(errors.length).toBe(1); | |
| expect(errors[0].message).toBe( | |
| `Invalid trust level "INVALID_TRUST_LEVEL" for path "/user/folder". Possible values are: ${possibleValues}.`, | |
| ); | |
| }); | |
| }); | |
| const itif = (condition: boolean) => (condition ? it : it.skip); | |
| describe('Symlinks Support', () => { | |
| const mockSettings: Settings = { | |
| security: { folderTrust: { enabled: true } }, | |
| }; | |
| // TODO: issue 19387 - Enable symlink tests on Windows | |
| itif(process.platform !== 'win32')( | |
| 'should trust a folder if the rule matches the realpath', | |
| () => { | |
| // Create a real directory and a symlink | |
| const realDir = path.join(tempDir, 'real'); | |
| const symlinkDir = path.join(tempDir, 'symlink'); | |
| fs.mkdirSync(realDir); | |
| fs.symlinkSync(realDir, symlinkDir, 'dir'); | |
| // Rule uses realpath | |
| const config = { [realDir]: TrustLevel.TRUST_FOLDER }; | |
| fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8'); | |
| // Check against symlink path | |
| expect(isWorkspaceTrusted(mockSettings, symlinkDir).isTrusted).toBe( | |
| true, | |
| ); | |
| }, | |
| ); | |
| }); | |
| describe('Verification: Auth and Trust Interaction', () => { | |
| it('should verify loadEnvironment returns early when untrusted', () => { | |
| const untrustedDir = path.join(tempDir, 'untrusted'); | |
| fs.mkdirSync(untrustedDir); | |
| const config = { [untrustedDir]: TrustLevel.DO_NOT_TRUST }; | |
| fs.writeFileSync(trustedFoldersPath, JSON.stringify(config), 'utf-8'); | |
| const envPath = path.join(untrustedDir, '.env'); | |
| fs.writeFileSync(envPath, 'GEMINI_API_KEY=secret', 'utf-8'); | |
| vi.stubEnv('GEMINI_API_KEY', ''); | |
| const settings = createMockSettings({ | |
| security: { folderTrust: { enabled: true } }, | |
| }); | |
| loadEnvironment(settings.merged, untrustedDir); | |
| expect(process.env['GEMINI_API_KEY']).toBe(''); | |
| vi.unstubAllEnvs(); | |
| }); | |
| }); | |
| }); | |