2025-05-20 13:02:41 -07:00
|
|
|
/**
|
|
|
|
|
* @license
|
|
|
|
|
* Copyright 2025 Google LLC
|
|
|
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { vi } from 'vitest';
|
|
|
|
|
import type { Mock } from 'vitest';
|
|
|
|
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
|
|
|
import { mockControl } from '../__mocks__/fs/promises.js';
|
|
|
|
|
import { ReadManyFilesTool } from './read-many-files.js';
|
Ignore folders files (#651)
# Add .gitignore-Aware File Filtering to gemini-cli
This pull request introduces .gitignore-based file filtering to the gemini-cli, ensuring that git-ignored files are automatically excluded from file-related operations and suggestions throughout the CLI. The update enhances usability, reduces noise from build artifacts and dependencies, and provides new configuration options for fine-tuning file discovery.
Key Improvements
.gitignore File Filtering
All @ (at) commands, file completions, and core discovery tools now honor .gitignore patterns by default.
Git-ignored files (such as node_modules/, dist/, .env, and .git) are excluded from results unless explicitly overridden.
The behavior can be customized via a new fileFiltering section in settings.json, including options for:
Turning .gitignore respect on/off.
Adding custom ignore patterns.
Allowing or excluding build artifacts.
Configuration & Documentation Updates
settings.json schema extended with fileFiltering options.
Documentation updated to explain new filtering controls and usage patterns.
Testing
New and updated integration/unit tests for file filtering logic, configuration merging, and edge cases.
Test coverage ensures .gitignore filtering works as intended across different workflows.
Internal Refactoring
Core file discovery logic refactored for maintainability and extensibility.
Underlying tools (ls, glob, read-many-files) now support git-aware filtering out of the box.
Co-authored-by: N. Taylor Mullen <ntaylormullen@google.com>
2025-06-03 21:40:46 -07:00
|
|
|
import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
|
2025-05-20 13:02:41 -07:00
|
|
|
import path from 'path';
|
|
|
|
|
import fs from 'fs'; // Actual fs for setup
|
|
|
|
|
import os from 'os';
|
2025-06-05 10:15:27 -07:00
|
|
|
import { Config } from '../config/config.js';
|
2025-05-20 13:02:41 -07:00
|
|
|
|
|
|
|
|
describe('ReadManyFilesTool', () => {
|
|
|
|
|
let tool: ReadManyFilesTool;
|
|
|
|
|
let tempRootDir: string;
|
|
|
|
|
let tempDirOutsideRoot: string;
|
|
|
|
|
let mockReadFileFn: Mock;
|
|
|
|
|
|
Ignore folders files (#651)
# Add .gitignore-Aware File Filtering to gemini-cli
This pull request introduces .gitignore-based file filtering to the gemini-cli, ensuring that git-ignored files are automatically excluded from file-related operations and suggestions throughout the CLI. The update enhances usability, reduces noise from build artifacts and dependencies, and provides new configuration options for fine-tuning file discovery.
Key Improvements
.gitignore File Filtering
All @ (at) commands, file completions, and core discovery tools now honor .gitignore patterns by default.
Git-ignored files (such as node_modules/, dist/, .env, and .git) are excluded from results unless explicitly overridden.
The behavior can be customized via a new fileFiltering section in settings.json, including options for:
Turning .gitignore respect on/off.
Adding custom ignore patterns.
Allowing or excluding build artifacts.
Configuration & Documentation Updates
settings.json schema extended with fileFiltering options.
Documentation updated to explain new filtering controls and usage patterns.
Testing
New and updated integration/unit tests for file filtering logic, configuration merging, and edge cases.
Test coverage ensures .gitignore filtering works as intended across different workflows.
Internal Refactoring
Core file discovery logic refactored for maintainability and extensibility.
Underlying tools (ls, glob, read-many-files) now support git-aware filtering out of the box.
Co-authored-by: N. Taylor Mullen <ntaylormullen@google.com>
2025-06-03 21:40:46 -07:00
|
|
|
// Mock config for testing
|
|
|
|
|
const mockConfig = {
|
|
|
|
|
getFileService: async () => {
|
|
|
|
|
const service = new FileDiscoveryService(tempRootDir);
|
|
|
|
|
await service.initialize({ respectGitIgnore: true });
|
|
|
|
|
return service;
|
|
|
|
|
},
|
|
|
|
|
getFileFilteringRespectGitIgnore: () => true,
|
2025-06-05 10:15:27 -07:00
|
|
|
getGeminiIgnorePatterns: () => ['**/foo.bar', 'foo.baz', 'foo.*'],
|
Ignore folders files (#651)
# Add .gitignore-Aware File Filtering to gemini-cli
This pull request introduces .gitignore-based file filtering to the gemini-cli, ensuring that git-ignored files are automatically excluded from file-related operations and suggestions throughout the CLI. The update enhances usability, reduces noise from build artifacts and dependencies, and provides new configuration options for fine-tuning file discovery.
Key Improvements
.gitignore File Filtering
All @ (at) commands, file completions, and core discovery tools now honor .gitignore patterns by default.
Git-ignored files (such as node_modules/, dist/, .env, and .git) are excluded from results unless explicitly overridden.
The behavior can be customized via a new fileFiltering section in settings.json, including options for:
Turning .gitignore respect on/off.
Adding custom ignore patterns.
Allowing or excluding build artifacts.
Configuration & Documentation Updates
settings.json schema extended with fileFiltering options.
Documentation updated to explain new filtering controls and usage patterns.
Testing
New and updated integration/unit tests for file filtering logic, configuration merging, and edge cases.
Test coverage ensures .gitignore filtering works as intended across different workflows.
Internal Refactoring
Core file discovery logic refactored for maintainability and extensibility.
Underlying tools (ls, glob, read-many-files) now support git-aware filtering out of the box.
Co-authored-by: N. Taylor Mullen <ntaylormullen@google.com>
2025-06-03 21:40:46 -07:00
|
|
|
} as Partial<Config> as Config;
|
|
|
|
|
|
2025-05-20 13:02:41 -07:00
|
|
|
beforeEach(async () => {
|
|
|
|
|
tempRootDir = fs.mkdtempSync(
|
|
|
|
|
path.join(os.tmpdir(), 'read-many-files-root-'),
|
|
|
|
|
);
|
|
|
|
|
tempDirOutsideRoot = fs.mkdtempSync(
|
|
|
|
|
path.join(os.tmpdir(), 'read-many-files-external-'),
|
|
|
|
|
);
|
Ignore folders files (#651)
# Add .gitignore-Aware File Filtering to gemini-cli
This pull request introduces .gitignore-based file filtering to the gemini-cli, ensuring that git-ignored files are automatically excluded from file-related operations and suggestions throughout the CLI. The update enhances usability, reduces noise from build artifacts and dependencies, and provides new configuration options for fine-tuning file discovery.
Key Improvements
.gitignore File Filtering
All @ (at) commands, file completions, and core discovery tools now honor .gitignore patterns by default.
Git-ignored files (such as node_modules/, dist/, .env, and .git) are excluded from results unless explicitly overridden.
The behavior can be customized via a new fileFiltering section in settings.json, including options for:
Turning .gitignore respect on/off.
Adding custom ignore patterns.
Allowing or excluding build artifacts.
Configuration & Documentation Updates
settings.json schema extended with fileFiltering options.
Documentation updated to explain new filtering controls and usage patterns.
Testing
New and updated integration/unit tests for file filtering logic, configuration merging, and edge cases.
Test coverage ensures .gitignore filtering works as intended across different workflows.
Internal Refactoring
Core file discovery logic refactored for maintainability and extensibility.
Underlying tools (ls, glob, read-many-files) now support git-aware filtering out of the box.
Co-authored-by: N. Taylor Mullen <ntaylormullen@google.com>
2025-06-03 21:40:46 -07:00
|
|
|
tool = new ReadManyFilesTool(tempRootDir, mockConfig);
|
2025-05-20 13:02:41 -07:00
|
|
|
|
|
|
|
|
mockReadFileFn = mockControl.mockReadFile;
|
|
|
|
|
mockReadFileFn.mockReset();
|
|
|
|
|
|
|
|
|
|
mockReadFileFn.mockImplementation(
|
|
|
|
|
async (filePath: fs.PathLike, options?: Record<string, unknown>) => {
|
|
|
|
|
const fp =
|
|
|
|
|
typeof filePath === 'string'
|
|
|
|
|
? filePath
|
|
|
|
|
: (filePath as Buffer).toString();
|
|
|
|
|
|
|
|
|
|
if (fs.existsSync(fp)) {
|
|
|
|
|
const originalFs = await vi.importActual<typeof fs>('fs');
|
|
|
|
|
return originalFs.promises.readFile(fp, options);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (fp.endsWith('nonexistent-file.txt')) {
|
|
|
|
|
const err = new Error(
|
|
|
|
|
`ENOENT: no such file or directory, open '${fp}'`,
|
|
|
|
|
);
|
|
|
|
|
(err as NodeJS.ErrnoException).code = 'ENOENT';
|
|
|
|
|
throw err;
|
|
|
|
|
}
|
|
|
|
|
if (fp.endsWith('unreadable.txt')) {
|
|
|
|
|
const err = new Error(`EACCES: permission denied, open '${fp}'`);
|
|
|
|
|
(err as NodeJS.ErrnoException).code = 'EACCES';
|
|
|
|
|
throw err;
|
|
|
|
|
}
|
|
|
|
|
if (fp.endsWith('.png'))
|
|
|
|
|
return Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); // PNG header
|
|
|
|
|
if (fp.endsWith('.pdf')) return Buffer.from('%PDF-1.4...'); // PDF start
|
|
|
|
|
if (fp.endsWith('binary.bin'))
|
|
|
|
|
return Buffer.from([0x00, 0x01, 0x02, 0x00, 0x03]);
|
|
|
|
|
|
|
|
|
|
const err = new Error(
|
|
|
|
|
`ENOENT: no such file or directory, open '${fp}' (unmocked path)`,
|
|
|
|
|
);
|
|
|
|
|
(err as NodeJS.ErrnoException).code = 'ENOENT';
|
|
|
|
|
throw err;
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
if (fs.existsSync(tempRootDir)) {
|
|
|
|
|
fs.rmSync(tempRootDir, { recursive: true, force: true });
|
|
|
|
|
}
|
|
|
|
|
if (fs.existsSync(tempDirOutsideRoot)) {
|
|
|
|
|
fs.rmSync(tempDirOutsideRoot, { recursive: true, force: true });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('validateParams', () => {
|
|
|
|
|
it('should return null for valid relative paths within root', () => {
|
|
|
|
|
const params = { paths: ['file1.txt', 'subdir/file2.txt'] };
|
|
|
|
|
expect(tool.validateParams(params)).toBeNull();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should return null for valid glob patterns within root', () => {
|
|
|
|
|
const params = { paths: ['*.txt', 'subdir/**/*.js'] };
|
|
|
|
|
expect(tool.validateParams(params)).toBeNull();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should return null for paths trying to escape the root (e.g., ../) as execute handles this', () => {
|
|
|
|
|
const params = { paths: ['../outside.txt'] };
|
|
|
|
|
expect(tool.validateParams(params)).toBeNull();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should return null for absolute paths as execute handles this', () => {
|
|
|
|
|
const params = { paths: [path.join(tempDirOutsideRoot, 'absolute.txt')] };
|
|
|
|
|
expect(tool.validateParams(params)).toBeNull();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should return error if paths array is empty', () => {
|
|
|
|
|
const params = { paths: [] };
|
|
|
|
|
expect(tool.validateParams(params)).toBe(
|
|
|
|
|
'The "paths" parameter is required and must be a non-empty array of strings/glob patterns.',
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should return null for valid exclude and include patterns', () => {
|
|
|
|
|
const params = {
|
|
|
|
|
paths: ['src/**/*.ts'],
|
|
|
|
|
exclude: ['**/*.test.ts'],
|
|
|
|
|
include: ['src/utils/*.ts'],
|
|
|
|
|
};
|
|
|
|
|
expect(tool.validateParams(params)).toBeNull();
|
|
|
|
|
});
|
2025-05-29 22:30:18 +00:00
|
|
|
|
|
|
|
|
it('should return error if paths array contains an empty string', () => {
|
|
|
|
|
const params = { paths: ['file1.txt', ''] };
|
|
|
|
|
expect(tool.validateParams(params)).toBe(
|
|
|
|
|
'Each item in "paths" must be a non-empty string/glob pattern.',
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should return error if include array contains non-string elements', () => {
|
|
|
|
|
const params = {
|
|
|
|
|
paths: ['file1.txt'],
|
|
|
|
|
include: ['*.ts', 123] as string[],
|
|
|
|
|
};
|
|
|
|
|
expect(tool.validateParams(params)).toBe(
|
|
|
|
|
'If provided, "include" must be an array of strings/glob patterns.',
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should return error if exclude array contains non-string elements', () => {
|
|
|
|
|
const params = {
|
|
|
|
|
paths: ['file1.txt'],
|
|
|
|
|
exclude: ['*.log', {}] as string[],
|
|
|
|
|
};
|
|
|
|
|
expect(tool.validateParams(params)).toBe(
|
|
|
|
|
'If provided, "exclude" must be an array of strings/glob patterns.',
|
|
|
|
|
);
|
|
|
|
|
});
|
2025-05-20 13:02:41 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('execute', () => {
|
|
|
|
|
const createFile = (filePath: string, content = '') => {
|
|
|
|
|
const fullPath = path.join(tempRootDir, filePath);
|
|
|
|
|
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
|
|
|
fs.writeFileSync(fullPath, content);
|
|
|
|
|
};
|
|
|
|
|
const createBinaryFile = (filePath: string, data: Uint8Array) => {
|
|
|
|
|
const fullPath = path.join(tempRootDir, filePath);
|
|
|
|
|
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
|
|
|
fs.writeFileSync(fullPath, data);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
it('should read a single specified file', async () => {
|
|
|
|
|
createFile('file1.txt', 'Content of file1');
|
|
|
|
|
const params = { paths: ['file1.txt'] };
|
|
|
|
|
const result = await tool.execute(params, new AbortController().signal);
|
|
|
|
|
expect(result.llmContent).toEqual([
|
|
|
|
|
'--- file1.txt ---\n\nContent of file1\n\n',
|
|
|
|
|
]);
|
|
|
|
|
expect(result.returnDisplay).toContain(
|
|
|
|
|
'Successfully read and concatenated content from **1 file(s)**',
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should read multiple specified files', async () => {
|
|
|
|
|
createFile('file1.txt', 'Content1');
|
|
|
|
|
createFile('subdir/file2.js', 'Content2');
|
|
|
|
|
const params = { paths: ['file1.txt', 'subdir/file2.js'] };
|
|
|
|
|
const result = await tool.execute(params, new AbortController().signal);
|
|
|
|
|
const content = result.llmContent as string[];
|
|
|
|
|
expect(
|
|
|
|
|
content.some((c) => c.includes('--- file1.txt ---\n\nContent1\n\n')),
|
|
|
|
|
).toBe(true);
|
|
|
|
|
expect(
|
|
|
|
|
content.some((c) =>
|
|
|
|
|
c.includes('--- subdir/file2.js ---\n\nContent2\n\n'),
|
|
|
|
|
),
|
|
|
|
|
).toBe(true);
|
|
|
|
|
expect(result.returnDisplay).toContain(
|
|
|
|
|
'Successfully read and concatenated content from **2 file(s)**',
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should handle glob patterns', async () => {
|
|
|
|
|
createFile('file.txt', 'Text file');
|
|
|
|
|
createFile('another.txt', 'Another text');
|
|
|
|
|
createFile('sub/data.json', '{}');
|
|
|
|
|
const params = { paths: ['*.txt'] };
|
|
|
|
|
const result = await tool.execute(params, new AbortController().signal);
|
|
|
|
|
const content = result.llmContent as string[];
|
|
|
|
|
expect(
|
|
|
|
|
content.some((c) => c.includes('--- file.txt ---\n\nText file\n\n')),
|
|
|
|
|
).toBe(true);
|
|
|
|
|
expect(
|
|
|
|
|
content.some((c) =>
|
|
|
|
|
c.includes('--- another.txt ---\n\nAnother text\n\n'),
|
|
|
|
|
),
|
|
|
|
|
).toBe(true);
|
|
|
|
|
expect(content.find((c) => c.includes('sub/data.json'))).toBeUndefined();
|
|
|
|
|
expect(result.returnDisplay).toContain(
|
|
|
|
|
'Successfully read and concatenated content from **2 file(s)**',
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should respect exclude patterns', async () => {
|
|
|
|
|
createFile('src/main.ts', 'Main content');
|
|
|
|
|
createFile('src/main.test.ts', 'Test content');
|
|
|
|
|
const params = { paths: ['src/**/*.ts'], exclude: ['**/*.test.ts'] };
|
|
|
|
|
const result = await tool.execute(params, new AbortController().signal);
|
|
|
|
|
const content = result.llmContent as string[];
|
|
|
|
|
expect(content).toEqual(['--- src/main.ts ---\n\nMain content\n\n']);
|
|
|
|
|
expect(
|
|
|
|
|
content.find((c) => c.includes('src/main.test.ts')),
|
|
|
|
|
).toBeUndefined();
|
|
|
|
|
expect(result.returnDisplay).toContain(
|
|
|
|
|
'Successfully read and concatenated content from **1 file(s)**',
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should handle non-existent specific files gracefully', async () => {
|
|
|
|
|
const params = { paths: ['nonexistent-file.txt'] };
|
|
|
|
|
const result = await tool.execute(params, new AbortController().signal);
|
|
|
|
|
expect(result.llmContent).toEqual([
|
|
|
|
|
'No files matching the criteria were found or all were skipped.',
|
|
|
|
|
]);
|
|
|
|
|
expect(result.returnDisplay).toContain(
|
|
|
|
|
'No files were read and concatenated based on the criteria.',
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should use default excludes', async () => {
|
|
|
|
|
createFile('node_modules/some-lib/index.js', 'lib code');
|
|
|
|
|
createFile('src/app.js', 'app code');
|
|
|
|
|
const params = { paths: ['**/*.js'] };
|
|
|
|
|
const result = await tool.execute(params, new AbortController().signal);
|
|
|
|
|
const content = result.llmContent as string[];
|
|
|
|
|
expect(content).toEqual(['--- src/app.js ---\n\napp code\n\n']);
|
|
|
|
|
expect(
|
|
|
|
|
content.find((c) => c.includes('node_modules/some-lib/index.js')),
|
|
|
|
|
).toBeUndefined();
|
|
|
|
|
expect(result.returnDisplay).toContain(
|
|
|
|
|
'Successfully read and concatenated content from **1 file(s)**',
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should NOT use default excludes if useDefaultExcludes is false', async () => {
|
|
|
|
|
createFile('node_modules/some-lib/index.js', 'lib code');
|
|
|
|
|
createFile('src/app.js', 'app code');
|
|
|
|
|
const params = { paths: ['**/*.js'], useDefaultExcludes: false };
|
|
|
|
|
const result = await tool.execute(params, new AbortController().signal);
|
|
|
|
|
const content = result.llmContent as string[];
|
|
|
|
|
expect(
|
|
|
|
|
content.some((c) =>
|
|
|
|
|
c.includes('--- node_modules/some-lib/index.js ---\n\nlib code\n\n'),
|
|
|
|
|
),
|
|
|
|
|
).toBe(true);
|
|
|
|
|
expect(
|
|
|
|
|
content.some((c) => c.includes('--- src/app.js ---\n\napp code\n\n')),
|
|
|
|
|
).toBe(true);
|
|
|
|
|
expect(result.returnDisplay).toContain(
|
|
|
|
|
'Successfully read and concatenated content from **2 file(s)**',
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should include images as inlineData parts if explicitly requested by extension', async () => {
|
|
|
|
|
createBinaryFile(
|
|
|
|
|
'image.png',
|
|
|
|
|
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
|
|
|
|
|
);
|
|
|
|
|
const params = { paths: ['*.png'] }; // Explicitly requesting .png
|
|
|
|
|
const result = await tool.execute(params, new AbortController().signal);
|
|
|
|
|
expect(result.llmContent).toEqual([
|
|
|
|
|
{
|
|
|
|
|
inlineData: {
|
|
|
|
|
data: Buffer.from([
|
|
|
|
|
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
|
|
|
|
|
]).toString('base64'),
|
|
|
|
|
mimeType: 'image/png',
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
expect(result.returnDisplay).toContain(
|
|
|
|
|
'Successfully read and concatenated content from **1 file(s)**',
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should include images as inlineData parts if explicitly requested by name', async () => {
|
|
|
|
|
createBinaryFile(
|
|
|
|
|
'myExactImage.png',
|
|
|
|
|
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
|
|
|
|
|
);
|
|
|
|
|
const params = { paths: ['myExactImage.png'] }; // Explicitly requesting by full name
|
|
|
|
|
const result = await tool.execute(params, new AbortController().signal);
|
|
|
|
|
expect(result.llmContent).toEqual([
|
|
|
|
|
{
|
|
|
|
|
inlineData: {
|
|
|
|
|
data: Buffer.from([
|
|
|
|
|
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
|
|
|
|
|
]).toString('base64'),
|
|
|
|
|
mimeType: 'image/png',
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should skip PDF files if not explicitly requested by extension or name', async () => {
|
|
|
|
|
createBinaryFile('document.pdf', Buffer.from('%PDF-1.4...'));
|
|
|
|
|
createFile('notes.txt', 'text notes');
|
|
|
|
|
const params = { paths: ['*'] }; // Generic glob, not specific to .pdf
|
|
|
|
|
const result = await tool.execute(params, new AbortController().signal);
|
|
|
|
|
const content = result.llmContent as string[];
|
|
|
|
|
expect(
|
|
|
|
|
content.some(
|
|
|
|
|
(c) => typeof c === 'string' && c.includes('--- notes.txt ---'),
|
|
|
|
|
),
|
|
|
|
|
).toBe(true);
|
2025-05-29 14:03:24 -07:00
|
|
|
expect(result.returnDisplay).toContain('**Skipped 1 item(s):**');
|
2025-05-20 13:02:41 -07:00
|
|
|
expect(result.returnDisplay).toContain(
|
|
|
|
|
'- `document.pdf` (Reason: asset file (image/pdf) was not explicitly requested by name or extension)',
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should include PDF files as inlineData parts if explicitly requested by extension', async () => {
|
|
|
|
|
createBinaryFile('important.pdf', Buffer.from('%PDF-1.4...'));
|
|
|
|
|
const params = { paths: ['*.pdf'] }; // Explicitly requesting .pdf files
|
|
|
|
|
const result = await tool.execute(params, new AbortController().signal);
|
|
|
|
|
expect(result.llmContent).toEqual([
|
|
|
|
|
{
|
|
|
|
|
inlineData: {
|
|
|
|
|
data: Buffer.from('%PDF-1.4...').toString('base64'),
|
|
|
|
|
mimeType: 'application/pdf',
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should include PDF files as inlineData parts if explicitly requested by name', async () => {
|
|
|
|
|
createBinaryFile('report-final.pdf', Buffer.from('%PDF-1.4...'));
|
|
|
|
|
const params = { paths: ['report-final.pdf'] };
|
|
|
|
|
const result = await tool.execute(params, new AbortController().signal);
|
|
|
|
|
expect(result.llmContent).toEqual([
|
|
|
|
|
{
|
|
|
|
|
inlineData: {
|
|
|
|
|
data: Buffer.from('%PDF-1.4...').toString('base64'),
|
|
|
|
|
mimeType: 'application/pdf',
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
});
|
2025-06-05 10:15:27 -07:00
|
|
|
|
|
|
|
|
it('should return error if path is ignored by a .geminiignore pattern', async () => {
|
|
|
|
|
createFile('foo.bar', '');
|
|
|
|
|
createFile('qux/foo.baz', '');
|
|
|
|
|
createFile('foo.quux', '');
|
|
|
|
|
const params = { paths: ['foo.bar', 'qux/foo.baz', 'foo.quux'] };
|
|
|
|
|
const result = await tool.execute(params, new AbortController().signal);
|
|
|
|
|
expect(result.returnDisplay).not.toContain('foo.bar');
|
|
|
|
|
expect(result.returnDisplay).toContain('qux/foo.baz');
|
|
|
|
|
expect(result.returnDisplay).not.toContain('foo.quux');
|
|
|
|
|
});
|
2025-05-20 13:02:41 -07:00
|
|
|
});
|
|
|
|
|
});
|