Introduce GEMINI_CLI_HOME for strict test isolation (#15907)

This commit is contained in:
N. Taylor Mullen
2026-01-06 20:09:39 -08:00
committed by GitHub
parent a26463b056
commit 7956eb239e
54 changed files with 455 additions and 148 deletions

View File

@@ -13,6 +13,10 @@ vi.mock('node:os', () => ({
homedir: vi.fn(),
}));
vi.mock('@google/gemini-cli-core', () => ({
homedir: () => os.homedir(),
}));
describe('resolvePath', () => {
beforeEach(() => {
vi.mocked(os.homedir).mockReturnValue('/home/user');

View File

@@ -4,8 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
import * as os from 'node:os';
import * as path from 'node:path';
import { homedir } from '@google/gemini-cli-core';
export function resolvePath(p: string): string {
if (!p) {
@@ -13,9 +13,9 @@ export function resolvePath(p: string): string {
}
let expandedPath = p;
if (p.toLowerCase().startsWith('%userprofile%')) {
expandedPath = os.homedir() + p.substring('%userprofile%'.length);
expandedPath = homedir() + p.substring('%userprofile%'.length);
} else if (p === '~' || p.startsWith('~/')) {
expandedPath = os.homedir() + p.substring(1);
expandedPath = homedir() + p.substring(1);
}
return path.normalize(expandedPath);
}

View File

@@ -12,9 +12,19 @@ import { start_sandbox } from './sandbox.js';
import { FatalSandboxError, type SandboxConfig } from '@google/gemini-cli-core';
import { EventEmitter } from 'node:events';
vi.mock('../config/settings.js', () => ({
USER_SETTINGS_DIR: '/home/user/.gemini',
const { mockedHomedir, mockedGetContainerPath } = vi.hoisted(() => ({
mockedHomedir: vi.fn().mockReturnValue('/home/user'),
mockedGetContainerPath: vi.fn().mockImplementation((p: string) => p),
}));
vi.mock('./sandboxUtils.js', async (importOriginal) => {
const actual = await importOriginal<typeof import('./sandboxUtils.js')>();
return {
...actual,
getContainerPath: mockedGetContainerPath,
};
});
vi.mock('node:child_process');
vi.mock('node:os');
vi.mock('node:fs');
@@ -44,6 +54,7 @@ vi.mock('node:util', async (importOriginal) => {
},
};
});
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
@@ -64,7 +75,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
}
},
GEMINI_DIR: '.gemini',
USER_SETTINGS_DIR: '/home/user/.gemini',
homedir: mockedHomedir,
};
});
@@ -341,13 +352,23 @@ describe('sandbox', () => {
await start_sandbox(config);
expect(spawn).toHaveBeenCalledWith(
// The first call is 'docker images -q ...'
expect(spawn).toHaveBeenNthCalledWith(
1,
'docker',
expect.arrayContaining(['images', '-q']),
);
// The second call is 'docker run ...'
expect(spawn).toHaveBeenNthCalledWith(
2,
'docker',
expect.arrayContaining([
'run',
'--volume',
'/host/path:/container/path:ro',
'--volume',
expect.stringContaining('/home/user/.gemini'),
expect.stringMatching(/[\\/]home[\\/]user[\\/]\.gemini/),
]),
expect.any(Object),
);

View File

@@ -5,12 +5,11 @@
*/
import { exec, execSync, spawn, type ChildProcess } from 'node:child_process';
import os from 'node:os';
import path from 'node:path';
import fs from 'node:fs';
import os from 'node:os';
import { fileURLToPath } from 'node:url';
import { quote, parse } from 'shell-quote';
import { USER_SETTINGS_DIR } from '../config/settings.js';
import { promisify } from 'node:util';
import type { Config, SandboxConfig } from '@google/gemini-cli-core';
import {
@@ -18,6 +17,7 @@ import {
debugLogger,
FatalSandboxError,
GEMINI_DIR,
homedir,
} from '@google/gemini-cli-core';
import { ConsolePatcher } from '../ui/utils/ConsolePatcher.js';
import { randomBytes } from 'node:crypto';
@@ -82,7 +82,7 @@ export async function start_sandbox(
'-D',
`TMP_DIR=${fs.realpathSync(os.tmpdir())}`,
'-D',
`HOME_DIR=${fs.realpathSync(os.homedir())}`,
`HOME_DIR=${fs.realpathSync(homedir())}`,
'-D',
`CACHE_DIR=${fs.realpathSync((await execAsync('getconf DARWIN_USER_CACHE_DIR')).stdout.trim())}`,
];
@@ -288,18 +288,23 @@ export async function start_sandbox(
// mount user settings directory inside container, after creating if missing
// note user/home changes inside sandbox and we mount at BOTH paths for consistency
const userSettingsDirOnHost = USER_SETTINGS_DIR;
const userHomeDirOnHost = homedir();
const userSettingsDirInSandbox = getContainerPath(
`/home/node/${GEMINI_DIR}`,
);
if (!fs.existsSync(userSettingsDirOnHost)) {
fs.mkdirSync(userSettingsDirOnHost);
if (!fs.existsSync(userHomeDirOnHost)) {
fs.mkdirSync(userHomeDirOnHost, { recursive: true });
}
const userSettingsDirOnHost = path.join(userHomeDirOnHost, GEMINI_DIR);
if (!fs.existsSync(userSettingsDirOnHost)) {
fs.mkdirSync(userSettingsDirOnHost, { recursive: true });
}
args.push(
'--volume',
`${userSettingsDirOnHost}:${userSettingsDirInSandbox}`,
);
if (userSettingsDirInSandbox !== userSettingsDirOnHost) {
if (userSettingsDirInSandbox !== getContainerPath(userSettingsDirOnHost)) {
args.push(
'--volume',
`${userSettingsDirOnHost}:${getContainerPath(userSettingsDirOnHost)}`,
@@ -309,8 +314,16 @@ export async function start_sandbox(
// mount os.tmpdir() as os.tmpdir() inside container
args.push('--volume', `${os.tmpdir()}:${getContainerPath(os.tmpdir())}`);
// mount homedir() as homedir() inside container
if (userHomeDirOnHost !== os.homedir()) {
args.push(
'--volume',
`${userHomeDirOnHost}:${getContainerPath(userHomeDirOnHost)}`,
);
}
// mount gcloud config directory if it exists
const gcloudConfigDir = path.join(os.homedir(), '.config', 'gcloud');
const gcloudConfigDir = path.join(homedir(), '.config', 'gcloud');
if (fs.existsSync(gcloudConfigDir)) {
args.push(
'--volume',
@@ -585,7 +598,7 @@ export async function start_sandbox(
// necessary on Linux to ensure the user exists within the
// container's /etc/passwd file, which is required by os.userInfo().
const username = 'gemini';
const homeDir = getContainerPath(os.homedir());
const homeDir = getContainerPath(homedir());
const setupUserCommands = [
// Use -f with groupadd to avoid errors if the group already exists.
@@ -606,7 +619,7 @@ export async function start_sandbox(
// We still need userFlag for the simpler proxy container, which does not have this issue.
userFlag = `--user ${uid}:${gid}`;
// When forcing a UID in the sandbox, $HOME can be reset to '/', so we copy $HOME as well.
args.push('--env', `HOME=${os.homedir()}`);
args.push('--env', `HOME=${homedir()}`);
}
// push container image name

View File

@@ -19,6 +19,15 @@ vi.mock('os', async (importOriginal) => {
};
});
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...actual,
homedir: () => os.homedir(),
};
});
describe('getUserStartupWarnings', () => {
let testRootDir: string;
let homeDir: string;

View File

@@ -5,8 +5,9 @@
*/
import fs from 'node:fs/promises';
import * as os from 'node:os';
import path from 'node:path';
import process from 'node:process';
import { homedir } from '@google/gemini-cli-core';
type WarningCheck = {
id: string;
@@ -20,7 +21,7 @@ const homeDirectoryCheck: WarningCheck = {
try {
const [workspaceRealPath, homeRealPath] = await Promise.all([
fs.realpath(workspaceRoot),
fs.realpath(os.homedir()),
fs.realpath(homedir()),
]);
if (workspaceRealPath === homeRealPath) {