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

@@ -9,9 +9,8 @@ import { HybridTokenStorage } from '../mcp/token-storage/hybrid-token-storage.js
import { OAUTH_FILE } from '../config/storage.js';
import type { OAuthCredentials } from '../mcp/token-storage/types.js';
import * as path from 'node:path';
import * as os from 'node:os';
import { promises as fs } from 'node:fs';
import { GEMINI_DIR } from '../utils/paths.js';
import { GEMINI_DIR, homedir } from '../utils/paths.js';
import { coreEvents } from '../utils/events.js';
const KEYCHAIN_SERVICE_NAME = 'gemini-cli-oauth';
@@ -91,7 +90,7 @@ export class OAuthCredentialStorage {
await this.storage.deleteCredentials(MAIN_ACCOUNT_KEY);
// Also try to remove the old file if it exists
const oldFilePath = path.join(os.homedir(), GEMINI_DIR, OAUTH_FILE);
const oldFilePath = path.join(homedir(), GEMINI_DIR, OAUTH_FILE);
await fs.rm(oldFilePath, { force: true }).catch(() => {});
} catch (error: unknown) {
coreEvents.emitFeedback(
@@ -107,7 +106,7 @@ export class OAuthCredentialStorage {
* Migrate credentials from old file-based storage to keychain
*/
private static async migrateFromFileStorage(): Promise<Credentials | null> {
const oldFilePath = path.join(os.homedir(), GEMINI_DIR, OAUTH_FILE);
const oldFilePath = path.join(homedir(), GEMINI_DIR, OAUTH_FILE);
let credsJson: string;
try {

View File

@@ -26,16 +26,24 @@ import { AuthType } from '../core/contentGenerator.js';
import type { Config } from '../config/config.js';
import readline from 'node:readline';
import { FORCE_ENCRYPTED_FILE_ENV_VAR } from '../mcp/token-storage/index.js';
import { GEMINI_DIR } from '../utils/paths.js';
import { GEMINI_DIR, homedir as pathsHomedir } from '../utils/paths.js';
import { debugLogger } from '../utils/debugLogger.js';
import { writeToStdout } from '../utils/stdio.js';
import { FatalCancellationError } from '../utils/errors.js';
import process from 'node:process';
vi.mock('os', async (importOriginal) => {
const os = await importOriginal<typeof import('os')>();
vi.mock('node:os', async (importOriginal) => {
const actual = await importOriginal<typeof import('node:os')>();
return {
...os,
...actual,
homedir: vi.fn(),
};
});
vi.mock('../utils/paths.js', async (importOriginal) => {
const actual = await importOriginal<typeof import('../utils/paths.js')>();
return {
...actual,
homedir: vi.fn(),
};
});
@@ -89,6 +97,7 @@ describe('oauth2', () => {
path.join(os.tmpdir(), 'gemini-cli-test-home-'),
);
vi.mocked(os.homedir).mockReturnValue(tempHomeDir);
vi.mocked(pathsHomedir).mockReturnValue(tempHomeDir);
});
afterEach(() => {
fs.rmSync(tempHomeDir, { recursive: true, force: true });
@@ -1129,15 +1138,10 @@ describe('oauth2', () => {
() => mockHttpServer as unknown as http.Server,
);
// Mock process.on to immediately trigger SIGINT
// Mock process.on to capture SIGINT handler
const processOnSpy = vi
.spyOn(process, 'on')
.mockImplementation((event, listener: () => void) => {
if (event === 'SIGINT') {
listener();
}
return process;
});
.mockImplementation(() => process);
const processRemoveListenerSpy = vi.spyOn(process, 'removeListener');
@@ -1146,6 +1150,24 @@ describe('oauth2', () => {
mockConfig,
);
// Wait for the SIGINT handler to be registered
let sigIntHandler: (() => void) | undefined;
await vi.waitFor(() => {
const sigintCall = processOnSpy.mock.calls.find(
(call) => call[0] === 'SIGINT',
);
sigIntHandler = sigintCall?.[1] as (() => void) | undefined;
if (!sigIntHandler)
throw new Error('SIGINT handler not registered yet');
});
expect(sigIntHandler).toBeDefined();
// Trigger SIGINT
if (sigIntHandler) {
sigIntHandler();
}
await expect(clientPromise).rejects.toThrow(FatalCancellationError);
expect(processRemoveListenerSpy).toHaveBeenCalledWith(
'SIGINT',
@@ -1180,17 +1202,10 @@ describe('oauth2', () => {
() => mockHttpServer as unknown as http.Server,
);
// Spy on process.stdin.on and immediately trigger Ctrl+C
// Spy on process.stdin.on to capture data handler
const stdinOnSpy = vi
.spyOn(process.stdin, 'on')
.mockImplementation(
(event: string, listener: (data: Buffer) => void) => {
if (event === 'data') {
listener(Buffer.from([0x03]));
}
return process.stdin;
},
);
.mockImplementation(() => process.stdin);
const stdinRemoveListenerSpy = vi.spyOn(
process.stdin,
@@ -1202,6 +1217,23 @@ describe('oauth2', () => {
mockConfig,
);
// Wait for the stdin handler to be registered
let dataHandler: ((data: Buffer) => void) | undefined;
await vi.waitFor(() => {
const dataCall = stdinOnSpy.mock.calls.find(
(call: [string, ...unknown[]]) => call[0] === 'data',
);
dataHandler = dataCall?.[1] as ((data: Buffer) => void) | undefined;
if (!dataHandler) throw new Error('stdin handler not registered yet');
});
expect(dataHandler).toBeDefined();
// Trigger Ctrl+C
if (dataHandler) {
dataHandler(Buffer.from([0x03]));
}
await expect(clientPromise).rejects.toThrow(FatalCancellationError);
expect(stdinRemoveListenerSpy).toHaveBeenCalledWith(
'data',
@@ -1302,7 +1334,8 @@ describe('oauth2', () => {
tempHomeDir = fs.mkdtempSync(
path.join(os.tmpdir(), 'gemini-cli-test-home-'),
);
(os.homedir as Mock).mockReturnValue(tempHomeDir);
vi.mocked(os.homedir).mockReturnValue(tempHomeDir);
vi.mocked(pathsHomedir).mockReturnValue(tempHomeDir);
});
afterEach(() => {

View File

@@ -336,7 +336,7 @@ async function initOauthClient(
// Note that SIGINT might not get raised on Ctrl+C in raw mode
// so we also need to look for Ctrl+C directly in stdin.
stdinHandler = (data) => {
stdinHandler = (data: Buffer) => {
if (data.includes(0x03)) {
reject(
new FatalCancellationError('Authentication cancelled by user.'),