mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-13 23:51:16 -07:00
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:
@@ -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' },
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
120
packages/cli/src/ui/components/PolicyUpdateDialog.test.tsx
Normal file
120
packages/cli/src/ui/components/PolicyUpdateDialog.test.tsx
Normal 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...',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user