|
4 | 4 | * SPDX-License-Identifier: Apache-2.0 |
5 | 5 | */ |
6 | 6 |
|
7 | | -import { vi, describe, it, expect, beforeEach, type Mock } from 'vitest'; |
| 7 | +import { |
| 8 | + vi, |
| 9 | + describe, |
| 10 | + it, |
| 11 | + expect, |
| 12 | + beforeEach, |
| 13 | + afterEach, |
| 14 | + type Mock, |
| 15 | +} from 'vitest'; |
8 | 16 | import EventEmitter from 'node:events'; |
9 | 17 | import type { Readable } from 'node:stream'; |
10 | 18 | import { type ChildProcess } from 'node:child_process'; |
@@ -1284,3 +1292,170 @@ describe('ShellExecutionService execution method selection', () => { |
1284 | 1292 | expect(result.executionMethod).toBe('child_process'); |
1285 | 1293 | }); |
1286 | 1294 | }); |
| 1295 | + |
| 1296 | +describe('ShellExecutionService environment variables', () => { |
| 1297 | + let mockPtyProcess: EventEmitter & { |
| 1298 | + pid: number; |
| 1299 | + kill: Mock; |
| 1300 | + onData: Mock; |
| 1301 | + onExit: Mock; |
| 1302 | + write: Mock; |
| 1303 | + resize: Mock; |
| 1304 | + }; |
| 1305 | + let mockChildProcess: EventEmitter & Partial<ChildProcess>; |
| 1306 | + |
| 1307 | + beforeEach(() => { |
| 1308 | + vi.clearAllMocks(); |
| 1309 | + vi.resetModules(); // Reset modules to ensure process.env changes are fresh |
| 1310 | + |
| 1311 | + // Mock for pty |
| 1312 | + mockPtyProcess = new EventEmitter() as EventEmitter & { |
| 1313 | + pid: number; |
| 1314 | + kill: Mock; |
| 1315 | + onData: Mock; |
| 1316 | + onExit: Mock; |
| 1317 | + write: Mock; |
| 1318 | + resize: Mock; |
| 1319 | + }; |
| 1320 | + mockPtyProcess.pid = 12345; |
| 1321 | + mockPtyProcess.kill = vi.fn(); |
| 1322 | + mockPtyProcess.onData = vi.fn(); |
| 1323 | + mockPtyProcess.onExit = vi.fn(); |
| 1324 | + mockPtyProcess.write = vi.fn(); |
| 1325 | + mockPtyProcess.resize = vi.fn(); |
| 1326 | + |
| 1327 | + mockPtySpawn.mockReturnValue(mockPtyProcess); |
| 1328 | + mockGetPty.mockResolvedValue({ |
| 1329 | + module: { spawn: mockPtySpawn }, |
| 1330 | + name: 'mock-pty', |
| 1331 | + }); |
| 1332 | + |
| 1333 | + // Mock for child_process |
| 1334 | + mockChildProcess = new EventEmitter() as EventEmitter & |
| 1335 | + Partial<ChildProcess>; |
| 1336 | + mockChildProcess.stdout = new EventEmitter() as Readable; |
| 1337 | + mockChildProcess.stderr = new EventEmitter() as Readable; |
| 1338 | + mockChildProcess.kill = vi.fn(); |
| 1339 | + Object.defineProperty(mockChildProcess, 'pid', { |
| 1340 | + value: 54321, |
| 1341 | + configurable: true, |
| 1342 | + }); |
| 1343 | + mockCpSpawn.mockReturnValue(mockChildProcess); |
| 1344 | + |
| 1345 | + // Default exit behavior for mocks |
| 1346 | + mockPtyProcess.onExit.mockImplementationOnce(({ exitCode, signal }) => { |
| 1347 | + // Small delay to allow async ops to complete |
| 1348 | + setTimeout(() => mockPtyProcess.emit('exit', { exitCode, signal }), 0); |
| 1349 | + }); |
| 1350 | + mockChildProcess.on('exit', (code, signal) => { |
| 1351 | + // Small delay to allow async ops to complete |
| 1352 | + setTimeout(() => mockChildProcess.emit('close', code, signal), 0); |
| 1353 | + }); |
| 1354 | + }); |
| 1355 | + |
| 1356 | + afterEach(() => { |
| 1357 | + // Clean up process.env after each test |
| 1358 | + vi.unstubAllEnvs(); |
| 1359 | + }); |
| 1360 | + |
| 1361 | + it('should use a sanitized environment when in a GitHub run', async () => { |
| 1362 | + // Mock the environment to simulate a GitHub Actions run |
| 1363 | + vi.stubEnv('GITHUB_SHA', 'test-sha'); |
| 1364 | + vi.stubEnv('MY_SENSITIVE_VAR', 'secret-value'); // This should be stripped out |
| 1365 | + vi.stubEnv('PATH', '/test/path'); // An essential var that should be kept |
| 1366 | + vi.stubEnv('GEMINI_CLI_TEST_VAR', 'test-value'); // A test var that should be kept |
| 1367 | + |
| 1368 | + vi.resetModules(); |
| 1369 | + const { ShellExecutionService } = await import( |
| 1370 | + './shellExecutionService.js' |
| 1371 | + ); |
| 1372 | + |
| 1373 | + // Test pty path |
| 1374 | + await ShellExecutionService.execute( |
| 1375 | + 'test-pty-command', |
| 1376 | + '/', |
| 1377 | + vi.fn(), |
| 1378 | + new AbortController().signal, |
| 1379 | + true, |
| 1380 | + shellExecutionConfig, |
| 1381 | + ); |
| 1382 | + |
| 1383 | + const ptyEnv = mockPtySpawn.mock.calls[0][2].env; |
| 1384 | + expect(ptyEnv).not.toHaveProperty('MY_SENSITIVE_VAR'); |
| 1385 | + expect(ptyEnv).toHaveProperty('PATH', '/test/path'); |
| 1386 | + expect(ptyEnv).toHaveProperty('GEMINI_CLI_TEST_VAR', 'test-value'); |
| 1387 | + |
| 1388 | + // Ensure pty process exits for next test |
| 1389 | + mockPtyProcess.onExit.mock.calls[0][0]({ exitCode: 0, signal: null }); |
| 1390 | + await new Promise(process.nextTick); |
| 1391 | + |
| 1392 | + // Test child_process path |
| 1393 | + mockGetPty.mockResolvedValue(null); // Force fallback |
| 1394 | + await ShellExecutionService.execute( |
| 1395 | + 'test-cp-command', |
| 1396 | + '/', |
| 1397 | + vi.fn(), |
| 1398 | + new AbortController().signal, |
| 1399 | + true, |
| 1400 | + shellExecutionConfig, |
| 1401 | + ); |
| 1402 | + |
| 1403 | + const cpEnv = mockCpSpawn.mock.calls[0][2].env; |
| 1404 | + expect(cpEnv).not.toHaveProperty('MY_SENSITIVE_VAR'); |
| 1405 | + expect(cpEnv).toHaveProperty('PATH', '/test/path'); |
| 1406 | + expect(cpEnv).toHaveProperty('GEMINI_CLI_TEST_VAR', 'test-value'); |
| 1407 | + |
| 1408 | + // Ensure child_process exits |
| 1409 | + mockChildProcess.emit('exit', 0, null); |
| 1410 | + mockChildProcess.emit('close', 0, null); |
| 1411 | + await new Promise(process.nextTick); |
| 1412 | + }); |
| 1413 | + |
| 1414 | + it('should include the full process.env when not in a GitHub run', async () => { |
| 1415 | + vi.stubEnv('MY_TEST_VAR', 'test-value'); |
| 1416 | + vi.stubEnv('GITHUB_SHA', ''); |
| 1417 | + vi.stubEnv('SURFACE', ''); |
| 1418 | + vi.resetModules(); |
| 1419 | + const { ShellExecutionService } = await import( |
| 1420 | + './shellExecutionService.js' |
| 1421 | + ); |
| 1422 | + |
| 1423 | + // Test pty path |
| 1424 | + await ShellExecutionService.execute( |
| 1425 | + 'test-pty-command-no-github', |
| 1426 | + '/', |
| 1427 | + vi.fn(), |
| 1428 | + new AbortController().signal, |
| 1429 | + true, |
| 1430 | + shellExecutionConfig, |
| 1431 | + ); |
| 1432 | + expect(mockPtySpawn).toHaveBeenCalled(); |
| 1433 | + const ptyEnv = mockPtySpawn.mock.calls[0][2].env; |
| 1434 | + expect(ptyEnv).toHaveProperty('MY_TEST_VAR', 'test-value'); |
| 1435 | + expect(ptyEnv).toHaveProperty('GEMINI_CLI', '1'); |
| 1436 | + |
| 1437 | + // Ensure pty process exits |
| 1438 | + mockPtyProcess.onExit.mock.calls[0][0]({ exitCode: 0, signal: null }); |
| 1439 | + await new Promise(process.nextTick); |
| 1440 | + |
| 1441 | + // Test child_process path (forcing fallback by making pty unavailable) |
| 1442 | + mockGetPty.mockResolvedValue(null); |
| 1443 | + await ShellExecutionService.execute( |
| 1444 | + 'test-cp-command-no-github', |
| 1445 | + '/', |
| 1446 | + vi.fn(), |
| 1447 | + new AbortController().signal, |
| 1448 | + true, // Still tries pty, but it will fall back |
| 1449 | + shellExecutionConfig, |
| 1450 | + ); |
| 1451 | + expect(mockCpSpawn).toHaveBeenCalled(); |
| 1452 | + const cpEnv = mockCpSpawn.mock.calls[0][2].env; |
| 1453 | + expect(cpEnv).toHaveProperty('MY_TEST_VAR', 'test-value'); |
| 1454 | + expect(cpEnv).toHaveProperty('GEMINI_CLI', '1'); |
| 1455 | + |
| 1456 | + // Ensure child_process exits |
| 1457 | + mockChildProcess.emit('exit', 0, null); |
| 1458 | + mockChildProcess.emit('close', 0, null); |
| 1459 | + await new Promise(process.nextTick); |
| 1460 | + }); |
| 1461 | +}); |
0 commit comments