fix(policy): refactor policy dialog to remove process.exit and fix integration tests

- Refactored `PolicyUpdateDialog` to remove side effects (`process.exit`, `relaunchApp`) and delegate logic to parent.
- Updated `AppContainer` to handle relaunch logic.
- Added comprehensive unit tests for `PolicyUpdateDialog`.
- Fixed `project-policy-cli.test.ts` to correctly mock `PolicyIntegrityManager`.
- Fixed typo in `packages/core/src/policy/config.ts`.
This commit is contained in:
Abhijit Balaji
2026-02-13 16:11:42 -08:00
parent c73e47bbbe
commit 73b3cb86eb
5 changed files with 136 additions and 40 deletions

View File

@@ -32,6 +32,14 @@ vi.mock('@google/gemini-cli-core', async () => {
checkers: [],
}),
getVersion: vi.fn().mockResolvedValue('test-version'),
PolicyIntegrityManager: vi.fn().mockImplementation(() => ({
checkIntegrity: vi.fn().mockResolvedValue({
status: 'match', // IntegrityStatus.MATCH
hash: 'test-hash',
fileCount: 1,
}),
})),
IntegrityStatus: { MATCH: 'match', NEW: 'new', MISMATCH: 'mismatch' },
};
});

View File

@@ -122,7 +122,7 @@ import { appEvents, AppEvent, TransientMessageType } from '../utils/events.js';
import { type UpdateObject } from './utils/updateCheck.js';
import { setUpdateHandler } from '../utils/handleAutoUpdate.js';
import { registerCleanup, runExitCleanup } from '../utils/cleanup.js';
import { RELAUNCH_EXIT_CODE } from '../utils/processUtils.js';
import { RELAUNCH_EXIT_CODE, relaunchApp } from '../utils/processUtils.js';
import type { SessionInfo } from '../utils/sessionUtils.js';
import { useMessageQueue } from './hooks/useMessageQueue.js';
import { useMcpStatus } from './hooks/useMcpStatus.js';
@@ -1462,6 +1462,10 @@ Logging in with Google... Restarting Gemini CLI to continue.
policyUpdateConfirmationRequest.newHash,
);
setIsRestartingPolicyUpdate(true);
// Give time for the UI to render the restarting message
setTimeout(async () => {
await relaunchApp();
}, 250);
} else {
setIsPolicyUpdateDialogOpen(false);
}

View File

@@ -0,0 +1,120 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, afterEach } from 'vitest';
import { act } from 'react';
import { renderWithProviders } from '../../test-utils/render.js';
import { waitFor } from '../../test-utils/async.js';
import {
PolicyUpdateDialog,
PolicyUpdateChoice,
} from './PolicyUpdateDialog.js';
describe('PolicyUpdateDialog', () => {
afterEach(() => {
vi.clearAllMocks();
});
it('renders correctly with default props', () => {
const onSelect = vi.fn();
const { lastFrame } = renderWithProviders(
<PolicyUpdateDialog
onSelect={onSelect}
scope="project"
identifier="/test/path"
isRestarting={false}
/>,
);
const output = lastFrame();
expect(output).toContain('New or changed project policies detected');
expect(output).toContain('Location: /test/path');
expect(output).toContain('Accept and Load');
expect(output).toContain('Ignore');
});
it('calls onSelect with ACCEPT when accept option is chosen', async () => {
const onSelect = vi.fn();
const { stdin } = renderWithProviders(
<PolicyUpdateDialog
onSelect={onSelect}
scope="project"
identifier="/test/path"
isRestarting={false}
/>,
);
// Accept is the first option, so pressing enter should select it
await act(async () => {
stdin.write('\r');
});
await waitFor(() => {
expect(onSelect).toHaveBeenCalledWith(PolicyUpdateChoice.ACCEPT);
});
});
it('calls onSelect with IGNORE when ignore option is chosen', async () => {
const onSelect = vi.fn();
const { stdin } = renderWithProviders(
<PolicyUpdateDialog
onSelect={onSelect}
scope="project"
identifier="/test/path"
isRestarting={false}
/>,
);
// Move down to Ignore option
await act(async () => {
stdin.write('\x1B[B'); // Down arrow
});
await act(async () => {
stdin.write('\r'); // Enter
});
await waitFor(() => {
expect(onSelect).toHaveBeenCalledWith(PolicyUpdateChoice.IGNORE);
});
});
it('calls onSelect with IGNORE when Escape is pressed', async () => {
const onSelect = vi.fn();
const { stdin } = renderWithProviders(
<PolicyUpdateDialog
onSelect={onSelect}
scope="project"
identifier="/test/path"
isRestarting={false}
/>,
);
await act(async () => {
stdin.write('\x1B'); // Escape key
});
await waitFor(() => {
expect(onSelect).toHaveBeenCalledWith(PolicyUpdateChoice.IGNORE);
});
});
it('displays restarting message when isRestarting is true', () => {
const onSelect = vi.fn();
const { lastFrame } = renderWithProviders(
<PolicyUpdateDialog
onSelect={onSelect}
scope="project"
identifier="/test/path"
isRestarting={true}
/>,
);
const output = lastFrame();
expect(output).toContain(
'Gemini CLI is restarting to apply the policy changes...',
);
});
});

View File

@@ -1,20 +1,15 @@
/**
* @license
* Copyright 2025 Google LLC
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { Box, Text } from 'ink';
import type React from 'react';
import { useEffect, useState, useCallback } from 'react';
import { theme } from '../semantic-colors.js';
import type { RadioSelectItem } from './shared/RadioButtonSelect.js';
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
import { useKeypress } from '../hooks/useKeypress.js';
import * as process from 'node:process';
import { relaunchApp } from '../../utils/processUtils.js';
import { runExitCleanup } from '../../utils/cleanup.js';
import { ExitCodes } from '@google/gemini-cli-core';
export enum PolicyUpdateChoice {
ACCEPT = 'accept',
@@ -34,33 +29,10 @@ export const PolicyUpdateDialog: React.FC<PolicyUpdateDialogProps> = ({
identifier,
isRestarting,
}) => {
const [exiting, setExiting] = useState(false);
useEffect(() => {
let timer: ReturnType<typeof setTimeout>;
if (isRestarting) {
timer = setTimeout(async () => {
await relaunchApp();
}, 250);
}
return () => {
if (timer) clearTimeout(timer);
};
}, [isRestarting]);
const handleExit = useCallback(() => {
setExiting(true);
// Give time for the UI to render the exiting message
setTimeout(async () => {
await runExitCleanup();
process.exit(ExitCodes.FATAL_CANCELLATION_ERROR);
}, 100);
}, []);
useKeypress(
(key) => {
if (key.name === 'escape') {
handleExit();
onSelect(PolicyUpdateChoice.IGNORE);
return true;
}
return false;
@@ -114,14 +86,6 @@ export const PolicyUpdateDialog: React.FC<PolicyUpdateDialogProps> = ({
</Text>
</Box>
)}
{exiting && (
<Box marginLeft={1} marginTop={1}>
<Text color={theme.status.warning}>
A selection must be made to continue. Exiting since escape was
pressed.
</Text>
</Box>
)}
</Box>
);
};

View File

@@ -61,7 +61,7 @@ export function getPolicyDirectories(
// Admin tier (highest priority)
dirs.push(Storage.getSystemPoliciesDir());
// User tier (second higheset priority)
// User tier (second highest priority)
if (policyPaths && policyPaths.length > 0) {
dirs.push(...policyPaths);
} else {