diff --git a/packages/cli/src/config/project-policy-cli.test.ts b/packages/cli/src/config/project-policy-cli.test.ts
index 6d7d5d5ac0..b219eafee1 100644
--- a/packages/cli/src/config/project-policy-cli.test.ts
+++ b/packages/cli/src/config/project-policy-cli.test.ts
@@ -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' },
};
});
diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx
index 70c7277f06..7a270b8b74 100644
--- a/packages/cli/src/ui/AppContainer.tsx
+++ b/packages/cli/src/ui/AppContainer.tsx
@@ -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);
}
diff --git a/packages/cli/src/ui/components/PolicyUpdateDialog.test.tsx b/packages/cli/src/ui/components/PolicyUpdateDialog.test.tsx
new file mode 100644
index 0000000000..ffc49e443b
--- /dev/null
+++ b/packages/cli/src/ui/components/PolicyUpdateDialog.test.tsx
@@ -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(
+ ,
+ );
+
+ 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(
+ ,
+ );
+
+ // 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(
+ ,
+ );
+
+ // 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(
+ ,
+ );
+
+ 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(
+ ,
+ );
+
+ const output = lastFrame();
+ expect(output).toContain(
+ 'Gemini CLI is restarting to apply the policy changes...',
+ );
+ });
+});
diff --git a/packages/cli/src/ui/components/PolicyUpdateDialog.tsx b/packages/cli/src/ui/components/PolicyUpdateDialog.tsx
index c7116c5a00..c05044bd9c 100644
--- a/packages/cli/src/ui/components/PolicyUpdateDialog.tsx
+++ b/packages/cli/src/ui/components/PolicyUpdateDialog.tsx
@@ -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 = ({
identifier,
isRestarting,
}) => {
- const [exiting, setExiting] = useState(false);
-
- useEffect(() => {
- let timer: ReturnType;
- 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 = ({
)}
- {exiting && (
-
-
- A selection must be made to continue. Exiting since escape was
- pressed.
-
-
- )}
);
};
diff --git a/packages/core/src/policy/config.ts b/packages/core/src/policy/config.ts
index 780b2da121..6acb59f70b 100644
--- a/packages/core/src/policy/config.ts
+++ b/packages/core/src/policy/config.ts
@@ -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 {