Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions packages/cli/src/nonInteractiveCli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,6 @@ describe('runNonInteractive', () => {
);
vi.mocked(handleAtCommand).mockImplementation(async ({ query }) => ({
processedQuery: [{ text: query }],
shouldProceed: true,
}));
});

Expand Down Expand Up @@ -573,7 +572,6 @@ describe('runNonInteractive', () => {
// 3. Setup the mock to return the processed parts
mockHandleAtCommand.mockResolvedValue({
processedQuery: processedParts,
shouldProceed: true,
});

// Mock a simple stream response from the Gemini client
Expand Down
6 changes: 3 additions & 3 deletions packages/cli/src/nonInteractiveCli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ export async function runNonInteractive({
}

if (!query) {
const { processedQuery, shouldProceed } = await handleAtCommand({
const { processedQuery, error } = await handleAtCommand({
query: input,
config,
addItem: (_item, _timestamp) => 0,
Expand All @@ -239,11 +239,11 @@ export async function runNonInteractive({
signal: abortController.signal,
});

if (!shouldProceed || !processedQuery) {
if (error || !processedQuery) {
// An error occurred during @include processing (e.g., file not found).
// The error message is already logged by handleAtCommand.
throw new FatalInputError(
'Exiting due to an error processing the @ command.',
error || 'Exiting due to an error processing the @ command.',
);
}
query = processedQuery as Part[];
Expand Down
88 changes: 55 additions & 33 deletions packages/cli/src/ui/hooks/atCommandProcessor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
COMMON_IGNORE_PATTERNS,
// DEFAULT_FILE_EXCLUDES,
} from '@google/gemini-cli-core';
import * as core from '@google/gemini-cli-core';
import * as os from 'node:os';
import { ToolCallStatus } from '../types.js';
import type { UseHistoryManagerReturn } from './useHistoryManager.js';
Expand Down Expand Up @@ -120,7 +121,6 @@ describe('handleAtCommand', () => {

expect(result).toEqual({
processedQuery: [{ text: query }],
shouldProceed: true,
});
});

Expand All @@ -138,7 +138,6 @@ describe('handleAtCommand', () => {

expect(result).toEqual({
processedQuery: [{ text: queryWithSpaces }],
shouldProceed: true,
});
expect(mockOnDebugMessage).toHaveBeenCalledWith(
'Lone @ detected, will be treated as text in the modified query.',
Expand Down Expand Up @@ -171,7 +170,6 @@ describe('handleAtCommand', () => {
{ text: fileContent },
{ text: '\n--- End of content ---' },
],
shouldProceed: true,
});
expect(mockAddItem).toHaveBeenCalledWith(
expect.objectContaining({
Expand Down Expand Up @@ -211,7 +209,6 @@ describe('handleAtCommand', () => {
{ text: fileContent },
{ text: '\n--- End of content ---' },
],
shouldProceed: true,
});
expect(mockOnDebugMessage).toHaveBeenCalledWith(
`Path ${dirPath} resolved to directory, using glob: ${resolvedGlob}`,
Expand Down Expand Up @@ -246,7 +243,6 @@ describe('handleAtCommand', () => {
{ text: fileContent },
{ text: '\n--- End of content ---' },
],
shouldProceed: true,
});
});

Expand Down Expand Up @@ -276,7 +272,6 @@ describe('handleAtCommand', () => {
{ text: fileContent },
{ text: '\n--- End of content ---' },
],
shouldProceed: true,
});
expect(mockAddItem).toHaveBeenCalledWith(
expect.objectContaining({
Expand Down Expand Up @@ -321,7 +316,6 @@ describe('handleAtCommand', () => {
{ text: content2 },
{ text: '\n--- End of content ---' },
],
shouldProceed: true,
});
});

Expand Down Expand Up @@ -362,7 +356,6 @@ describe('handleAtCommand', () => {
{ text: content2 },
{ text: '\n--- End of content ---' },
],
shouldProceed: true,
});
});

Expand Down Expand Up @@ -401,7 +394,6 @@ describe('handleAtCommand', () => {
{ text: content1 },
{ text: '\n--- End of content ---' },
],
shouldProceed: true,
});
expect(mockOnDebugMessage).toHaveBeenCalledWith(
`Path ${invalidFile} not found directly, attempting glob search.`,
Expand All @@ -428,7 +420,6 @@ describe('handleAtCommand', () => {

expect(result).toEqual({
processedQuery: [{ text: 'Check @nonexistent.txt and @ also' }],
shouldProceed: true,
});
});

Expand Down Expand Up @@ -462,7 +453,6 @@ describe('handleAtCommand', () => {

expect(result).toEqual({
processedQuery: [{ text: query }],
shouldProceed: true,
});
expect(mockOnDebugMessage).toHaveBeenCalledWith(
`Path ${gitIgnoredFile} is git-ignored and will be skipped.`,
Expand Down Expand Up @@ -501,7 +491,6 @@ describe('handleAtCommand', () => {
{ text: 'console.log("Hello world");' },
{ text: '\n--- End of content ---' },
],
shouldProceed: true,
});
});

Expand Down Expand Up @@ -534,7 +523,6 @@ describe('handleAtCommand', () => {
{ text: '# Project README' },
{ text: '\n--- End of content ---' },
],
shouldProceed: true,
});
expect(mockOnDebugMessage).toHaveBeenCalledWith(
`Path ${gitIgnoredFile} is git-ignored and will be skipped.`,
Expand Down Expand Up @@ -562,7 +550,6 @@ describe('handleAtCommand', () => {

expect(result).toEqual({
processedQuery: [{ text: query }],
shouldProceed: true,
});
expect(mockOnDebugMessage).toHaveBeenCalledWith(
`Path ${gitFile} is git-ignored and will be skipped.`,
Expand Down Expand Up @@ -595,7 +582,8 @@ describe('handleAtCommand', () => {
`Glob tool not found. Path ${invalidFile} will be skipped.`,
);
expect(result.processedQuery).toEqual([{ text: query }]);
expect(result.shouldProceed).toBe(true);
expect(result.processedQuery).not.toBeNull();
expect(result.error).toBeUndefined();
});
});

Expand All @@ -622,7 +610,6 @@ describe('handleAtCommand', () => {

expect(result).toEqual({
processedQuery: [{ text: query }],
shouldProceed: true,
});
expect(mockOnDebugMessage).toHaveBeenCalledWith(
`Path ${geminiIgnoredFile} is gemini-ignored and will be skipped.`,
Expand Down Expand Up @@ -660,7 +647,6 @@ describe('handleAtCommand', () => {
{ text: 'console.log("Hello world");' },
{ text: '\n--- End of content ---' },
],
shouldProceed: true,
});
});

Expand Down Expand Up @@ -696,7 +682,6 @@ describe('handleAtCommand', () => {
{ text: '// Main application entry' },
{ text: '\n--- End of content ---' },
],
shouldProceed: true,
});
expect(mockOnDebugMessage).toHaveBeenCalledWith(
`Path ${geminiIgnoredFile} is gemini-ignored and will be skipped.`,
Expand Down Expand Up @@ -824,7 +809,6 @@ describe('handleAtCommand', () => {
{ text: fileContent },
{ text: '\n--- End of content ---' },
],
shouldProceed: true,
});
},
);
Expand Down Expand Up @@ -863,7 +847,6 @@ describe('handleAtCommand', () => {
{ text: content2 },
{ text: '\n--- End of content ---' },
],
shouldProceed: true,
});
});

Expand Down Expand Up @@ -893,7 +876,6 @@ describe('handleAtCommand', () => {
{ text: fileContent },
{ text: '\n--- End of content ---' },
],
shouldProceed: true,
});
});

Expand Down Expand Up @@ -924,7 +906,6 @@ describe('handleAtCommand', () => {
{ text: fileContent },
{ text: '\n--- End of content ---' },
],
shouldProceed: true,
});
});

Expand Down Expand Up @@ -955,7 +936,6 @@ describe('handleAtCommand', () => {
{ text: fileContent },
{ text: '\n--- End of content ---' },
],
shouldProceed: true,
});
});

Expand Down Expand Up @@ -986,11 +966,10 @@ describe('handleAtCommand', () => {
{ text: fileContent },
{ text: '\n--- End of content ---' },
],
shouldProceed: true,
});
});

it('should not terminate at period within file name', async () => {
it('should correctly handle file paths with multiple periods', async () => {
const fileContent = 'Version info';
const filePath = await createTestFile(
path.join(testRootDir, 'version.1.2.3.txt'),
Expand All @@ -1017,7 +996,6 @@ describe('handleAtCommand', () => {
{ text: fileContent },
{ text: '\n--- End of content ---' },
],
shouldProceed: true,
});
});

Expand Down Expand Up @@ -1046,7 +1024,6 @@ describe('handleAtCommand', () => {
{ text: fileContent },
{ text: '\n--- End of content ---' },
],
shouldProceed: true,
});
});

Expand Down Expand Up @@ -1075,7 +1052,6 @@ describe('handleAtCommand', () => {
{ text: fileContent },
{ text: '\n--- End of content ---' },
],
shouldProceed: true,
});
});

Expand Down Expand Up @@ -1104,7 +1080,6 @@ describe('handleAtCommand', () => {
{ text: fileContent },
{ text: '\n--- End of content ---' },
],
shouldProceed: true,
});
});
});
Expand Down Expand Up @@ -1136,7 +1111,6 @@ describe('handleAtCommand', () => {
{ text: fileContent },
{ text: '\n--- End of content ---' },
],
shouldProceed: true,
});

expect(mockOnDebugMessage).toHaveBeenCalledWith(
Expand Down Expand Up @@ -1165,7 +1139,8 @@ describe('handleAtCommand', () => {
signal: abortController.signal,
});

expect(result.shouldProceed).toBe(true);
expect(result.processedQuery).not.toBeNull();
expect(result.error).toBeUndefined();
expect(result.processedQuery).toEqual(
expect.arrayContaining([
{ text: `Check @${path.join(subDirPath, '**')} please.` },
Expand Down Expand Up @@ -1207,7 +1182,6 @@ describe('handleAtCommand', () => {

expect(result).toEqual({
processedQuery: [{ text: `Check @${outsidePath} please.` }],
shouldProceed: true,
});

expect(mockOnDebugMessage).toHaveBeenCalledWith(
Expand Down Expand Up @@ -1326,7 +1300,8 @@ describe('handleAtCommand', () => {
signal: abortController.signal,
});

expect(result.shouldProceed).toBe(false);
expect(result.processedQuery).toBeNull();
expect(result.error).toBeDefined();
expect(mockAddItem).toHaveBeenCalledWith(
expect.objectContaining({
type: 'tool_group',
Expand All @@ -1342,4 +1317,51 @@ describe('handleAtCommand', () => {
);
});
});

it('should return error if the read_many_files tool is cancelled by user', async () => {
const fileContent = 'Some content';
const filePath = await createTestFile(
path.join(testRootDir, 'file.txt'),
fileContent,
);
const query = `@${filePath}`;

// Simulate user cancellation
const mockToolInstance = {
buildAndExecute: vi
.fn()
.mockRejectedValue(new Error('User cancelled operation')),
displayName: 'Read Many Files',
build: vi.fn(() => ({
execute: mockToolInstance.buildAndExecute,
getDescription: vi.fn(() => 'Mocked tool description'),
})),
};
const viSpy = vi.spyOn(core, 'ReadManyFilesTool');
viSpy.mockImplementation(
() => mockToolInstance as unknown as core.ReadManyFilesTool,
);

const result = await handleAtCommand({
query,
config: mockConfig,
addItem: mockAddItem,
onDebugMessage: mockOnDebugMessage,
messageId: 134,
signal: abortController.signal,
});

expect(result).toEqual({
processedQuery: null,
error: `Exiting due to an error processing the @ command: Error reading files (file.txt): User cancelled operation`,
});

expect(mockAddItem).toHaveBeenCalledWith(
expect.objectContaining({
type: 'tool_group',
tools: [expect.objectContaining({ status: ToolCallStatus.Error })],
}),
134,
);
});
});
Loading