mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-28 14:04:41 -07:00
fix(core): enhance sandbox usability and fix build error (#24460)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import {
|
||||
getProactiveToolSuggestions,
|
||||
isNetworkReliantCommand,
|
||||
} from './proactivePermissions.js';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
|
||||
vi.mock('node:os');
|
||||
vi.mock('node:fs', () => ({
|
||||
default: {
|
||||
promises: {
|
||||
access: vi.fn(),
|
||||
},
|
||||
constants: {
|
||||
F_OK: 0,
|
||||
},
|
||||
},
|
||||
promises: {
|
||||
access: vi.fn(),
|
||||
},
|
||||
constants: {
|
||||
F_OK: 0,
|
||||
},
|
||||
}));
|
||||
|
||||
describe('proactivePermissions', () => {
|
||||
const homeDir = '/Users/testuser';
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(os.homedir).mockReturnValue(homeDir);
|
||||
vi.mocked(os.platform).mockReturnValue('darwin');
|
||||
});
|
||||
|
||||
describe('isNetworkReliantCommand', () => {
|
||||
it('should return true for always-network tools', () => {
|
||||
expect(isNetworkReliantCommand('ssh')).toBe(true);
|
||||
expect(isNetworkReliantCommand('git')).toBe(true);
|
||||
expect(isNetworkReliantCommand('curl')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for network-heavy node subcommands', () => {
|
||||
expect(isNetworkReliantCommand('npm', 'install')).toBe(true);
|
||||
expect(isNetworkReliantCommand('yarn', 'add')).toBe(true);
|
||||
expect(isNetworkReliantCommand('bun', '')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for local node subcommands', () => {
|
||||
expect(isNetworkReliantCommand('npm', 'test')).toBe(false);
|
||||
expect(isNetworkReliantCommand('yarn', 'run')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for unknown tools', () => {
|
||||
expect(isNetworkReliantCommand('ls')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getProactiveToolSuggestions', () => {
|
||||
it('should return undefined for unknown tools', async () => {
|
||||
expect(await getProactiveToolSuggestions('ls')).toBeUndefined();
|
||||
expect(await getProactiveToolSuggestions('node')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return permissions for npm if paths exist', async () => {
|
||||
vi.mocked(fs.promises.access).mockImplementation(
|
||||
(p: fs.PathLike, _mode?: number) => {
|
||||
const pathStr = p.toString();
|
||||
if (
|
||||
pathStr === path.join(homeDir, '.npm') ||
|
||||
pathStr === path.join(homeDir, '.cache') ||
|
||||
pathStr === path.join(homeDir, '.npmrc')
|
||||
) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(new Error('ENOENT'));
|
||||
},
|
||||
);
|
||||
|
||||
const permissions = await getProactiveToolSuggestions('npm');
|
||||
expect(permissions).toBeDefined();
|
||||
expect(permissions?.network).toBe(true);
|
||||
// .npmrc should be read-only
|
||||
expect(permissions?.fileSystem?.read).toContain(
|
||||
path.join(homeDir, '.npmrc'),
|
||||
);
|
||||
expect(permissions?.fileSystem?.write).not.toContain(
|
||||
path.join(homeDir, '.npmrc'),
|
||||
);
|
||||
// .npm should be read-write
|
||||
expect(permissions?.fileSystem?.read).toContain(
|
||||
path.join(homeDir, '.npm'),
|
||||
);
|
||||
expect(permissions?.fileSystem?.write).toContain(
|
||||
path.join(homeDir, '.npm'),
|
||||
);
|
||||
// .cache should be read-write
|
||||
expect(permissions?.fileSystem?.write).toContain(
|
||||
path.join(homeDir, '.cache'),
|
||||
);
|
||||
// should NOT contain .ssh or .gitconfig for npm
|
||||
expect(permissions?.fileSystem?.read).not.toContain(
|
||||
path.join(homeDir, '.ssh'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should grant network access and suggest primary cache paths even if they do not exist', async () => {
|
||||
vi.mocked(fs.promises.access).mockRejectedValue(new Error('ENOENT'));
|
||||
const permissions = await getProactiveToolSuggestions('npm');
|
||||
expect(permissions).toBeDefined();
|
||||
expect(permissions?.network).toBe(true);
|
||||
expect(permissions?.fileSystem?.write).toContain(
|
||||
path.join(homeDir, '.npm'),
|
||||
);
|
||||
// .cache is optional and should NOT be included if it doesn't exist
|
||||
expect(permissions?.fileSystem?.write).not.toContain(
|
||||
path.join(homeDir, '.cache'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should suggest .ssh and .gitconfig only for git', async () => {
|
||||
vi.mocked(fs.promises.access).mockImplementation(
|
||||
(p: fs.PathLike, _mode?: number) => {
|
||||
const pathStr = p.toString();
|
||||
if (
|
||||
pathStr === path.join(homeDir, '.ssh') ||
|
||||
pathStr === path.join(homeDir, '.gitconfig')
|
||||
) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(new Error('ENOENT'));
|
||||
},
|
||||
);
|
||||
|
||||
const permissions = await getProactiveToolSuggestions('git');
|
||||
expect(permissions?.network).toBe(true);
|
||||
expect(permissions?.fileSystem?.read).toContain(
|
||||
path.join(homeDir, '.ssh'),
|
||||
);
|
||||
expect(permissions?.fileSystem?.read).toContain(
|
||||
path.join(homeDir, '.gitconfig'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should suggest .ssh but NOT .gitconfig for ssh', async () => {
|
||||
vi.mocked(fs.promises.access).mockImplementation(
|
||||
(p: fs.PathLike, _mode?: number) => {
|
||||
const pathStr = p.toString();
|
||||
if (pathStr === path.join(homeDir, '.ssh')) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(new Error('ENOENT'));
|
||||
},
|
||||
);
|
||||
|
||||
const permissions = await getProactiveToolSuggestions('ssh');
|
||||
expect(permissions?.network).toBe(true);
|
||||
expect(permissions?.fileSystem?.read).toContain(
|
||||
path.join(homeDir, '.ssh'),
|
||||
);
|
||||
expect(permissions?.fileSystem?.read).not.toContain(
|
||||
path.join(homeDir, '.gitconfig'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle Windows specific paths', async () => {
|
||||
vi.mocked(os.platform).mockReturnValue('win32');
|
||||
const appData = 'C:\\Users\\testuser\\AppData\\Roaming';
|
||||
vi.stubEnv('AppData', appData);
|
||||
|
||||
vi.mocked(fs.promises.access).mockImplementation(
|
||||
(p: fs.PathLike, _mode?: number) => {
|
||||
const pathStr = p.toString();
|
||||
if (pathStr === path.join(appData, 'npm')) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(new Error('ENOENT'));
|
||||
},
|
||||
);
|
||||
|
||||
const permissions = await getProactiveToolSuggestions('npm.exe');
|
||||
expect(permissions).toBeDefined();
|
||||
expect(permissions?.fileSystem?.read).toContain(
|
||||
path.join(appData, 'npm'),
|
||||
);
|
||||
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it('should include bun, pnpm, and yarn specific paths', async () => {
|
||||
vi.mocked(fs.promises.access).mockResolvedValue(undefined);
|
||||
|
||||
const bun = await getProactiveToolSuggestions('bun');
|
||||
expect(bun?.fileSystem?.read).toContain(path.join(homeDir, '.bun'));
|
||||
expect(bun?.fileSystem?.read).not.toContain(path.join(homeDir, '.yarn'));
|
||||
|
||||
const yarn = await getProactiveToolSuggestions('yarn');
|
||||
expect(yarn?.fileSystem?.read).toContain(path.join(homeDir, '.yarn'));
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user