mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-26 04:54:25 -07:00
test: fix Windows CI execution and resolve exposed platform failures (#24476)
This commit is contained in:
@@ -74,8 +74,9 @@ describe('findSecretFiles', () => {
|
||||
});
|
||||
|
||||
it('should find secret files in the root directory', async () => {
|
||||
const workspace = path.resolve('/workspace');
|
||||
vi.mocked(fsPromises.readdir).mockImplementation(((dir: string) => {
|
||||
if (dir === '/workspace') {
|
||||
if (dir === workspace) {
|
||||
return Promise.resolve([
|
||||
{ name: '.env', isDirectory: () => false, isFile: () => true },
|
||||
{
|
||||
@@ -89,19 +90,20 @@ describe('findSecretFiles', () => {
|
||||
return Promise.resolve([] as unknown as fs.Dirent[]);
|
||||
}) as unknown as typeof fsPromises.readdir);
|
||||
|
||||
const secrets = await findSecretFiles('/workspace');
|
||||
expect(secrets).toEqual([path.join('/workspace', '.env')]);
|
||||
const secrets = await findSecretFiles(workspace);
|
||||
expect(secrets).toEqual([path.join(workspace, '.env')]);
|
||||
});
|
||||
|
||||
it('should NOT find secret files recursively (shallow scan only)', async () => {
|
||||
const workspace = path.resolve('/workspace');
|
||||
vi.mocked(fsPromises.readdir).mockImplementation(((dir: string) => {
|
||||
if (dir === '/workspace') {
|
||||
if (dir === workspace) {
|
||||
return Promise.resolve([
|
||||
{ name: '.env', isDirectory: () => false, isFile: () => true },
|
||||
{ name: 'packages', isDirectory: () => true, isFile: () => false },
|
||||
] as unknown as fs.Dirent[]);
|
||||
}
|
||||
if (dir === path.join('/workspace', 'packages')) {
|
||||
if (dir === path.join(workspace, 'packages')) {
|
||||
return Promise.resolve([
|
||||
{ name: '.env.local', isDirectory: () => false, isFile: () => true },
|
||||
] as unknown as fs.Dirent[]);
|
||||
@@ -109,12 +111,12 @@ describe('findSecretFiles', () => {
|
||||
return Promise.resolve([] as unknown as fs.Dirent[]);
|
||||
}) as unknown as typeof fsPromises.readdir);
|
||||
|
||||
const secrets = await findSecretFiles('/workspace');
|
||||
expect(secrets).toEqual([path.join('/workspace', '.env')]);
|
||||
const secrets = await findSecretFiles(workspace);
|
||||
expect(secrets).toEqual([path.join(workspace, '.env')]);
|
||||
// Should NOT have called readdir for subdirectories
|
||||
expect(fsPromises.readdir).toHaveBeenCalledTimes(1);
|
||||
expect(fsPromises.readdir).not.toHaveBeenCalledWith(
|
||||
path.join('/workspace', 'packages'),
|
||||
path.join(workspace, 'packages'),
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
@@ -169,98 +171,111 @@ describe('SandboxManager', () => {
|
||||
|
||||
it('should handle case sensitivity correctly per platform', () => {
|
||||
vi.spyOn(os, 'platform').mockReturnValue('win32');
|
||||
expect(getPathIdentity('/Workspace/Foo')).toBe('/workspace/foo');
|
||||
expect(getPathIdentity('/Workspace/Foo')).toBe(
|
||||
path.normalize('/workspace/foo'),
|
||||
);
|
||||
|
||||
vi.spyOn(os, 'platform').mockReturnValue('darwin');
|
||||
expect(getPathIdentity('/Tmp/Foo')).toBe('/tmp/foo');
|
||||
expect(getPathIdentity('/Tmp/Foo')).toBe(path.normalize('/tmp/foo'));
|
||||
|
||||
vi.spyOn(os, 'platform').mockReturnValue('linux');
|
||||
expect(getPathIdentity('/Tmp/Foo')).toBe('/Tmp/Foo');
|
||||
expect(getPathIdentity('/Tmp/Foo')).toBe(path.normalize('/Tmp/Foo'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveSandboxPaths', () => {
|
||||
it('should resolve allowed and forbidden paths', async () => {
|
||||
const workspace = path.resolve('/workspace');
|
||||
const forbidden = path.join(workspace, 'forbidden');
|
||||
const allowed = path.join(workspace, 'allowed');
|
||||
const options = {
|
||||
workspace: '/workspace',
|
||||
forbiddenPaths: async () => ['/workspace/forbidden'],
|
||||
workspace,
|
||||
forbiddenPaths: async () => [forbidden],
|
||||
};
|
||||
const req = {
|
||||
command: 'ls',
|
||||
args: [],
|
||||
cwd: '/workspace',
|
||||
cwd: workspace,
|
||||
env: {},
|
||||
policy: {
|
||||
allowedPaths: ['/workspace/allowed'],
|
||||
allowedPaths: [allowed],
|
||||
},
|
||||
};
|
||||
|
||||
const result = await resolveSandboxPaths(options, req as SandboxRequest);
|
||||
|
||||
expect(result.allowed).toEqual(['/workspace/allowed']);
|
||||
expect(result.forbidden).toEqual(['/workspace/forbidden']);
|
||||
expect(result.allowed).toEqual([allowed]);
|
||||
expect(result.forbidden).toEqual([forbidden]);
|
||||
});
|
||||
|
||||
it('should filter out workspace from allowed paths', async () => {
|
||||
const workspace = path.resolve('/workspace');
|
||||
const other = path.resolve('/other/path');
|
||||
const options = {
|
||||
workspace: '/workspace',
|
||||
workspace,
|
||||
};
|
||||
const req = {
|
||||
command: 'ls',
|
||||
args: [],
|
||||
cwd: '/workspace',
|
||||
cwd: workspace,
|
||||
env: {},
|
||||
policy: {
|
||||
allowedPaths: ['/workspace', '/workspace/', '/other/path'],
|
||||
allowedPaths: [workspace, workspace + path.sep, other],
|
||||
},
|
||||
};
|
||||
|
||||
const result = await resolveSandboxPaths(options, req as SandboxRequest);
|
||||
|
||||
expect(result.allowed).toEqual(['/other/path']);
|
||||
expect(result.allowed).toEqual([other]);
|
||||
});
|
||||
|
||||
it('should prioritize forbidden paths over allowed paths', async () => {
|
||||
const workspace = path.resolve('/workspace');
|
||||
const secret = path.join(workspace, 'secret');
|
||||
const normal = path.join(workspace, 'normal');
|
||||
const options = {
|
||||
workspace: '/workspace',
|
||||
forbiddenPaths: async () => ['/workspace/secret'],
|
||||
workspace,
|
||||
forbiddenPaths: async () => [secret],
|
||||
};
|
||||
const req = {
|
||||
command: 'ls',
|
||||
args: [],
|
||||
cwd: '/workspace',
|
||||
cwd: workspace,
|
||||
env: {},
|
||||
policy: {
|
||||
allowedPaths: ['/workspace/secret', '/workspace/normal'],
|
||||
allowedPaths: [secret, normal],
|
||||
},
|
||||
};
|
||||
|
||||
const result = await resolveSandboxPaths(options, req as SandboxRequest);
|
||||
|
||||
expect(result.allowed).toEqual(['/workspace/normal']);
|
||||
expect(result.forbidden).toEqual(['/workspace/secret']);
|
||||
expect(result.allowed).toEqual([normal]);
|
||||
expect(result.forbidden).toEqual([secret]);
|
||||
});
|
||||
|
||||
it('should handle case-insensitive conflicts on supported platforms', async () => {
|
||||
vi.spyOn(os, 'platform').mockReturnValue('darwin');
|
||||
const workspace = path.resolve('/workspace');
|
||||
const secretUpper = path.join(workspace, 'SECRET');
|
||||
const secretLower = path.join(workspace, 'secret');
|
||||
const options = {
|
||||
workspace: '/workspace',
|
||||
forbiddenPaths: async () => ['/workspace/SECRET'],
|
||||
workspace,
|
||||
forbiddenPaths: async () => [secretUpper],
|
||||
};
|
||||
const req = {
|
||||
command: 'ls',
|
||||
args: [],
|
||||
cwd: '/workspace',
|
||||
cwd: workspace,
|
||||
env: {},
|
||||
policy: {
|
||||
allowedPaths: ['/workspace/secret'],
|
||||
allowedPaths: [secretLower],
|
||||
},
|
||||
};
|
||||
|
||||
const result = await resolveSandboxPaths(options, req as SandboxRequest);
|
||||
|
||||
expect(result.allowed).toEqual([]);
|
||||
expect(result.forbidden).toEqual(['/workspace/SECRET']);
|
||||
expect(result.forbidden).toEqual([secretUpper]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -270,62 +285,69 @@ describe('SandboxManager', () => {
|
||||
});
|
||||
|
||||
it('should return the realpath if the file exists', async () => {
|
||||
vi.mocked(fsPromises.realpath).mockResolvedValue(
|
||||
'/real/path/to/file.txt' as never,
|
||||
);
|
||||
const result = await tryRealpath('/some/symlink/to/file.txt');
|
||||
expect(result).toBe('/real/path/to/file.txt');
|
||||
expect(fsPromises.realpath).toHaveBeenCalledWith(
|
||||
'/some/symlink/to/file.txt',
|
||||
);
|
||||
const realPath = path.resolve('/real/path/to/file.txt');
|
||||
const symlinkPath = path.resolve('/some/symlink/to/file.txt');
|
||||
vi.mocked(fsPromises.realpath).mockResolvedValue(realPath as never);
|
||||
const result = await tryRealpath(symlinkPath);
|
||||
expect(result).toBe(realPath);
|
||||
expect(fsPromises.realpath).toHaveBeenCalledWith(symlinkPath);
|
||||
});
|
||||
|
||||
it('should fallback to parent directory if file does not exist (ENOENT)', async () => {
|
||||
const nonexistent = path.resolve('/workspace/nonexistent.txt');
|
||||
const workspace = path.resolve('/workspace');
|
||||
const realWorkspace = path.resolve('/real/workspace');
|
||||
|
||||
vi.mocked(fsPromises.realpath).mockImplementation(((p: string) => {
|
||||
if (p === '/workspace/nonexistent.txt') {
|
||||
if (p === nonexistent) {
|
||||
return Promise.reject(
|
||||
Object.assign(new Error('ENOENT: no such file or directory'), {
|
||||
code: 'ENOENT',
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (p === '/workspace') {
|
||||
return Promise.resolve('/real/workspace');
|
||||
if (p === workspace) {
|
||||
return Promise.resolve(realWorkspace);
|
||||
}
|
||||
return Promise.reject(new Error(`Unexpected path: ${p}`));
|
||||
}) as never);
|
||||
|
||||
const result = await tryRealpath('/workspace/nonexistent.txt');
|
||||
const result = await tryRealpath(nonexistent);
|
||||
|
||||
// It should combine the real path of the parent with the original basename
|
||||
expect(result).toBe(path.join('/real/workspace', 'nonexistent.txt'));
|
||||
expect(result).toBe(path.join(realWorkspace, 'nonexistent.txt'));
|
||||
});
|
||||
|
||||
it('should recursively fallback up the directory tree on multiple ENOENT errors', async () => {
|
||||
const missingFile = path.resolve(
|
||||
'/workspace/missing_dir/missing_file.txt',
|
||||
);
|
||||
const missingDir = path.resolve('/workspace/missing_dir');
|
||||
const workspace = path.resolve('/workspace');
|
||||
const realWorkspace = path.resolve('/real/workspace');
|
||||
|
||||
vi.mocked(fsPromises.realpath).mockImplementation(((p: string) => {
|
||||
if (p === '/workspace/missing_dir/missing_file.txt') {
|
||||
if (p === missingFile) {
|
||||
return Promise.reject(
|
||||
Object.assign(new Error('ENOENT'), { code: 'ENOENT' }),
|
||||
);
|
||||
}
|
||||
if (p === '/workspace/missing_dir') {
|
||||
if (p === missingDir) {
|
||||
return Promise.reject(
|
||||
Object.assign(new Error('ENOENT'), { code: 'ENOENT' }),
|
||||
);
|
||||
}
|
||||
if (p === '/workspace') {
|
||||
return Promise.resolve('/real/workspace');
|
||||
if (p === workspace) {
|
||||
return Promise.resolve(realWorkspace);
|
||||
}
|
||||
return Promise.reject(new Error(`Unexpected path: ${p}`));
|
||||
}) as never);
|
||||
|
||||
const result = await tryRealpath(
|
||||
'/workspace/missing_dir/missing_file.txt',
|
||||
);
|
||||
const result = await tryRealpath(missingFile);
|
||||
|
||||
// It should resolve '/workspace' to '/real/workspace' and append the missing parts
|
||||
expect(result).toBe(
|
||||
path.join('/real/workspace', 'missing_dir', 'missing_file.txt'),
|
||||
path.join(realWorkspace, 'missing_dir', 'missing_file.txt'),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -340,6 +362,7 @@ describe('SandboxManager', () => {
|
||||
});
|
||||
|
||||
it('should throw an error if realpath fails with a non-ENOENT error (e.g. EACCES)', async () => {
|
||||
const secretFile = path.resolve('/secret/file.txt');
|
||||
vi.mocked(fsPromises.realpath).mockImplementation(() =>
|
||||
Promise.reject(
|
||||
Object.assign(new Error('EACCES: permission denied'), {
|
||||
@@ -348,7 +371,7 @@ describe('SandboxManager', () => {
|
||||
),
|
||||
);
|
||||
|
||||
await expect(tryRealpath('/secret/file.txt')).rejects.toThrow(
|
||||
await expect(tryRealpath(secretFile)).rejects.toThrow(
|
||||
'EACCES: permission denied',
|
||||
);
|
||||
});
|
||||
@@ -358,10 +381,11 @@ describe('SandboxManager', () => {
|
||||
const sandboxManager = new NoopSandboxManager();
|
||||
|
||||
it('should pass through the command and arguments unchanged', async () => {
|
||||
const cwd = path.resolve('/tmp');
|
||||
const req = {
|
||||
command: 'ls',
|
||||
args: ['-la'],
|
||||
cwd: '/tmp',
|
||||
cwd,
|
||||
env: { PATH: '/usr/bin' },
|
||||
};
|
||||
|
||||
@@ -372,10 +396,11 @@ describe('SandboxManager', () => {
|
||||
});
|
||||
|
||||
it('should sanitize the environment variables', async () => {
|
||||
const cwd = path.resolve('/tmp');
|
||||
const req = {
|
||||
command: 'echo',
|
||||
args: ['hello'],
|
||||
cwd: '/tmp',
|
||||
cwd,
|
||||
env: {
|
||||
PATH: '/usr/bin',
|
||||
GITHUB_TOKEN: 'ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
|
||||
@@ -398,10 +423,11 @@ describe('SandboxManager', () => {
|
||||
});
|
||||
|
||||
it('should allow disabling environment variable redaction if requested in config', async () => {
|
||||
const cwd = path.resolve('/tmp');
|
||||
const req = {
|
||||
command: 'echo',
|
||||
args: ['hello'],
|
||||
cwd: '/tmp',
|
||||
cwd,
|
||||
env: {
|
||||
API_KEY: 'sensitive-key',
|
||||
},
|
||||
@@ -419,10 +445,11 @@ describe('SandboxManager', () => {
|
||||
});
|
||||
|
||||
it('should respect allowedEnvironmentVariables in config but filter sensitive ones', async () => {
|
||||
const cwd = path.resolve('/tmp');
|
||||
const req = {
|
||||
command: 'echo',
|
||||
args: ['hello'],
|
||||
cwd: '/tmp',
|
||||
cwd,
|
||||
env: {
|
||||
MY_SAFE_VAR: 'safe-value',
|
||||
MY_TOKEN: 'secret-token',
|
||||
@@ -443,10 +470,11 @@ describe('SandboxManager', () => {
|
||||
});
|
||||
|
||||
it('should respect blockedEnvironmentVariables in config', async () => {
|
||||
const cwd = path.resolve('/tmp');
|
||||
const req = {
|
||||
command: 'echo',
|
||||
args: ['hello'],
|
||||
cwd: '/tmp',
|
||||
cwd,
|
||||
env: {
|
||||
SAFE_VAR: 'safe-value',
|
||||
BLOCKED_VAR: 'blocked-value',
|
||||
@@ -488,7 +516,7 @@ describe('SandboxManager', () => {
|
||||
it('should return NoopSandboxManager if sandboxing is disabled', () => {
|
||||
const manager = createSandboxManager(
|
||||
{ enabled: false },
|
||||
{ workspace: '/workspace' },
|
||||
{ workspace: path.resolve('/workspace') },
|
||||
);
|
||||
expect(manager).toBeInstanceOf(NoopSandboxManager);
|
||||
});
|
||||
@@ -503,7 +531,7 @@ describe('SandboxManager', () => {
|
||||
vi.spyOn(os, 'platform').mockReturnValue(platform);
|
||||
const manager = createSandboxManager(
|
||||
{ enabled: true },
|
||||
{ workspace: '/workspace' },
|
||||
{ workspace: path.resolve('/workspace') },
|
||||
);
|
||||
expect(manager).toBeInstanceOf(expected);
|
||||
},
|
||||
@@ -513,7 +541,7 @@ describe('SandboxManager', () => {
|
||||
vi.spyOn(os, 'platform').mockReturnValue('win32');
|
||||
const manager = createSandboxManager(
|
||||
{ enabled: true },
|
||||
{ workspace: '/workspace' },
|
||||
{ workspace: path.resolve('/workspace') },
|
||||
);
|
||||
expect(manager).toBeInstanceOf(WindowsSandboxManager);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user