Merge branch 'main' into mk-bundling-no-npmrc

This commit is contained in:
matt korwel
2025-10-22 12:19:52 -07:00
committed by GitHub
34 changed files with 568 additions and 120 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.11.0-nightly.20251021.e72c00cf",
"version": "0.12.0-nightly.20251022.0542de95",
"description": "Gemini CLI",
"repository": {
"type": "git",
@@ -25,7 +25,7 @@
"dist"
],
"config": {
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.11.0-nightly.20251021.e72c00cf"
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.12.0-nightly.20251022.0542de95"
},
"dependencies": {
"@google/gemini-cli-core": "file:../core",
+1
View File
@@ -16,6 +16,7 @@ import { newCommand } from './extensions/new.js';
export const extensionsCommand: CommandModule = {
command: 'extensions <command>',
aliases: ['extension'],
describe: 'Manage Gemini CLI extensions.',
builder: (yargs) =>
yargs
+17
View File
@@ -1112,6 +1112,23 @@ describe('Approval mode tool exclusion logic', () => {
expect(excludedTools).not.toContain(WRITE_FILE_TOOL_NAME); // Should be allowed in auto_edit
});
it('should throw an error if YOLO mode is attempted when disableYoloMode is true', async () => {
process.argv = ['node', 'script.js', '--yolo'];
const argv = await parseArguments({} as Settings);
const settings: Settings = {
security: {
disableYoloMode: true,
},
};
const extensions: GeminiCLIExtension[] = [];
await expect(
loadCliConfig(settings, extensions, 'test-session', argv),
).rejects.toThrow(
'Cannot start in YOLO mode when it is disabled by settings',
);
});
it('should throw an error for invalid approval mode values in loadCliConfig', async () => {
// Create a mock argv with an invalid approval mode that bypasses argument parsing validation
const invalidArgv: Partial<CliArgs> & { approvalMode: string } = {
+16
View File
@@ -442,6 +442,21 @@ export async function loadCliConfig(
argv.yolo || false ? ApprovalMode.YOLO : ApprovalMode.DEFAULT;
}
// Override approval mode if disableYoloMode is set.
if (settings.security?.disableYoloMode) {
if (approvalMode === ApprovalMode.YOLO) {
debugLogger.error('YOLO mode is disabled by the "disableYolo" setting.');
throw new FatalConfigError(
'Cannot start in YOLO mode when it is disabled by settings',
);
}
approvalMode = ApprovalMode.DEFAULT;
} else if (approvalMode === ApprovalMode.YOLO) {
debugLogger.warn(
'YOLO mode is enabled. All tool calls will be automatically approved.',
);
}
// Force approval mode to default if the folder is not trusted.
if (!trustedFolder && approvalMode !== ApprovalMode.DEFAULT) {
debugLogger.warn(
@@ -583,6 +598,7 @@ export async function loadCliConfig(
geminiMdFileCount: fileCount,
geminiMdFilePaths: filePaths,
approvalMode,
disableYoloMode: settings.security?.disableYoloMode,
showMemoryUsage: settings.ui?.showMemoryUsage || false,
accessibility: {
...settings.ui?.accessibility,
+34
View File
@@ -609,6 +609,40 @@ describe('Settings Loading and Merging', () => {
expect(settings.merged.security?.folderTrust?.enabled).toBe(true); // System setting should be used
});
it('should not allow user or workspace to override system disableYoloMode', () => {
(mockFsExistsSync as Mock).mockReturnValue(true);
const userSettingsContent = {
security: {
disableYoloMode: false,
},
};
const workspaceSettingsContent = {
security: {
disableYoloMode: false, // This should be ignored
},
};
const systemSettingsContent = {
security: {
disableYoloMode: true,
},
};
(fs.readFileSync as Mock).mockImplementation(
(p: fs.PathOrFileDescriptor) => {
if (p === getSystemSettingsPath())
return JSON.stringify(systemSettingsContent);
if (p === USER_SETTINGS_PATH)
return JSON.stringify(userSettingsContent);
if (p === MOCK_WORKSPACE_SETTINGS_PATH)
return JSON.stringify(workspaceSettingsContent);
return '{}';
},
);
const settings = loadSettings(MOCK_WORKSPACE_DIR);
expect(settings.merged.security?.disableYoloMode).toBe(true); // System setting should be used
});
it('should handle contextFileName correctly when only in user settings', () => {
(mockFsExistsSync as Mock).mockImplementation(
(p: fs.PathLike) => p === USER_SETTINGS_PATH,
@@ -937,6 +937,15 @@ const SETTINGS_SCHEMA = {
description: 'Security-related settings.',
showInDialog: false,
properties: {
disableYoloMode: {
type: 'boolean',
label: 'Disable YOLO Mode',
category: 'Security',
requiresRestart: true,
default: false,
description: 'Disable YOLO mode, even if enabled by a flag.',
showInDialog: true,
},
folderTrust: {
type: 'object',
label: 'Folder Trust',
@@ -254,7 +254,7 @@ describe('BaseSelectionList', () => {
});
});
describe.skip('Scrolling and Pagination (maxItemsToShow)', () => {
describe('Scrolling and Pagination (maxItemsToShow)', () => {
const longList = Array.from({ length: 10 }, (_, i) => ({
value: `Item ${i + 1}`,
label: `Item ${i + 1}`,
@@ -323,11 +323,13 @@ describe('BaseSelectionList', () => {
// New visible window should be Items 2, 3, 4 (scroll offset 1).
await updateActiveIndex(3);
const output = lastFrame();
expect(output).not.toContain('Item 1');
expect(output).toContain('Item 2');
expect(output).toContain('Item 4');
expect(output).not.toContain('Item 5');
await waitFor(() => {
const output = lastFrame();
expect(output).not.toContain('Item 1');
expect(output).toContain('Item 2');
expect(output).toContain('Item 4');
expect(output).not.toContain('Item 5');
});
});
it('should scroll up when activeIndex moves before the visible window', async () => {
@@ -335,19 +337,23 @@ describe('BaseSelectionList', () => {
await updateActiveIndex(4);
let output = lastFrame();
expect(output).toContain('Item 3'); // Should see items 3, 4, 5
expect(output).toContain('Item 5');
expect(output).not.toContain('Item 2');
await waitFor(() => {
const output = lastFrame();
expect(output).toContain('Item 3'); // Should see items 3, 4, 5
expect(output).toContain('Item 5');
expect(output).not.toContain('Item 2');
});
// Now test scrolling up: move to index 1 (Item 2)
// This should trigger scroll up to show items 2, 3, 4
await updateActiveIndex(1);
output = lastFrame();
expect(output).toContain('Item 2');
expect(output).toContain('Item 4');
expect(output).not.toContain('Item 5'); // Item 5 should no longer be visible
await waitFor(() => {
const output = lastFrame();
expect(output).toContain('Item 2');
expect(output).toContain('Item 4');
expect(output).not.toContain('Item 5'); // Item 5 should no longer be visible
});
});
it('should pin the scroll offset to the end if selection starts near the end', async () => {
@@ -375,16 +381,19 @@ describe('BaseSelectionList', () => {
expect(lastFrame()).toContain('Item 1');
await updateActiveIndex(3); // Should trigger scroll
let output = lastFrame();
expect(output).toContain('Item 2');
expect(output).toContain('Item 4');
expect(output).not.toContain('Item 1');
await waitFor(() => {
const output = lastFrame();
expect(output).toContain('Item 2');
expect(output).toContain('Item 4');
expect(output).not.toContain('Item 1');
});
await updateActiveIndex(5); // Scroll further
output = lastFrame();
expect(output).toContain('Item 4');
expect(output).toContain('Item 6');
expect(output).not.toContain('Item 3');
await waitFor(() => {
const output = lastFrame();
expect(output).toContain('Item 4');
expect(output).toContain('Item 6');
expect(output).not.toContain('Item 3');
});
});
it('should correctly identify the selected item within the visible window', () => {
@@ -412,17 +421,16 @@ describe('BaseSelectionList', () => {
expect.objectContaining({ value: 'Item 6' }),
expect.objectContaining({ isSelected: true }),
);
});
// Item 4 (index 3) should not be selected
expect(mockRenderItem).toHaveBeenCalledWith(
expect.objectContaining({ value: 'Item 4' }),
expect.objectContaining({ isSelected: false }),
);
// Item 4 (index 3) should not be selected
expect(mockRenderItem).toHaveBeenCalledWith(
expect.objectContaining({ value: 'Item 4' }),
expect.objectContaining({ isSelected: false }),
);
});
});
it('should handle maxItemsToShow larger than the list length', () => {
// Test edge case where maxItemsToShow exceeds available items
const { lastFrame } = renderComponent(
{ items: longList, maxItemsToShow: 15 },
0,
@@ -953,6 +953,40 @@ describe('useTextBuffer', () => {
expect(getBufferState(result).lines).toEqual(['', '']);
});
it('should do nothing for a tab key press', () => {
const { result } = renderHook(() =>
useTextBuffer({ viewport, isValidPath: () => false }),
);
act(() =>
result.current.handleInput({
name: 'tab',
ctrl: false,
meta: false,
shift: false,
paste: false,
sequence: '\t',
}),
);
expect(getBufferState(result).text).toBe('');
});
it('should do nothing for a shift tab key press', () => {
const { result } = renderHook(() =>
useTextBuffer({ viewport, isValidPath: () => false }),
);
act(() =>
result.current.handleInput({
name: 'tab',
ctrl: false,
meta: false,
shift: true,
paste: false,
sequence: '\u001b[9;2u',
}),
);
expect(getBufferState(result).text).toBe('');
});
it('should handle "Backspace" key', () => {
const { result } = renderHook(() =>
useTextBuffer({
@@ -1933,7 +1933,7 @@ export function useTextBuffer({
else if (key.name === 'delete' || (key.ctrl && key.name === 'd')) del();
else if (key.ctrl && !key.shift && key.name === 'z') undo();
else if (key.ctrl && key.shift && key.name === 'z') redo();
else if (input && !key.ctrl && !key.meta) {
else if (input && !key.ctrl && !key.meta && key.name !== 'tab') {
insert(input, { paste: key.paste });
}
},
@@ -37,6 +37,7 @@ vi.mock('@google/gemini-cli-core', async () => {
interface MockConfigInstanceShape {
getApprovalMode: Mock<() => ApprovalMode>;
setApprovalMode: Mock<(value: ApprovalMode) => void>;
isYoloModeDisabled: Mock<() => boolean>;
isTrustedFolder: Mock<() => boolean>;
getCoreTools: Mock<() => string[]>;
getToolDiscoveryCommand: Mock<() => string | undefined>;
@@ -76,6 +77,7 @@ describe('useAutoAcceptIndicator', () => {
setApprovalMode: instanceSetApprovalModeMock as Mock<
(value: ApprovalMode) => void
>,
isYoloModeDisabled: vi.fn().mockReturnValue(false),
isTrustedFolder: vi.fn().mockReturnValue(true) as Mock<() => boolean>,
getCoreTools: vi.fn().mockReturnValue([]) as Mock<() => string[]>,
getToolDiscoveryCommand: vi.fn().mockReturnValue(undefined) as Mock<
@@ -471,6 +473,45 @@ describe('useAutoAcceptIndicator', () => {
});
});
describe('when YOLO mode is disabled by settings', () => {
beforeEach(() => {
// Ensure isYoloModeDisabled returns true for these tests
if (mockConfigInstance && mockConfigInstance.isYoloModeDisabled) {
mockConfigInstance.isYoloModeDisabled.mockReturnValue(true);
}
});
it('should not enable YOLO mode when Ctrl+Y is pressed and add an info message', () => {
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);
const mockAddItem = vi.fn();
const { result } = renderHook(() =>
useAutoAcceptIndicator({
config: mockConfigInstance as unknown as ActualConfigType,
addItem: mockAddItem,
}),
);
expect(result.current).toBe(ApprovalMode.DEFAULT);
act(() => {
capturedUseKeypressHandler({ name: 'y', ctrl: true } as Key);
});
// setApprovalMode should not be called because the check should return early
expect(mockConfigInstance.setApprovalMode).not.toHaveBeenCalled();
// An info message should be added
expect(mockAddItem).toHaveBeenCalledWith(
{
type: MessageType.WARNING,
text: 'You cannot enter YOLO mode since it is disabled in your settings.',
},
expect.any(Number),
);
// The mode should not change
expect(result.current).toBe(ApprovalMode.DEFAULT);
});
});
it('should call onApprovalModeChange when switching to YOLO mode', () => {
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);
@@ -34,6 +34,21 @@ export function useAutoAcceptIndicator({
let nextApprovalMode: ApprovalMode | undefined;
if (key.ctrl && key.name === 'y') {
if (
config.isYoloModeDisabled() &&
config.getApprovalMode() !== ApprovalMode.YOLO
) {
if (addItem) {
addItem(
{
type: MessageType.WARNING,
text: 'You cannot enter YOLO mode since it is disabled in your settings.',
},
Date.now(),
);
}
return;
}
nextApprovalMode =
config.getApprovalMode() === ApprovalMode.YOLO
? ApprovalMode.DEFAULT
@@ -32,7 +32,6 @@ import {
ApprovalMode,
MockTool,
} from '@google/gemini-cli-core';
import type { HistoryItemWithoutId, HistoryItemToolGroup } from '../types.js';
import { ToolCallStatus } from '../types.js';
// Mocks
@@ -101,11 +100,9 @@ const mockToolRequiresConfirmation = new MockTool({
describe('useReactToolScheduler in YOLO Mode', () => {
let onComplete: Mock;
let setPendingHistoryItem: Mock;
beforeEach(() => {
onComplete = vi.fn();
setPendingHistoryItem = vi.fn();
mockToolRegistry.getTool.mockClear();
(mockToolRequiresConfirmation.execute as Mock).mockClear();
(mockToolRequiresConfirmation.shouldConfirmExecute as Mock).mockClear();
@@ -128,7 +125,7 @@ describe('useReactToolScheduler in YOLO Mode', () => {
useReactToolScheduler(
onComplete,
mockConfig as unknown as Config,
setPendingHistoryItem,
() => undefined,
() => {},
),
);
@@ -187,26 +184,11 @@ describe('useReactToolScheduler in YOLO Mode', () => {
}),
}),
]);
// Ensure no confirmation UI was triggered (setPendingHistoryItem should not have been called with confirmation details)
const setPendingHistoryItemCalls = setPendingHistoryItem.mock.calls;
const confirmationCall = setPendingHistoryItemCalls.find((call) => {
const item = typeof call[0] === 'function' ? call[0]({}) : call[0];
return item?.tools?.[0]?.confirmationDetails;
});
expect(confirmationCall).toBeUndefined();
});
});
describe('useReactToolScheduler', () => {
// TODO(ntaylormullen): The following tests are skipped due to difficulties in
// reliably testing the asynchronous state updates and interactions with timers.
// These tests involve complex sequences of events, including confirmations,
// live output updates, and cancellations, which are challenging to assert
// correctly with the current testing setup. Further investigation is needed
// to find a robust way to test these scenarios.
let onComplete: Mock;
let setPendingHistoryItem: Mock;
let capturedOnConfirmForTest:
| ((outcome: ToolConfirmationOutcome) => void | Promise<void>)
| undefined;
@@ -214,29 +196,6 @@ describe('useReactToolScheduler', () => {
beforeEach(() => {
onComplete = vi.fn();
capturedOnConfirmForTest = undefined;
setPendingHistoryItem = vi.fn((updaterOrValue) => {
let pendingItem: HistoryItemWithoutId | null = null;
if (typeof updaterOrValue === 'function') {
// Loosen the type for prevState to allow for more flexible updates in tests
const prevState: Partial<HistoryItemToolGroup> = {
type: 'tool_group', // Still default to tool_group for most cases
tools: [],
};
pendingItem = updaterOrValue(prevState as any); // Allow any for more flexibility
} else {
pendingItem = updaterOrValue;
}
// Capture onConfirm if it exists, regardless of the exact type of pendingItem
// This is a common pattern in these tests.
if (
(pendingItem as HistoryItemToolGroup)?.tools?.[0]?.confirmationDetails
?.onConfirm
) {
capturedOnConfirmForTest = (pendingItem as HistoryItemToolGroup)
.tools[0].confirmationDetails?.onConfirm;
}
});
mockToolRegistry.getTool.mockClear();
(mockTool.execute as Mock).mockClear();
@@ -273,7 +232,7 @@ describe('useReactToolScheduler', () => {
useReactToolScheduler(
onComplete,
mockConfig as unknown as Config,
setPendingHistoryItem,
() => undefined,
() => {},
),
);
@@ -448,7 +407,7 @@ describe('useReactToolScheduler', () => {
expect(result.current[0]).toEqual([]);
});
it.skip('should handle tool requiring confirmation - approved', async () => {
it('should handle tool requiring confirmation - approved', async () => {
mockToolRegistry.getTool.mockReturnValue(mockToolRequiresConfirmation);
const expectedOutput = 'Confirmed output';
(mockToolRequiresConfirmation.execute as Mock).mockResolvedValue({
@@ -471,7 +430,9 @@ describe('useReactToolScheduler', () => {
await vi.runAllTimersAsync();
});
expect(setPendingHistoryItem).toHaveBeenCalled();
const waitingCall = result.current[0][0] as any;
expect(waitingCall.status).toBe('awaiting_approval');
capturedOnConfirmForTest = waitingCall.confirmationDetails?.onConfirm;
expect(capturedOnConfirmForTest).toBeDefined();
await act(async () => {
@@ -510,7 +471,7 @@ describe('useReactToolScheduler', () => {
]);
});
it.skip('should handle tool requiring confirmation - cancelled by user', async () => {
it('should handle tool requiring confirmation - cancelled by user', async () => {
mockToolRegistry.getTool.mockReturnValue(mockToolRequiresConfirmation);
const { result } = renderScheduler();
const schedule = result.current[1];
@@ -527,7 +488,9 @@ describe('useReactToolScheduler', () => {
await vi.runAllTimersAsync();
});
expect(setPendingHistoryItem).toHaveBeenCalled();
const waitingCall = result.current[0][0] as any;
expect(waitingCall.status).toBe('awaiting_approval');
capturedOnConfirmForTest = waitingCall.confirmationDetails?.onConfirm;
expect(capturedOnConfirmForTest).toBeDefined();
await act(async () => {
@@ -552,7 +515,8 @@ describe('useReactToolScheduler', () => {
expect.objectContaining({
functionResponse: expect.objectContaining({
response: expect.objectContaining({
error: `User did not allow tool call ${request.name}. Reason: User cancelled.`,
error:
'[Operation Cancelled] Reason: User did not allow tool call',
}),
}),
}),
@@ -562,7 +526,7 @@ describe('useReactToolScheduler', () => {
]);
});
it.skip('should handle live output updates', async () => {
it('should handle live output updates', async () => {
mockToolRegistry.getTool.mockReturnValue(mockToolWithLiveOutput);
let liveUpdateFn: ((output: string) => void) | undefined;
let resolveExecutePromise: (value: ToolResult) => void;
@@ -600,7 +564,7 @@ describe('useReactToolScheduler', () => {
});
expect(liveUpdateFn).toBeDefined();
expect(setPendingHistoryItem).toHaveBeenCalled();
expect(result.current[0][0].status).toBe('executing');
await act(async () => {
liveUpdateFn?.('Live output 1');
@@ -742,7 +706,7 @@ describe('useReactToolScheduler', () => {
expect(result.current[0]).toEqual([]);
});
it.skip('should throw error if scheduling while already running', async () => {
it('should queue if scheduling while already running', async () => {
mockToolRegistry.getTool.mockReturnValue(mockTool);
const longExecutePromise = new Promise<ToolResult>((resolve) =>
setTimeout(
@@ -777,9 +741,7 @@ describe('useReactToolScheduler', () => {
await vi.runAllTimersAsync();
});
expect(() => schedule(request2, new AbortController().signal)).toThrow(
'Cannot schedule tool calls while other tool calls are running',
);
schedule(request2, new AbortController().signal);
await act(async () => {
await vi.advanceTimersByTimeAsync(50);
@@ -795,6 +757,21 @@ describe('useReactToolScheduler', () => {
response: expect.objectContaining({ resultDisplay: 'done display' }),
}),
]);
// Wait for request2 to complete
await act(async () => {
await vi.advanceTimersByTimeAsync(50);
await vi.runAllTimersAsync();
await act(async () => {
await vi.runAllTimersAsync();
});
});
expect(onComplete).toHaveBeenCalledWith([
expect.objectContaining({
status: 'success',
request: request2,
response: expect.objectContaining({ resultDisplay: 'done display' }),
}),
]);
expect(result.current[0]).toEqual([]);
});
});