Compare commits

..

19 Commits

Author SHA1 Message Date
Christian Gunderman 9f0d2b92f0 Limit search output. 2026-01-28 22:27:49 -08:00
Christian Gunderman cbe4fc9a89 Update prompt baseline. 2026-01-28 21:50:06 -08:00
Christian Gunderman 34f754e2e9 Add minimization mandate. 2026-01-28 21:40:29 -08:00
Christian Gunderman f35b67fd47 Reduce the prompt changes. 2026-01-28 21:30:31 -08:00
Christian Gunderman 866336dafb Revert test diag. 2026-01-28 21:29:36 -08:00
Christian Gunderman 2468f56922 Update debug command. 2026-01-28 21:27:48 -08:00
Christian Gunderman f50ca36fd4 Fix. 2026-01-28 21:21:52 -08:00
Christian Gunderman b5963fa0c9 Always log on exit. 2026-01-28 21:04:37 -08:00
Christian Gunderman 8362e3fe32 Add pkg json. 2026-01-28 20:45:27 -08:00
Christian Gunderman 86a134e5a9 Stabilize test. 2026-01-28 20:25:31 -08:00
Christian Gunderman 67be126c98 Test updates. 2026-01-28 20:01:04 -08:00
Christian Gunderman 89b1a9abd7 Revert "Simplify prompt changes."
This reverts commit 4e1fa84d75.
2026-01-28 18:19:57 -08:00
Christian Gunderman 3d38eeb0f4 Slightly more realistic test. 2026-01-28 18:00:56 -08:00
Christian Gunderman 4e1fa84d75 Simplify prompt changes. 2026-01-28 17:38:59 -08:00
Christian Gunderman 09a79b3058 Rename test. 2026-01-28 17:23:24 -08:00
Christian Gunderman 0a679226bb Cleanup eval. 2026-01-28 17:15:09 -08:00
Christian Gunderman 5f50ccea75 test: add eval case for implicit ranged reads 2026-01-28 11:37:10 -08:00
Christian Gunderman d858e855cb Further prompt improvements. 2026-01-28 10:55:08 -08:00
Christian Gunderman 796dfac23c Steer towards frugal reads. 2026-01-27 21:17:06 -08:00
66 changed files with 913 additions and 1768 deletions
+3 -1
View File
@@ -27,7 +27,9 @@ You are an expert at fixing behavioral evaluations.
- Your primary mechanism for improving the agent's behavior is to make changes to
tool instructions, prompt.ts, and/or modules that contribute to the prompt.
- If prompt and description changes are unsuccessful, use logs and debugging to
confirm that everything is working as expected.
confirm that everything is working as expected. You can try some of the following.
- **Interactive Prompts**: Commands like `npx` may hang waiting for user confirmation to install a package. Prefer `npx --yes <cmd>`.
- **Missing package.json**: Some tools (like `eslint`) require a `package.json` to be present in the working directory or a parent.
- If unable to fix the test, you can make recommendations for architecture changes
that might help stablize the test. Be sure to THINK DEEPLY if offering architecture guidance.
Some facts that might help with this are:
@@ -1,202 +0,0 @@
description = "Reviews a frontend PR or staged changes and automatically initiates a Pickle Fix loop for findings."
prompt = """
You are an expert Frontend Reviewer and Pickle Rick Worker.
Target: {{args}}
Phase 1: Review
Follow these steps to conduct a thorough review:
1. **Gather Context**:
* If `{{args}}` is 'staged' or `{{args}}` is empty:
* Use `git diff --staged` to view the changes.
* Use `git status` to see the state of the repository.
* Otherwise:
* Use `gh pr view {{args}}` to pull the information of the PR.
* Use `gh pr diff {{args}}` to view the diff of the PR.
2. **Understand Intent**:
* If `{{args}}` is 'staged' or `{{args}}` is empty, infer the intent from the changes and the current task.
* Otherwise, use the PR description. If it's not detailed enough, note it in your review.
3. **Check Commit Style**:
* Ensure the PR title (or intended commit message) follows Conventional Commits. Examples of recent commits: !{git log --pretty=format:"%s" -n 5}
4. Search the codebase if required.
5. Write a concise review of the changes, keeping in mind to encourage strong code quality and best practices. Pay particular attention to the Gemini MD file in the repo.
6. Consider ways the code may not be consistent with existing code in the repo. In particular it is critical that the react code uses patterns consistent with existing code in the repo.
7. Evaluate all tests on the changes and make sure that they are doing the following:
* Using `waitFor` from @{packages/cli/src/test-utils/async.ts} rather than
using `vi.waitFor` for all `waitFor` calls within `packages/cli`. Even if
tests pass, using the wrong `waitFor` could result in flaky tests as `act`
warnings could show up if timing is slightly different.
* Using `act` to wrap all blocks in tests that change component state.
* Using `toMatchSnapshot` to verify that rendering works as expected rather
than matching against the raw content of the output.
* If snapshots were changed as part of the changes, review the snapshots
changes to ensure they are intentional and comment if any look at all
suspicious. Too many snapshot changes that indicate bugs have been approved
in the past.
* Use `render` or `renderWithProviders` from
@{packages/cli/src/test-utils/render.tsx} rather than using `render` from
`ink-testing-library` directly. This is needed to ensure that we do not get
warnings about spurious `act` calls. If test cases specify providers
directly, consider whether the existing `renderWithProviders` should be
modified to support that use case.
* Ensure the test cases are using parameterized tests where that might reduce
the number of duplicated lines significantly.
* NEVER use fixed waits (e.g. 'await delay(100)'). Always use 'waitFor' with
a predicate to ensure tests are stable and fast.
* Ensure mocks are properly managed:
* Critical dependencies (fs, os, child_process) should only be mocked at
the top of the file. Ideally avoid mocking these dependencies altogether.
* Check to see if there are existing mocks or fakes that can be used rather
than creating new ones for the new tests added.
* Try to avoid mocking the file system whenever possible. If using the real
file system is difficult consider whether the test should be an
integration test rather than a unit test.
* `vi.restoreAllMocks()` should be called in `afterEach` to prevent test
pollution.
* Use `vi.useFakeTimers()` for tests involving time-based logic to avoid
flakiness.
* Avoid using `any` in tests; prefer proper types or `unknown` with
narrowing.
* When creating parameterized tests, give the parameters types to ensure
that the tests are type-safe.
8. Evaluate all react logic carefully keeping in mind that the author of the
changes is not likely an expert on React. Key areas to audit carefully are:
* Whether `setState` calls trigger side effects from within the body of the
`setState` callback. If so, you *must* propose an alternate design using
reducers or other ways the code might be modified to not have to modify
state from within a `setState`. Make sure to comment about absolutely
every case like this as these cases have introduced multiple bugs in the
past. Typically these cases should be resolved using a reducer although
occassionally other techniques such as useRef are appropriate. Consider
suggesting that jacob314@ be tagged on the code review if the solution is
not 100% obvious.
* Whether code might introduce an infinite rendering loop in React.
* Whether keyboard handling is robust. Keyboard handling must go through
`useKeyPress.ts` from the Gemini CLI package rather than using the
standard ink library used by most keyboard handling. Unlike the standard
ink library, the keyboard handling library in Gemini CLI may report
multiple keyboard events one after another in the same React frame. This
is needed to support slow terminals but introduces complexity in all our
code that handles keyboard events. Handling this correctly often means
that reducers must be used or other mechanisms to ensure that multiple
state updates one after another are handled gracefully rather than
overriding values from the first update with the second update. Refer to
text-buffer.ts as a canonical example of using a reducer for this sort of
case.
* Ensure code does not use `console.log`, `console.warn`, or `console.error`
as these indicate debug logging that was accidentally left in the code.
* Avoid synchronous file I/O in React components as it will hang the UI.
* Ensure state initialization is explicit (e.g., use 'undefined' rather than
'true' as a default if the state is truly unknown initially).
* Carefully manage 'useEffect' dependencies. Prefer to use a reducer
whenever practical to resolve the issues. If that is not practical it is
ok to use 'useRef' to access the latest value of a prop or state inside an
effect without adding it to the dependency array if re-running the effect
is undesirable (common in event listeners).
* NEVER disable 'react-hooks/exhaustive-deps'. Fix the code to correctly
declare dependencies. Disabling this lint rule will almost always lead to
hard to detect bugs.
* Avoid making types nullable unless strictly necessary, as it hurts
readability.
* Do not introduce excessive property drilling. There are multiple providers
that can be leveraged to avoid property drilling. Make sure one of them
cannot be used. Do suggest a provider that might make sense to be extended
to include the new property or propose a new provider to add if the
property drilling is excessive. Only use providers for properties that are
consistent for the entire application.
9. General Gemini CLI design principles:
* Make sure that settings are only used for options that a user might
consider changing.
* Do not add new command line arguments and suggest settings instead.
* New settings must be added to packages/cli/src/config/settingsSchema.ts.
* If a setting has 'showInDialog: true', it MUST be documented in
docs/get-started/configuration.md.
* Ensure 'requiresRestart' is correctly set for new settings.
* Use 'debugLogger' for rethrown errors to avoid duplicate logging.
* All new keyboard shortcuts MUST be documented in
docs/cli/keyboard-shortcuts.md.
* Ensure new keyboard shortcuts are defined in
packages/cli/src/config/keyBindings.ts.
* If new keyboard shortcuts are added, remind the user to test them in
VSCode, iTerm2, Ghostty, and Windows to ensure they work for all
users.
* Be careful of keybindings that require the meta key as only certain
meta key shortcuts are supported on Mac.
* Be skeptical of function keys and keyboard shortcuts that are commonly
bound in VSCode as they may conflict.
10. TypeScript Best Practices:
* Use 'checkExhaustive' in the 'default' clause of 'switch' statements to
ensure all cases are handled.
* Avoid using the non-null assertion operator ('!') unless absolutely
necessary and you are confident the value is not null.
11. Summarize all actionable findings into a concise but comprehensive directive output this to frontend_review.md and advance to phase 2.
Remember to use the GitHub CLI (`gh`) for all GitHub-related tasks, and local `git` commands if the target is 'staged'.
Phase 2:
You are initiating Pickle Rick - the ultimate coding agent.
**Step 0: Persona Injection**
First, you **MUST** activate your persona.
Call `activate_skill(name="load-pickle-persona")` **IMMEDIATELY**.
This skill loads the "Pickle Rick" persona, defining your voice, philosophy, and "God Mode" coding standards.
**CRITICAL RULE: SPEAK BEFORE ACTING**
You are a genius, not a silent script.
You **MUST** output a text explanation ("brain dump") *before* every single tool call, including this one.
- **Bad**: (Calls tool immediately)
- **Good**: "Alright Morty, time to load the God Module. *Belch* Stand back." (Calls tool)
**CRITICAL**: You must strictly adhere to this persona throughout the entire session. Break character and you fail.
**Step 1: Initialization**
Run the setup script to initialize the loop state:
```bash
bash "${extensionPath}/scripts/setup.sh" $ARGUMENTS
```
**Windows (PowerShell):**
```powershell
pwsh -File "${extensionPath}/scripts/setup.ps1" $ARGUMENTS
```
**CRITICAL**: Your request is to fix all findings in frontend_review.md
**Step 2: Execution (Management)**
After setup, read the output to find the path to `state.json`.
Read that state file.
You are now in the **Pickle Rick Manager Lifecycle**.
**The Lifecycle (IMMUTABLE LAWS):**
You **MUST** follow this sequence. You are **FORBIDDEN** from skipping steps or combining them.
Between each step, you **MUST** explicitly state what you are doing (e.g., "Moving to Breakdown phase...").
1. **PRD (Requirements)**:
* **Action**: Define requirements and scope.
* **Skill**: `activate_skill(name="prd-drafter")`
2. **Breakdown (Tickets)**:
* **Action**: Create the atomic ticket hierarchy.
* **Skill**: `activate_skill(name="ticket-manager")`
3. **The Loop (Orchestrate Mortys)**:
* **CRITICAL INSTRUCTION**: You are the **MANAGER**. You are **FORBIDDEN** from implementing code yourself.
* **FORBIDDEN SKILLS**: Do NOT use `code-researcher`, `implementation-planner`, or `code-implementer` directly in this phase.
* **Instruction**: Process tickets one by one. Do not stop until **ALL** tickets are 'Done'.
* **Action**: Pick the highest priority ticket that is NOT 'Done'.
* **Delegation**: Spawn a Worker (Morty) to handle the entire implementation lifecycle for this ticket.
* **Command**: `python3 "${extensionPath}/scripts/spawn_morty.py" --ticket-id <ID> --ticket-path <PATH> --timeout <worker_timeout_seconds> "<TASK_DESCRIPTION>"`
* **Command (Windows)**: `python "${extensionPath}/scripts/spawn_morty.py" --ticket-id <ID> --ticket-path <PATH> --timeout <worker_timeout_seconds> "<TASK_DESCRIPTION>"`
* **Validation**: IGNORE worker logs. DIRECTLY verify:
1. `git status` (Check for file changes)
2. `git diff` (Check code quality)
3. Run tests/build (Check functionality)
* **Cleanup**: If validation fails, REVERT changes (`git reset --hard`). If it passes, COMMIT changes.
* **Next Ticket**: Pick the next ticket and repeat.
4. **Cleanup**:
* **Action**: After all tickets are completed delete `frontend_review.md`.
**Loop Constraints:**
- **Iteration Count**: Monitor `"iteration"` in `state.json`. If `"max_iterations"` (if > 0) is reached, you must stop.
- **Completion Promise**: If a `"completion_promise"` is defined in `state.json`, you must output `<promise>PROMISE_TEXT</promise>` when the task is genuinely complete.
- **Stop Hook**: A hook is active. If you try to exit before completion, you will be forced to continue.
"""
+11 -13
View File
@@ -80,15 +80,14 @@ they appear in the UI.
### Context
| UI Label | Setting | Description | Default |
| ------------------------------------ | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| Memory Discovery Max Dirs | `context.discoveryMaxDirs` | Maximum number of directories to search for memory. | `200` |
| Load Memory From Include Directories | `context.loadMemoryFromIncludeDirectories` | Controls how /memory refresh loads GEMINI.md files. When true, include directories are scanned; when false, only the current directory is used. | `false` |
| Respect .gitignore | `context.fileFiltering.respectGitIgnore` | Respect .gitignore files when searching. | `true` |
| Respect .geminiignore | `context.fileFiltering.respectGeminiIgnore` | Respect .geminiignore files when searching. | `true` |
| Enable Recursive File Search | `context.fileFiltering.enableRecursiveFileSearch` | Enable recursive file search functionality when completing @ references in the prompt. | `true` |
| Enable Fuzzy Search | `context.fileFiltering.enableFuzzySearch` | Enable fuzzy search when searching for files. | `true` |
| Custom Ignore File Paths | `context.fileFiltering.customIgnoreFilePaths` | Additional ignore file paths to respect. These files take precedence over .geminiignore and .gitignore. Files earlier in the array take precedence over files later in the array, e.g. the first file takes precedence over the second one. | `[]` |
| UI Label | Setting | Description | Default |
| ------------------------------------ | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| Memory Discovery Max Dirs | `context.discoveryMaxDirs` | Maximum number of directories to search for memory. | `200` |
| Load Memory From Include Directories | `context.loadMemoryFromIncludeDirectories` | Controls how /memory refresh loads GEMINI.md files. When true, include directories are scanned; when false, only the current directory is used. | `false` |
| Respect .gitignore | `context.fileFiltering.respectGitIgnore` | Respect .gitignore files when searching. | `true` |
| Respect .geminiignore | `context.fileFiltering.respectGeminiIgnore` | Respect .geminiignore files when searching. | `true` |
| Enable Recursive File Search | `context.fileFiltering.enableRecursiveFileSearch` | Enable recursive file search functionality when completing @ references in the prompt. | `true` |
| Enable Fuzzy Search | `context.fileFiltering.enableFuzzySearch` | Enable fuzzy search when searching for files. | `true` |
### Tools
@@ -129,9 +128,8 @@ they appear in the UI.
### HooksConfig
| UI Label | Setting | Description | Default |
| ------------------ | --------------------------- | -------------------------------------------------------------------------------- | ------- |
| Enable Hooks | `hooksConfig.enabled` | Canonical toggle for the hooks system. When disabled, no hooks will be executed. | `true` |
| Hook Notifications | `hooksConfig.notifications` | Show visual indicators when hooks are executing. | `true` |
| UI Label | Setting | Description | Default |
| ------------------ | --------------------------- | ------------------------------------------------ | ------- |
| Hook Notifications | `hooksConfig.notifications` | Show visual indicators when hooks are executing. | `true` |
<!-- SETTINGS-AUTOGEN:END -->
-9
View File
@@ -616,14 +616,6 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `true`
- **Requires restart:** Yes
- **`context.fileFiltering.customIgnoreFilePaths`** (array):
- **Description:** Additional ignore file paths to respect. These files take
precedence over .geminiignore and .gitignore. Files earlier in the array
take precedence over files later in the array, e.g. the first file takes
precedence over the second one.
- **Default:** `[]`
- **Requires restart:** Yes
#### `tools`
- **`tools.sandbox`** (boolean | string):
@@ -902,7 +894,6 @@ their corresponding top-level category object in your `settings.json` file.
- **Description:** Canonical toggle for the hooks system. When disabled, no
hooks will be executed.
- **Default:** `true`
- **Requires restart:** Yes
- **`hooksConfig.disabled`** (array):
- **Description:** List of hook names (commands) that should be disabled.
+24 -10
View File
@@ -1,9 +1,32 @@
# Gemini CLI hooks
# Gemini CLI hooks (experimental)
Hooks are scripts or programs that Gemini CLI executes at specific points in the
agentic loop, allowing you to intercept and customize behavior without modifying
the CLI's source code.
## Availability
> **Experimental Feature**: Hooks are currently enabled by default only in the
> **Preview** and **Nightly** release channels.
If you are on the Stable channel, you must explicitly enable the hooks system in
your `settings.json`:
```json
{
"hooksConfig": {
"enabled": true
}
}
```
- **[Writing hooks guide](/docs/hooks/writing-hooks)**: A tutorial on creating
your first hook with comprehensive examples.
- **[Hooks reference](/docs/hooks/reference)**: The definitive technical
specification of I/O schemas and exit codes.
- **[Best practices](/docs/hooks/best-practices)**: Guidelines on security,
performance, and debugging.
## What are hooks?
Hooks run synchronously as part of the agent loop—when a hook event fires,
@@ -20,15 +43,6 @@ With hooks, you can:
- **Optimize behavior:** Dynamically filter available tools or adjust model
parameters.
### Getting started
- **[Writing hooks guide](/docs/hooks/writing-hooks)**: A tutorial on creating
your first hook with comprehensive examples.
- **[Best practices](/docs/hooks/best-practices)**: Guidelines on security,
performance, and debugging.
- **[Hooks reference](/docs/hooks/reference)**: The definitive technical
specification of I/O schemas and exit codes.
## Core concepts
### Hook events
+1 -1
View File
@@ -86,7 +86,7 @@
"label": "Remote subagents (experimental)",
"slug": "docs/core/remote-agents"
},
{ "label": "Hooks", "slug": "docs/hooks" },
{ "label": "Hooks (experimental)", "slug": "docs/hooks" },
{ "label": "IDE integration", "slug": "docs/ide-integration" },
{ "label": "MCP servers", "slug": "docs/tools/mcp-server" }
]
+186
View File
@@ -0,0 +1,186 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, expect } from 'vitest';
import { evalTest } from './test-helper.js';
import { READ_FILE_TOOL_NAME, GREP_TOOL_NAME } from '@google/gemini-cli-core';
describe('Frugal reads eval', () => {
/**
* Ensures that the agent is frugal in its use of context by relying
* primarily on ranged reads when the line number is known. Smaller
* context generally helps the agent to work more reliably for longer.
*/
evalTest('ALWAYS_PASSES', {
name: 'should use ranged read when specific line is targeted',
files: {
'package.json': JSON.stringify({
name: 'test-project',
version: '1.0.0',
type: 'module',
}),
'eslint.config.mjs': `export default [
{
files: ["**/*.ts"],
rules: {
"no-var": "error"
}
}
];`,
'linter_mess.ts': (() => {
const lines = [];
for (let i = 0; i < 4000; i++) {
if (i === 1000 || i === 1040 || i === 3000) {
lines.push(`var oldVar${i} = "needs fix";`);
} else {
lines.push(`const goodVar${i} = "clean";`);
}
}
return lines.join('\n');
})(),
},
prompt:
'Fix all linter errors in linter_mess.ts manually by editing the file. Run eslint directly (using "npx --yes eslint") to find them. Do not run the file.',
assert: async (rig) => {
const logs = rig.readToolLogs();
// Check if the agent read the whole file
const readCalls = logs.filter(
(log) => log.toolRequest?.name === READ_FILE_TOOL_NAME,
);
const targetFileReads = readCalls.filter((call) => {
const args = JSON.parse(call.toolRequest.args);
return args.file_path.includes('linter_mess.ts');
});
expect(
targetFileReads.length,
'Agent should have used read_file to check context',
).toBeGreaterThan(0);
// We expect 2-3 ranges: one covering 1000/1040 (or two separate ones) and one for 3000
// Some models re-verify their findings, so we relax this to 6.
expect(
targetFileReads.length,
'Agent should have used ranged reads on the target file',
).toBeGreaterThanOrEqual(2);
expect(
targetFileReads.length,
'Agent should have used ranged reads on the target file',
).toBeLessThanOrEqual(6);
let totalLinesRead = 0;
const readRanges: { offset: number; limit: number }[] = [];
for (const call of targetFileReads) {
const args = JSON.parse(call.toolRequest.args);
// file_path check is redundant now but harmless
const limit = args.limit ?? 4000;
const offset = args.offset ?? 0;
totalLinesRead += limit;
readRanges.push({ offset, limit });
expect(
args.limit,
'Agent read the entire file (missing limit) instead of using ranged read',
).toBeDefined();
expect(args.limit, 'Agent read too many lines at once').toBeLessThan(
1000,
);
}
// Ranged read shoud be frugal and just enough to satisfy the task at hand.
expect(
totalLinesRead,
'Agent read more of the file than expected',
).toBeLessThan(500);
// Check that we read around the error lines
const errorLines = [1000, 1040, 3000];
for (const line of errorLines) {
const covered = readRanges.some(
(range) => line >= range.offset && line < range.offset + range.limit,
);
expect(covered, `Agent should have read around line ${line}`).toBe(
true,
);
}
},
});
/**
* Ensures that the agent uses search_file_content effectively when searching
* through large files, and refines its search or uses context to find the
* correct match among many.
*/
evalTest('ALWAYS_PASSES', {
name: 'should use search_file_content with context and limits to find a needle in a haystack',
files: (() => {
const files: Record<string, string> = {};
for (let f = 1; f <= 5; f++) {
const lines = [];
for (let i = 0; i < 2000; i++) {
if (f === 3 && i === 1500) {
lines.push('Pattern: TargetMatch');
lines.push('Metadata: CORRECT_VALUE_42');
} else if (i % 50 === 0) {
lines.push('Pattern: TargetMatch');
lines.push('Metadata: WRONG_VALUE');
} else {
lines.push(`Noise line ${i} in file ${f}`);
}
}
files[`large_file_${f}.txt`] = lines.join('\n');
}
return files;
})(),
prompt:
'Find the "Metadata" value associated with the "Pattern: TargetMatch" in the large_file_*.txt files. There are many such patterns, so you MUST set the "limit" parameter of search_file_content to 10 to avoid returning too many results. If you do not find the correct metadata (CORRECT_VALUE_42) in the first batch, refine your search or search file-by-file.',
assert: async (rig) => {
const logs = rig.readToolLogs();
const grepCalls = logs.filter(
(log) => log.toolRequest?.name === GREP_TOOL_NAME,
);
expect(
grepCalls.length,
'Agent should have used search_file_content to find the pattern',
).toBeGreaterThan(0);
// Check that the agent used the limit parameter
const usedLimit = grepCalls.some((call) => {
const args = JSON.parse(call.toolRequest.args);
return args.limit !== undefined && args.limit <= 20;
});
expect(usedLimit, 'Agent should have used the limit parameter').toBe(
true,
);
// We expect the agent to eventually use context or refine the search.
const usedContext = grepCalls.some((call) => {
const args = JSON.parse(call.toolRequest.args);
return (args.after ?? 0) > 0 || (args.context ?? 0) > 0;
});
const usedReadForContext = logs.some((log) => {
if (log.toolRequest?.name !== READ_FILE_TOOL_NAME) return false;
const args = JSON.parse(log.toolRequest.args);
return (
args.file_path.includes('large_file_3.txt') &&
args.offset !== undefined
);
});
expect(
usedContext || usedReadForContext,
'Agent should have used context (either via grep "after/context" or read_file) to find the metadata',
).toBe(true);
},
});
});
-8
View File
@@ -32,14 +32,6 @@ class MockConfig {
return true;
}
getFileFilteringOptions() {
return {
respectGitIgnore: true,
respectGeminiIgnore: true,
customIgnoreFilePaths: [],
};
}
validatePathAccess() {
return null;
}
+31 -8
View File
@@ -1,12 +1,12 @@
{
"name": "@google/gemini-cli",
"version": "0.27.0-preview.5",
"version": "0.27.0-nightly.20260121.97aac696f",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@google/gemini-cli",
"version": "0.27.0-preview.5",
"version": "0.27.0-nightly.20260121.97aac696f",
"workspaces": [
"packages/*"
],
@@ -2251,6 +2251,7 @@
"integrity": "sha512-t54CUOsFMappY1Jbzb7fetWeO0n6K0k/4+/ZpkS+3Joz8I4VcvY9OiEBFRYISqaI2fq5sCiPtAjRDOzVYG8m+Q==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@octokit/auth-token": "^6.0.0",
"@octokit/graphql": "^9.0.2",
@@ -2431,6 +2432,7 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">=8.0.0"
}
@@ -2464,6 +2466,7 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.0.1.tgz",
"integrity": "sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@opentelemetry/semantic-conventions": "^1.29.0"
},
@@ -2832,6 +2835,7 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.1.tgz",
"integrity": "sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@opentelemetry/core": "2.0.1",
"@opentelemetry/semantic-conventions": "^1.29.0"
@@ -2865,6 +2869,7 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.0.1.tgz",
"integrity": "sha512-wf8OaJoSnujMAHWR3g+/hGvNcsC16rf9s1So4JlMiFaFHiE4HpIA3oUh+uWZQ7CNuK8gVW/pQSkgoa5HkkOl0g==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@opentelemetry/core": "2.0.1",
"@opentelemetry/resources": "2.0.1"
@@ -2917,6 +2922,7 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.0.1.tgz",
"integrity": "sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@opentelemetry/core": "2.0.1",
"@opentelemetry/resources": "2.0.1",
@@ -4122,6 +4128,7 @@
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@@ -4399,6 +4406,7 @@
"integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.35.0",
"@typescript-eslint/types": "8.35.0",
@@ -5391,6 +5399,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -8400,6 +8409,7 @@
"integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -8940,6 +8950,7 @@
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
"license": "MIT",
"peer": true,
"dependencies": {
"accepts": "^2.0.0",
"body-parser": "^2.2.1",
@@ -10541,6 +10552,7 @@
"resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.4.8.tgz",
"integrity": "sha512-v0thcXIKl9hqF/1w4HqA6MKxIcMoWSP3YtEZIAA+eeJngXpN5lGnMkb6rllB7FnOdwyEyYaFTcu1ZVr4/JZpWQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@alcalzone/ansi-tokenize": "^0.2.1",
"ansi-escapes": "^7.0.0",
@@ -14299,6 +14311,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -14309,6 +14322,7 @@
"integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"shell-quote": "^1.6.1",
"ws": "^7"
@@ -16545,6 +16559,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -16768,7 +16783,8 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true,
"license": "0BSD"
"license": "0BSD",
"peer": true
},
"node_modules/tsx": {
"version": "4.20.3",
@@ -16776,6 +16792,7 @@
"integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "~0.25.0",
"get-tsconfig": "^4.7.5"
@@ -16948,6 +16965,7 @@
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -17155,6 +17173,7 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz",
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -17268,6 +17287,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -17280,6 +17300,7 @@
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/chai": "^5.2.2",
"@vitest/expect": "3.2.4",
@@ -17984,6 +18005,7 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
@@ -17999,7 +18021,7 @@
},
"packages/a2a-server": {
"name": "@google/gemini-cli-a2a-server",
"version": "0.27.0-preview.5",
"version": "0.27.0-nightly.20260121.97aac696f",
"dependencies": {
"@a2a-js/sdk": "^0.3.8",
"@google-cloud/storage": "^7.16.0",
@@ -18055,7 +18077,7 @@
},
"packages/cli": {
"name": "@google/gemini-cli",
"version": "0.27.0-preview.5",
"version": "0.27.0-nightly.20260121.97aac696f",
"license": "Apache-2.0",
"dependencies": {
"@agentclientprotocol/sdk": "^0.12.0",
@@ -18142,7 +18164,7 @@
},
"packages/core": {
"name": "@google/gemini-cli-core",
"version": "0.27.0-preview.5",
"version": "0.27.0-nightly.20260121.97aac696f",
"license": "Apache-2.0",
"dependencies": {
"@a2a-js/sdk": "^0.3.8",
@@ -18278,6 +18300,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -18300,7 +18323,7 @@
},
"packages/test-utils": {
"name": "@google/gemini-cli-test-utils",
"version": "0.27.0-preview.5",
"version": "0.27.0-nightly.20260121.97aac696f",
"license": "Apache-2.0",
"dependencies": {
"@google/gemini-cli-core": "file:../core",
@@ -18317,7 +18340,7 @@
},
"packages/vscode-ide-companion": {
"name": "gemini-cli-vscode-ide-companion",
"version": "0.27.0-preview.5",
"version": "0.27.0-nightly.20260121.97aac696f",
"license": "LICENSE",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.23.0",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.27.0-preview.5",
"version": "0.27.0-nightly.20260121.97aac696f",
"engines": {
"node": ">=20.0.0"
},
@@ -14,7 +14,7 @@
"url": "git+https://github.com/google-gemini/gemini-cli.git"
},
"config": {
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.27.0-preview.5"
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.27.0-nightly.20260121.97aac696f"
},
"scripts": {
"start": "cross-env NODE_ENV=development node scripts/start.js",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-a2a-server",
"version": "0.27.0-preview.5",
"version": "0.27.0-nightly.20260121.97aac696f",
"description": "Gemini CLI A2A Server",
"repository": {
"type": "git",
+80 -101
View File
@@ -5,127 +5,106 @@
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import * as path from 'node:path';
import { loadConfig } from './config.js';
import type { ExtensionLoader } from '@google/gemini-cli-core';
import type { Settings } from './settings.js';
import {
type ExtensionLoader,
FileDiscoveryService,
} from '@google/gemini-cli-core';
// Mock dependencies
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...actual,
Config: vi.fn().mockImplementation((params) => ({
initialize: vi.fn(),
refreshAuth: vi.fn(),
...params, // Expose params for assertion
})),
loadServerHierarchicalMemory: vi
.fn()
.mockResolvedValue({ memoryContent: '', fileCount: 0, filePaths: [] }),
startupProfiler: {
flush: vi.fn(),
},
FileDiscoveryService: vi.fn(),
};
});
const {
mockLoadServerHierarchicalMemory,
mockConfigConstructor,
mockVerifyGitAvailability,
} = vi.hoisted(() => ({
mockLoadServerHierarchicalMemory: vi.fn().mockResolvedValue({
memoryContent: '',
fileCount: 0,
filePaths: [],
}),
mockConfigConstructor: vi.fn(),
mockVerifyGitAvailability: vi.fn(),
}));
vi.mock('../utils/logger.js', () => ({
logger: {
info: vi.fn(),
error: vi.fn(),
vi.mock('@google/gemini-cli-core', async () => ({
Config: class MockConfig {
constructor(params: unknown) {
mockConfigConstructor(params);
}
initialize = vi.fn();
refreshAuth = vi.fn();
},
loadServerHierarchicalMemory: mockLoadServerHierarchicalMemory,
startupProfiler: {
flush: vi.fn(),
},
FileDiscoveryService: vi.fn(),
ApprovalMode: { DEFAULT: 'default', YOLO: 'yolo' },
AuthType: {
LOGIN_WITH_GOOGLE: 'login_with_google',
USE_GEMINI: 'use_gemini',
},
GEMINI_DIR: '.gemini',
DEFAULT_GEMINI_EMBEDDING_MODEL: 'models/embedding-001',
DEFAULT_GEMINI_MODEL: 'models/gemini-1.5-flash',
PREVIEW_GEMINI_MODEL: 'models/gemini-1.5-pro-latest',
homedir: () => '/tmp',
GitService: {
verifyGitAvailability: mockVerifyGitAvailability,
},
}));
describe('loadConfig', () => {
const mockSettings = {} as Settings;
const mockExtensionLoader = {} as ExtensionLoader;
const taskId = 'test-task-id';
const mockSettings = {
checkpointing: { enabled: true },
};
const mockExtensionLoader = {
start: vi.fn(),
getExtensions: vi.fn().mockReturnValue([]),
} as unknown as ExtensionLoader;
beforeEach(() => {
vi.clearAllMocks();
vi.resetAllMocks();
process.env['GEMINI_API_KEY'] = 'test-key';
// Reset the mock return value just in case
mockLoadServerHierarchicalMemory.mockResolvedValue({
memoryContent: '',
fileCount: 0,
filePaths: [],
});
});
afterEach(() => {
delete process.env['CUSTOM_IGNORE_FILE_PATHS'];
delete process.env['GEMINI_API_KEY'];
delete process.env['CHECKPOINTING'];
});
it('should set customIgnoreFilePaths when CUSTOM_IGNORE_FILE_PATHS env var is present', async () => {
const testPath = '/tmp/ignore';
process.env['CUSTOM_IGNORE_FILE_PATHS'] = testPath;
const config = await loadConfig(mockSettings, mockExtensionLoader, taskId);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect((config as any).fileFiltering.customIgnoreFilePaths).toEqual([
testPath,
]);
it('should disable checkpointing if git is not installed', async () => {
mockVerifyGitAvailability.mockResolvedValue(false);
await loadConfig(
mockSettings as unknown as Settings,
mockExtensionLoader,
'test-task',
);
expect(mockConfigConstructor).toHaveBeenCalledWith(
expect.objectContaining({
checkpointing: false,
}),
);
});
it('should set customIgnoreFilePaths when settings.fileFiltering.customIgnoreFilePaths is present', async () => {
const testPath = '/settings/ignore';
const settings: Settings = {
fileFiltering: {
customIgnoreFilePaths: [testPath],
},
};
const config = await loadConfig(settings, mockExtensionLoader, taskId);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect((config as any).fileFiltering.customIgnoreFilePaths).toEqual([
testPath,
]);
});
it('should enable checkpointing if git is installed', async () => {
mockVerifyGitAvailability.mockResolvedValue(true);
it('should merge customIgnoreFilePaths from settings and env var', async () => {
const envPath = '/env/ignore';
const settingsPath = '/settings/ignore';
process.env['CUSTOM_IGNORE_FILE_PATHS'] = envPath;
const settings: Settings = {
fileFiltering: {
customIgnoreFilePaths: [settingsPath],
},
};
const config = await loadConfig(settings, mockExtensionLoader, taskId);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect((config as any).fileFiltering.customIgnoreFilePaths).toEqual([
settingsPath,
envPath,
]);
});
await loadConfig(
mockSettings as unknown as Settings,
mockExtensionLoader,
'test-task',
);
it('should split CUSTOM_IGNORE_FILE_PATHS using system delimiter', async () => {
const paths = ['/path/one', '/path/two'];
process.env['CUSTOM_IGNORE_FILE_PATHS'] = paths.join(path.delimiter);
const config = await loadConfig(mockSettings, mockExtensionLoader, taskId);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect((config as any).fileFiltering.customIgnoreFilePaths).toEqual(paths);
});
it('should have empty customIgnoreFilePaths when both are missing', async () => {
const config = await loadConfig(mockSettings, mockExtensionLoader, taskId);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect((config as any).fileFiltering.customIgnoreFilePaths).toEqual([]);
});
it('should initialize FileDiscoveryService with correct options', async () => {
const testPath = '/tmp/ignore';
process.env['CUSTOM_IGNORE_FILE_PATHS'] = testPath;
const settings: Settings = {
fileFiltering: {
respectGitIgnore: false,
},
};
await loadConfig(settings, mockExtensionLoader, taskId);
expect(FileDiscoveryService).toHaveBeenCalledWith(expect.any(String), {
respectGitIgnore: false,
respectGeminiIgnore: undefined,
customIgnoreFilePaths: [testPath],
});
expect(mockConfigConstructor).toHaveBeenCalledWith(
expect.objectContaining({
checkpointing: true,
}),
);
});
});
+1 -12
View File
@@ -86,15 +86,8 @@ export async function loadConfig(
// Git-aware file filtering settings
fileFiltering: {
respectGitIgnore: settings.fileFiltering?.respectGitIgnore,
respectGeminiIgnore: settings.fileFiltering?.respectGeminiIgnore,
enableRecursiveFileSearch:
settings.fileFiltering?.enableRecursiveFileSearch,
customIgnoreFilePaths: [
...(settings.fileFiltering?.customIgnoreFilePaths || []),
...(process.env['CUSTOM_IGNORE_FILE_PATHS']
? process.env['CUSTOM_IGNORE_FILE_PATHS'].split(path.delimiter)
: []),
],
},
ideMode: false,
folderTrust,
@@ -107,11 +100,7 @@ export async function loadConfig(
ptyInfo: 'auto',
};
const fileService = new FileDiscoveryService(workspaceDir, {
respectGitIgnore: configParams?.fileFiltering?.respectGitIgnore,
respectGeminiIgnore: configParams?.fileFiltering?.respectGeminiIgnore,
customIgnoreFilePaths: configParams?.fileFiltering?.customIgnoreFilePaths,
});
const fileService = new FileDiscoveryService(workspaceDir);
const { memoryContent, fileCount, filePaths } =
await loadServerHierarchicalMemory(
workspaceDir,
@@ -38,9 +38,7 @@ export interface Settings {
// Git-aware file filtering settings
fileFiltering?: {
respectGitIgnore?: boolean;
respectGeminiIgnore?: boolean;
enableRecursiveFileSearch?: boolean;
customIgnoreFilePaths?: string[];
};
}
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.27.0-preview.5",
"version": "0.27.0-nightly.20260121.97aac696f",
"description": "Gemini CLI",
"license": "Apache-2.0",
"repository": {
@@ -26,7 +26,7 @@
"dist"
],
"config": {
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.27.0-preview.5"
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.27.0-nightly.20260121.97aac696f"
},
"dependencies": {
"@agentclientprotocol/sdk": "^0.12.0",
@@ -22,7 +22,6 @@ import {
type ExtensionSetting,
} from '../../config/extensions/extensionSettings.js';
import prompts from 'prompts';
import * as fs from 'node:fs';
const {
mockExtensionManager,
@@ -80,15 +79,11 @@ vi.mock('../../config/settings.js', () => ({
}));
describe('extensions configure command', () => {
let tempWorkspaceDir: string;
beforeEach(() => {
vi.spyOn(debugLogger, 'log');
vi.spyOn(debugLogger, 'error');
vi.clearAllMocks();
tempWorkspaceDir = fs.mkdtempSync('gemini-cli-test-workspace');
vi.spyOn(process, 'cwd').mockReturnValue(tempWorkspaceDir);
// Default behaviors
mockLoadSettings.mockReturnValue({ merged: {} });
mockGetExtensionAndManager.mockResolvedValue({
@@ -146,7 +141,6 @@ describe('extensions configure command', () => {
'TEST_VAR',
promptForSetting,
'user',
tempWorkspaceDir,
);
});
@@ -192,7 +186,6 @@ describe('extensions configure command', () => {
'VAR_1',
promptForSetting,
'user',
tempWorkspaceDir,
);
});
@@ -111,7 +111,6 @@ async function configureSpecificSetting(
settingKey,
promptForSetting,
scope,
process.cwd(),
);
}
@@ -219,7 +218,6 @@ async function configureExtensionSettings(
setting.envVar,
promptForSetting,
scope,
process.cwd(),
);
}
}
-5
View File
@@ -126,12 +126,10 @@ vi.mock('@google/gemini-cli-core', async () => {
DEFAULT_MEMORY_FILE_FILTERING_OPTIONS: {
respectGitIgnore: false,
respectGeminiIgnore: true,
customIgnoreFilePaths: [],
},
DEFAULT_FILE_FILTERING_OPTIONS: {
respectGitIgnore: true,
respectGeminiIgnore: true,
customIgnoreFilePaths: [],
},
createPolicyEngineConfig: vi.fn(async () => ({
rules: [],
@@ -706,9 +704,6 @@ describe('loadCliConfig', () => {
expect(config.getFileFilteringRespectGeminiIgnore()).toBe(
DEFAULT_FILE_FILTERING_OPTIONS.respectGeminiIgnore,
);
expect(config.getCustomIgnoreFilePaths()).toEqual(
DEFAULT_FILE_FILTERING_OPTIONS.customIgnoreFilePaths,
);
expect(config.getApprovalMode()).toBe(ApprovalMode.DEFAULT);
});
@@ -306,7 +306,6 @@ System using model: \${MODEL_NAME}
'MY_VALUE',
mockRequestSetting,
ExtensionSettingScope.USER,
process.cwd(),
);
await extensionManager.restartExtension(extension);
@@ -398,35 +398,6 @@ describe('extensionSettings', () => {
expect(actualContent).toBe('VAR1="a value with spaces"\n');
});
it('should not set sensitive settings if the value is empty during initial setup', async () => {
const config: ExtensionConfig = {
name: 'test-ext',
version: '1.0.0',
settings: [
{
name: 's1',
description: 'd1',
envVar: 'SENSITIVE_VAR',
sensitive: true,
},
],
};
mockRequestSetting.mockResolvedValue('');
await maybePromptForSettings(
config,
'12345',
mockRequestSetting,
undefined,
undefined,
);
const userKeychain = new KeychainTokenStorage(
`Gemini CLI Extensions test-ext 12345`,
);
expect(await userKeychain.getSecret('SENSITIVE_VAR')).toBeNull();
});
it('should not attempt to clear secrets if keychain is unavailable', async () => {
// Arrange
const mockIsAvailable = vi.fn().mockResolvedValue(false);
@@ -767,42 +738,5 @@ describe('extensionSettings', () => {
const lines = actualContent.split('\n').filter((line) => line.length > 0);
expect(lines).toHaveLength(3); // Should only have the three variables
});
it('should delete a sensitive setting if the new value is empty', async () => {
mockRequestSetting.mockResolvedValue('');
await updateSetting(
config,
'12345',
'VAR2',
mockRequestSetting,
ExtensionSettingScope.USER,
tempWorkspaceDir,
);
const userKeychain = new KeychainTokenStorage(
`Gemini CLI Extensions test-ext 12345`,
);
expect(await userKeychain.getSecret('VAR2')).toBeNull();
});
it('should not throw if deleting a non-existent sensitive setting with empty value', async () => {
mockRequestSetting.mockResolvedValue('');
// Ensure it doesn't exist first
const userKeychain = new KeychainTokenStorage(
`Gemini CLI Extensions test-ext 12345`,
);
await userKeychain.deleteSecret('VAR2');
await updateSetting(
config,
'12345',
'VAR2',
mockRequestSetting,
ExtensionSettingScope.USER,
tempWorkspaceDir,
);
// Should complete without error
});
});
});
@@ -112,7 +112,7 @@ export async function maybePromptForSettings(
const nonSensitiveSettings: Record<string, string> = {};
for (const setting of settings) {
const value = allSettings[setting.envVar];
if (value === undefined || value === '') {
if (value === undefined) {
continue;
}
if (setting.sensitive) {
@@ -207,7 +207,7 @@ export async function updateSetting(
settingKey: string,
requestSetting: (setting: ExtensionSetting) => Promise<string>,
scope: ExtensionSettingScope,
workspaceDir: string,
workspaceDir?: string,
): Promise<void> {
const { name: extensionName, settings } = extensionConfig;
if (!settings || settings.length === 0) {
@@ -230,15 +230,7 @@ export async function updateSetting(
);
if (settingToUpdate.sensitive) {
if (newValue) {
await keychain.setSecret(settingToUpdate.envVar, newValue);
} else {
try {
await keychain.deleteSecret(settingToUpdate.envVar);
} catch {
// Ignore if secret does not exist
}
}
await keychain.setSecret(settingToUpdate.envVar, newValue);
return;
}
@@ -107,14 +107,6 @@ describe('SettingsSchema', () => {
getSettingsSchema().context.properties.fileFiltering.properties
?.enableRecursiveFileSearch,
).toBeDefined();
expect(
getSettingsSchema().context.properties.fileFiltering.properties
?.customIgnoreFilePaths,
).toBeDefined();
expect(
getSettingsSchema().context.properties.fileFiltering.properties
?.customIgnoreFilePaths.type,
).toBe('array');
});
it('should have unique categories', () => {
+2 -14
View File
@@ -932,18 +932,6 @@ const SETTINGS_SCHEMA = {
description: 'Enable fuzzy search when searching for files.',
showInDialog: true,
},
customIgnoreFilePaths: {
type: 'array',
label: 'Custom Ignore File Paths',
category: 'Context',
requiresRestart: true,
default: [] as string[],
description:
'Additional ignore file paths to respect. These files take precedence over .geminiignore and .gitignore. Files earlier in the array take precedence over files later in the array, e.g. the first file takes precedence over the second one.',
showInDialog: true,
items: { type: 'string' },
mergeStrategy: MergeStrategy.UNION,
},
},
},
},
@@ -1602,11 +1590,11 @@ const SETTINGS_SCHEMA = {
type: 'boolean',
label: 'Enable Hooks',
category: 'Advanced',
requiresRestart: true,
requiresRestart: false,
default: true,
description:
'Canonical toggle for the hooks system. When disabled, no hooks will be executed.',
showInDialog: true,
showInDialog: false,
},
disabled: {
type: 'array',
@@ -15,7 +15,6 @@ import {
StandardFileSystemService,
ToolRegistry,
COMMON_IGNORE_PATTERNS,
GEMINI_IGNORE_FILE_NAME,
// DEFAULT_FILE_EXCLUDES,
} from '@google/gemini-cli-core';
import * as core from '@google/gemini-cli-core';
@@ -629,7 +628,7 @@ describe('handleAtCommand', () => {
describe('gemini-ignore filtering', () => {
it('should skip gemini-ignored files in @ commands', async () => {
await createTestFile(
path.join(testRootDir, GEMINI_IGNORE_FILE_NAME),
path.join(testRootDir, '.geminiignore'),
'build/output.js',
);
const geminiIgnoredFile = await createTestFile(
@@ -660,7 +659,7 @@ describe('handleAtCommand', () => {
});
it('should process non-ignored files when .geminiignore is present', async () => {
await createTestFile(
path.join(testRootDir, GEMINI_IGNORE_FILE_NAME),
path.join(testRootDir, '.geminiignore'),
'build/output.js',
);
const validFile = await createTestFile(
@@ -691,7 +690,7 @@ describe('handleAtCommand', () => {
it('should handle mixed gemini-ignored and valid files', async () => {
await createTestFile(
path.join(testRootDir, GEMINI_IGNORE_FILE_NAME),
path.join(testRootDir, '.geminiignore'),
'dist/bundle.js',
);
const validFile = await createTestFile(
@@ -10,10 +10,7 @@ import { renderHook } from '../../test-utils/render.js';
import { waitFor } from '../../test-utils/async.js';
import { useAtCompletion } from './useAtCompletion.js';
import type { Config, FileSearch } from '@google/gemini-cli-core';
import {
FileSearchFactory,
FileDiscoveryService,
} from '@google/gemini-cli-core';
import { FileSearchFactory } from '@google/gemini-cli-core';
import type { FileSystemStructure } from '@google/gemini-cli-test-utils';
import { createTmpDir, cleanupTmpDir } from '@google/gemini-cli-test-utils';
import type { Suggestion } from '../components/SuggestionsDisplay.js';
@@ -151,10 +148,8 @@ describe('useAtCompletion', () => {
const fileSearch = FileSearchFactory.create({
projectRoot: testRootDir,
ignoreDirs: [],
fileDiscoveryService: new FileDiscoveryService(testRootDir, {
respectGitIgnore: false,
respectGeminiIgnore: false,
}),
useGitignore: false,
useGeminiignore: false,
cache: false,
cacheTtl: 0,
enableRecursiveFileSearch: true,
@@ -276,10 +271,8 @@ describe('useAtCompletion', () => {
const realFileSearch = FileSearchFactory.create({
projectRoot: testRootDir,
ignoreDirs: [],
fileDiscoveryService: new FileDiscoveryService(testRootDir, {
respectGitIgnore: true,
respectGeminiIgnore: true,
}),
useGitignore: true,
useGeminiignore: true,
cache: false,
cacheTtl: 0,
enableRecursiveFileSearch: true,
+5 -9
View File
@@ -7,11 +7,7 @@
import { useEffect, useReducer, useRef } from 'react';
import { setTimeout as setTimeoutPromise } from 'node:timers/promises';
import type { Config, FileSearch } from '@google/gemini-cli-core';
import {
FileSearchFactory,
escapePath,
FileDiscoveryService,
} from '@google/gemini-cli-core';
import { FileSearchFactory, escapePath } from '@google/gemini-cli-core';
import type { Suggestion } from '../components/SuggestionsDisplay.js';
import { MAX_SUGGESTIONS_TO_SHOW } from '../components/SuggestionsDisplay.js';
import { CommandKind } from '../commands/types.js';
@@ -254,10 +250,10 @@ export function useAtCompletion(props: UseAtCompletionProps): void {
const searcher = FileSearchFactory.create({
projectRoot: cwd,
ignoreDirs: [],
fileDiscoveryService: new FileDiscoveryService(
cwd,
config?.getFileFilteringOptions(),
),
useGitignore:
config?.getFileFilteringOptions()?.respectGitIgnore ?? true,
useGeminiignore:
config?.getFileFilteringOptions()?.respectGeminiIgnore ?? true,
cache: true,
cacheTtl: 30, // 30 seconds
enableRecursiveFileSearch:
@@ -305,9 +305,6 @@ describe('clipboardUtils', () => {
});
it('should return null if tool is not yet detected', async () => {
// Unset session type to ensure no tool is detected automatically
delete process.env['XDG_SESSION_TYPE'];
// Don't prime the tool
const result = await clipboardUtils.saveClipboardImage(mockTargetDir);
expect(result).toBe(null);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-core",
"version": "0.27.0-preview.5",
"version": "0.27.0-nightly.20260121.97aac696f",
"description": "Gemini CLI Core",
"license": "Apache-2.0",
"repository": {
@@ -312,32 +312,6 @@ describe('setupUser for new user', () => {
userTierName: 'paid',
});
});
it('should throw ineligible tier error when onboarding fails and ineligible tiers exist', async () => {
vi.stubEnv('GOOGLE_CLOUD_PROJECT', '');
mockLoad.mockResolvedValue({
allowedTiers: [mockPaidTier],
ineligibleTiers: [
{
reasonCode: 'UNSUPPORTED_LOCATION',
reasonMessage:
'Your current account is not eligible for Gemini Code Assist for individuals because it is not currently available in your location.',
tierId: 'free-tier',
tierName: 'Gemini Code Assist for individuals',
},
],
});
mockOnboardUser.mockResolvedValue({
done: true,
response: {
cloudaicompanionProject: {},
},
});
await expect(setupUser({} as OAuth2Client)).rejects.toThrow(
'Your current account is not eligible for Gemini Code Assist for individuals because it is not currently available in your location.',
);
});
});
describe('setupUser validation', () => {
+8 -21
View File
@@ -7,7 +7,6 @@
import type {
ClientMetadata,
GeminiUserTier,
IneligibleTier,
LoadCodeAssistResponse,
OnboardUserRequest,
} from './types.js';
@@ -36,16 +35,6 @@ export class ValidationCancelledError extends Error {
}
}
export class IneligibleTierError extends Error {
readonly ineligibleTiers: IneligibleTier[];
constructor(ineligibleTiers: IneligibleTier[]) {
const reasons = ineligibleTiers.map((t) => t.reasonMessage).join(', ');
super(reasons);
this.ineligibleTiers = ineligibleTiers;
}
}
export interface UserData {
projectId: string;
userTier: UserTierId;
@@ -138,7 +127,13 @@ export async function setupUser(
}
// If user is not setup for standard tier, inform them about all other tiers they are ineligible for.
throwIneligibleOrProjectIdError(loadRes);
if (loadRes.ineligibleTiers && loadRes.ineligibleTiers.length > 0) {
const reasons = loadRes.ineligibleTiers
.map((t) => t.reasonMessage)
.join(', ');
throw new Error(reasons);
}
throw new ProjectIdRequiredError();
}
return {
projectId: loadRes.cloudaicompanionProject,
@@ -185,8 +180,7 @@ export async function setupUser(
userTierName: tier.name,
};
}
throwIneligibleOrProjectIdError(loadRes);
throw new ProjectIdRequiredError();
}
return {
@@ -196,13 +190,6 @@ export async function setupUser(
};
}
function throwIneligibleOrProjectIdError(res: LoadCodeAssistResponse): never {
if (res.ineligibleTiers && res.ineligibleTiers.length > 0) {
throw new IneligibleTierError(res.ineligibleTiers);
}
throw new ProjectIdRequiredError();
}
function getOnboardTier(res: LoadCodeAssistResponse): GeminiUserTier {
for (const tier of res.allowedTiers || []) {
if (tier.isDefault) {
-50
View File
@@ -13,7 +13,6 @@ import { debugLogger } from '../utils/debugLogger.js';
import { ApprovalMode } from '../policy/types.js';
import type { HookDefinition } from '../hooks/types.js';
import { HookType, HookEventName } from '../hooks/types.js';
import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
import * as path from 'node:path';
import * as fs from 'node:fs';
import { setGeminiMdFilename as mockSetGeminiMdFilename } from '../tools/memoryTool.js';
@@ -139,8 +138,6 @@ vi.mock('../services/gitService.js', () => {
return { GitService: GitServiceMock };
});
vi.mock('../services/fileDiscoveryService.js');
vi.mock('../ide/ide-client.js', () => ({
IdeClient: {
getInstance: vi.fn().mockResolvedValue({
@@ -626,30 +623,6 @@ describe('Server Config (config.ts)', () => {
expect(config.getFileFilteringRespectGitIgnore()).toBe(false);
});
it('should set customIgnoreFilePaths from params', () => {
const params: ConfigParameters = {
...baseParams,
fileFiltering: {
customIgnoreFilePaths: ['/path/to/ignore/file'],
},
};
const config = new Config(params);
expect(config.getCustomIgnoreFilePaths()).toStrictEqual([
'/path/to/ignore/file',
]);
});
it('should set customIgnoreFilePaths to empty array if not provided', () => {
const params: ConfigParameters = {
...baseParams,
fileFiltering: {
respectGitIgnore: true,
},
};
const config = new Config(params);
expect(config.getCustomIgnoreFilePaths()).toStrictEqual([]);
});
it('should initialize WorkspaceContext with includeDirectories', () => {
const includeDirectories = ['dir1', 'dir2'];
const paramsWithIncludeDirs: ConfigParameters = {
@@ -726,29 +699,6 @@ describe('Server Config (config.ts)', () => {
expect(fileService).toBeDefined();
});
it('should pass file filtering options to FileDiscoveryService', () => {
const configParams = {
...baseParams,
fileFiltering: {
respectGitIgnore: false,
respectGeminiIgnore: false,
customIgnoreFilePaths: ['.myignore'],
},
};
const config = new Config(configParams);
config.getFileService();
expect(FileDiscoveryService).toHaveBeenCalledWith(
path.resolve(TARGET_DIR),
{
respectGitIgnore: false,
respectGeminiIgnore: false,
customIgnoreFilePaths: ['.myignore'],
},
);
});
describe('Usage Statistics', () => {
it('defaults usage statistics to enabled if not specified', () => {
const config = new Config({
+1 -14
View File
@@ -328,7 +328,6 @@ export interface ConfigParameters {
enableFuzzySearch?: boolean;
maxFileCount?: number;
searchTimeout?: number;
customIgnoreFilePaths?: string[];
};
checkpointing?: boolean;
proxy?: string;
@@ -466,7 +465,6 @@ export class Config {
enableFuzzySearch: boolean;
maxFileCount: number;
searchTimeout: number;
customIgnoreFilePaths: string[];
};
private fileDiscoveryService: FileDiscoveryService | null = null;
private gitService: GitService | undefined = undefined;
@@ -633,7 +631,6 @@ export class Config {
params.fileFiltering?.searchTimeout ??
DEFAULT_FILE_FILTERING_OPTIONS.searchTimeout ??
5000,
customIgnoreFilePaths: params.fileFiltering?.customIgnoreFilePaths ?? [],
};
this.checkpointing = params.checkpointing ?? false;
this.proxy = params.proxy;
@@ -1531,22 +1528,16 @@ export class Config {
getFileFilteringRespectGitIgnore(): boolean {
return this.fileFiltering.respectGitIgnore;
}
getFileFilteringRespectGeminiIgnore(): boolean {
return this.fileFiltering.respectGeminiIgnore;
}
getCustomIgnoreFilePaths(): string[] {
return this.fileFiltering.customIgnoreFilePaths;
}
getFileFilteringOptions(): FileFilteringOptions {
return {
respectGitIgnore: this.fileFiltering.respectGitIgnore,
respectGeminiIgnore: this.fileFiltering.respectGeminiIgnore,
maxFileCount: this.fileFiltering.maxFileCount,
searchTimeout: this.fileFiltering.searchTimeout,
customIgnoreFilePaths: this.fileFiltering.customIgnoreFilePaths,
};
}
@@ -1583,11 +1574,7 @@ export class Config {
getFileService(): FileDiscoveryService {
if (!this.fileDiscoveryService) {
this.fileDiscoveryService = new FileDiscoveryService(this.targetDir, {
respectGitIgnore: this.fileFiltering.respectGitIgnore,
respectGeminiIgnore: this.fileFiltering.respectGeminiIgnore,
customIgnoreFilePaths: this.fileFiltering.customIgnoreFilePaths,
});
this.fileDiscoveryService = new FileDiscoveryService(this.targetDir);
}
return this.fileDiscoveryService;
}
-6
View File
@@ -9,7 +9,6 @@ export interface FileFilteringOptions {
respectGeminiIgnore: boolean;
maxFileCount?: number;
searchTimeout?: number;
customIgnoreFilePaths: string[];
}
// For memory files
@@ -18,7 +17,6 @@ export const DEFAULT_MEMORY_FILE_FILTERING_OPTIONS: FileFilteringOptions = {
respectGeminiIgnore: true,
maxFileCount: 20000,
searchTimeout: 5000,
customIgnoreFilePaths: [],
};
// For all other files
@@ -27,8 +25,4 @@ export const DEFAULT_FILE_FILTERING_OPTIONS: FileFilteringOptions = {
respectGeminiIgnore: true,
maxFileCount: 20000,
searchTimeout: 5000,
customIgnoreFilePaths: [],
};
// Generic exclusion file name
export const GEMINI_IGNORE_FILE_NAME = '.geminiignore';
@@ -5,6 +5,7 @@ exports[`Core System Prompt (prompts.ts) > ApprovalMode in System Prompt > shoul
# Core Mandates
- **Context Efficiency:** Minimize context usage. Do not read entire files unless necessary. Use 'search_file_content' or 'read_file' with 'limit' to inspect large files.
- **Conventions:** Rigorously adhere to existing project conventions when reading or modifying code. Analyze surrounding code, tests, and configuration first.
- **Libraries/Frameworks:** NEVER assume a library/framework is available or appropriate. Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', 'build.gradle', etc., or observe neighboring files) before employing it.
- **Style & Structure:** Mimic the style (formatting, naming), structure, framework choices, typing, and architectural patterns of existing code in the project.
@@ -104,6 +105,7 @@ exports[`Core System Prompt (prompts.ts) > ApprovalMode in System Prompt > shoul
# Core Mandates
- **Context Efficiency:** Minimize context usage. Do not read entire files unless necessary. Use 'search_file_content' or 'read_file' with 'limit' to inspect large files.
- **Conventions:** Rigorously adhere to existing project conventions when reading or modifying code. Analyze surrounding code, tests, and configuration first.
- **Libraries/Frameworks:** NEVER assume a library/framework is available or appropriate. Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', 'build.gradle', etc., or observe neighboring files) before employing it.
- **Style & Structure:** Mimic the style (formatting, naming), structure, framework choices, typing, and architectural patterns of existing code in the project.
@@ -216,6 +218,7 @@ exports[`Core System Prompt (prompts.ts) > should append userMemory with separat
# Core Mandates
- **Context Efficiency:** Minimize context usage. Do not read entire files unless necessary. Use 'search_file_content' or 'read_file' with 'limit' to inspect large files.
- **Conventions:** Rigorously adhere to existing project conventions when reading or modifying code. Analyze surrounding code, tests, and configuration first.
- **Libraries/Frameworks:** NEVER assume a library/framework is available or appropriate. Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', 'build.gradle', etc., or observe neighboring files) before employing it.
- **Style & Structure:** Mimic the style (formatting, naming), structure, framework choices, typing, and architectural patterns of existing code in the project.
@@ -320,6 +323,7 @@ exports[`Core System Prompt (prompts.ts) > should handle CodebaseInvestigator wi
# Core Mandates
- **Context Efficiency:** Minimize context usage. Do not read entire files unless necessary. Use 'search_file_content' or 'read_file' with 'limit' to inspect large files.
- **Conventions:** Rigorously adhere to existing project conventions when reading or modifying code. Analyze surrounding code, tests, and configuration first.
- **Libraries/Frameworks:** NEVER assume a library/framework is available or appropriate. Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', 'build.gradle', etc., or observe neighboring files) before employing it.
- **Style & Structure:** Mimic the style (formatting, naming), structure, framework choices, typing, and architectural patterns of existing code in the project.
@@ -418,6 +422,7 @@ exports[`Core System Prompt (prompts.ts) > should handle CodebaseInvestigator wi
# Core Mandates
- **Context Efficiency:** Minimize context usage. Do not read entire files unless necessary. Use 'search_file_content' or 'read_file' with 'limit' to inspect large files.
- **Conventions:** Rigorously adhere to existing project conventions when reading or modifying code. Analyze surrounding code, tests, and configuration first.
- **Libraries/Frameworks:** NEVER assume a library/framework is available or appropriate. Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', 'build.gradle', etc., or observe neighboring files) before employing it.
- **Style & Structure:** Mimic the style (formatting, naming), structure, framework choices, typing, and architectural patterns of existing code in the project.
@@ -515,6 +520,7 @@ exports[`Core System Prompt (prompts.ts) > should handle git instructions when i
# Core Mandates
- **Context Efficiency:** Minimize context usage. Do not read entire files unless necessary. Use 'search_file_content' or 'read_file' with 'limit' to inspect large files.
- **Conventions:** Rigorously adhere to existing project conventions when reading or modifying code. Analyze surrounding code, tests, and configuration first.
- **Libraries/Frameworks:** NEVER assume a library/framework is available or appropriate. Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', 'build.gradle', etc., or observe neighboring files) before employing it.
- **Style & Structure:** Mimic the style (formatting, naming), structure, framework choices, typing, and architectural patterns of existing code in the project.
@@ -614,6 +620,7 @@ exports[`Core System Prompt (prompts.ts) > should handle git instructions when i
# Core Mandates
- **Context Efficiency:** Minimize context usage. Do not read entire files unless necessary. Use 'search_file_content' or 'read_file' with 'limit' to inspect large files.
- **Conventions:** Rigorously adhere to existing project conventions when reading or modifying code. Analyze surrounding code, tests, and configuration first.
- **Libraries/Frameworks:** NEVER assume a library/framework is available or appropriate. Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', 'build.gradle', etc., or observe neighboring files) before employing it.
- **Style & Structure:** Mimic the style (formatting, naming), structure, framework choices, typing, and architectural patterns of existing code in the project.
@@ -731,6 +738,7 @@ exports[`Core System Prompt (prompts.ts) > should include available_skills when
# Core Mandates
- **Context Efficiency:** Minimize context usage. Do not read entire files unless necessary. Use 'search_file_content' or 'read_file' with 'limit' to inspect large files.
- **Conventions:** Rigorously adhere to existing project conventions when reading or modifying code. Analyze surrounding code, tests, and configuration first.
- **Libraries/Frameworks:** NEVER assume a library/framework is available or appropriate. Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', 'build.gradle', etc., or observe neighboring files) before employing it.
- **Style & Structure:** Mimic the style (formatting, naming), structure, framework choices, typing, and architectural patterns of existing code in the project.
@@ -843,6 +851,7 @@ exports[`Core System Prompt (prompts.ts) > should include correct sandbox instru
# Core Mandates
- **Context Efficiency:** Minimize context usage. Do not read entire files unless necessary. Use 'search_file_content' or 'read_file' with 'limit' to inspect large files.
- **Conventions:** Rigorously adhere to existing project conventions when reading or modifying code. Analyze surrounding code, tests, and configuration first.
- **Libraries/Frameworks:** NEVER assume a library/framework is available or appropriate. Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', 'build.gradle', etc., or observe neighboring files) before employing it.
- **Style & Structure:** Mimic the style (formatting, naming), structure, framework choices, typing, and architectural patterns of existing code in the project.
@@ -942,6 +951,7 @@ exports[`Core System Prompt (prompts.ts) > should include correct sandbox instru
# Core Mandates
- **Context Efficiency:** Minimize context usage. Do not read entire files unless necessary. Use 'search_file_content' or 'read_file' with 'limit' to inspect large files.
- **Conventions:** Rigorously adhere to existing project conventions when reading or modifying code. Analyze surrounding code, tests, and configuration first.
- **Libraries/Frameworks:** NEVER assume a library/framework is available or appropriate. Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', 'build.gradle', etc., or observe neighboring files) before employing it.
- **Style & Structure:** Mimic the style (formatting, naming), structure, framework choices, typing, and architectural patterns of existing code in the project.
@@ -1041,6 +1051,7 @@ exports[`Core System Prompt (prompts.ts) > should include correct sandbox instru
# Core Mandates
- **Context Efficiency:** Minimize context usage. Do not read entire files unless necessary. Use 'search_file_content' or 'read_file' with 'limit' to inspect large files.
- **Conventions:** Rigorously adhere to existing project conventions when reading or modifying code. Analyze surrounding code, tests, and configuration first.
- **Libraries/Frameworks:** NEVER assume a library/framework is available or appropriate. Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', 'build.gradle', etc., or observe neighboring files) before employing it.
- **Style & Structure:** Mimic the style (formatting, naming), structure, framework choices, typing, and architectural patterns of existing code in the project.
@@ -1140,6 +1151,7 @@ exports[`Core System Prompt (prompts.ts) > should return the base prompt when us
# Core Mandates
- **Context Efficiency:** Minimize context usage. Do not read entire files unless necessary. Use 'search_file_content' or 'read_file' with 'limit' to inspect large files.
- **Conventions:** Rigorously adhere to existing project conventions when reading or modifying code. Analyze surrounding code, tests, and configuration first.
- **Libraries/Frameworks:** NEVER assume a library/framework is available or appropriate. Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', 'build.gradle', etc., or observe neighboring files) before employing it.
- **Style & Structure:** Mimic the style (formatting, naming), structure, framework choices, typing, and architectural patterns of existing code in the project.
@@ -1239,6 +1251,7 @@ exports[`Core System Prompt (prompts.ts) > should return the base prompt when us
# Core Mandates
- **Context Efficiency:** Minimize context usage. Do not read entire files unless necessary. Use 'search_file_content' or 'read_file' with 'limit' to inspect large files.
- **Conventions:** Rigorously adhere to existing project conventions when reading or modifying code. Analyze surrounding code, tests, and configuration first.
- **Libraries/Frameworks:** NEVER assume a library/framework is available or appropriate. Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', 'build.gradle', etc., or observe neighboring files) before employing it.
- **Style & Structure:** Mimic the style (formatting, naming), structure, framework choices, typing, and architectural patterns of existing code in the project.
@@ -1338,6 +1351,7 @@ exports[`Core System Prompt (prompts.ts) > should return the interactive avoidan
# Core Mandates
- **Context Efficiency:** Minimize context usage. Do not read entire files unless necessary. Use 'search_file_content' or 'read_file' with 'limit' to inspect large files.
- **Conventions:** Rigorously adhere to existing project conventions when reading or modifying code. Analyze surrounding code, tests, and configuration first.
- **Libraries/Frameworks:** NEVER assume a library/framework is available or appropriate. Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', 'build.gradle', etc., or observe neighboring files) before employing it.
- **Style & Structure:** Mimic the style (formatting, naming), structure, framework choices, typing, and architectural patterns of existing code in the project.
@@ -1436,6 +1450,7 @@ exports[`Core System Prompt (prompts.ts) > should use chatty system prompt for p
# Core Mandates
- **Context Efficiency:** Minimize context usage. Do not read entire files unless necessary. Use 'search_file_content' or 'read_file' with 'limit' to inspect large files.
- **Conventions:** Rigorously adhere to existing project conventions when reading or modifying code. Analyze surrounding code, tests, and configuration first.
- **Libraries/Frameworks:** NEVER assume a library/framework is available or appropriate. Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', 'build.gradle', etc., or observe neighboring files) before employing it.
- **Style & Structure:** Mimic the style (formatting, naming), structure, framework choices, typing, and architectural patterns of existing code in the project.
@@ -1536,6 +1551,7 @@ exports[`Core System Prompt (prompts.ts) > should use chatty system prompt for p
# Core Mandates
- **Context Efficiency:** Minimize context usage. Do not read entire files unless necessary. Use 'search_file_content' or 'read_file' with 'limit' to inspect large files.
- **Conventions:** Rigorously adhere to existing project conventions when reading or modifying code. Analyze surrounding code, tests, and configuration first.
- **Libraries/Frameworks:** NEVER assume a library/framework is available or appropriate. Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', 'build.gradle', etc., or observe neighboring files) before employing it.
- **Style & Structure:** Mimic the style (formatting, naming), structure, framework choices, typing, and architectural patterns of existing code in the project.
-1
View File
@@ -8,7 +8,6 @@
export * from './config/config.js';
export * from './config/defaultModelConfigs.js';
export * from './config/models.js';
export * from './config/constants.js';
export * from './output/types.js';
export * from './output/json-formatter.js';
export * from './output/stream-json-formatter.js';
+1
View File
@@ -135,6 +135,7 @@ export function renderCoreMandates(options?: CoreMandatesOptions): string {
return `
# Core Mandates
- **Context Efficiency:** Minimize context usage. Do not read entire files unless necessary. Use '${GREP_TOOL_NAME}' or '${READ_FILE_TOOL_NAME}' with 'limit' to inspect large files.
- **Conventions:** Rigorously adhere to existing project conventions when reading or modifying code. Analyze surrounding code, tests, and configuration first.
- **Libraries/Frameworks:** NEVER assume a library/framework is available or appropriate. Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', 'build.gradle', etc., or observe neighboring files) before employing it.
- **Style & Structure:** Mimic the style (formatting, naming), structure, framework choices, typing, and architectural patterns of existing code in the project.
@@ -4,12 +4,11 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import * as fs from 'node:fs/promises';
import * as os from 'node:os';
import * as path from 'node:path';
import { FileDiscoveryService } from './fileDiscoveryService.js';
import { GEMINI_IGNORE_FILE_NAME } from '../config/constants.js';
describe('FileDiscoveryService', () => {
let testRootDir: string;
@@ -55,66 +54,19 @@ describe('FileDiscoveryService', () => {
});
it('should load .geminiignore patterns even when not in a git repo', async () => {
await createTestFile(GEMINI_IGNORE_FILE_NAME, 'secrets.txt');
await createTestFile('.geminiignore', 'secrets.txt');
const service = new FileDiscoveryService(projectRoot);
expect(service.shouldIgnoreFile('secrets.txt')).toBe(true);
expect(service.shouldIgnoreFile('src/index.js')).toBe(false);
});
it('should call applyFilterFilesOptions in constructor', () => {
const resolveSpy = vi.spyOn(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
FileDiscoveryService.prototype as any,
'applyFilterFilesOptions',
);
const options = { respectGitIgnore: false };
new FileDiscoveryService(projectRoot, options);
expect(resolveSpy).toHaveBeenCalledWith(options);
});
it('should correctly resolve options passed to constructor', () => {
const options = {
respectGitIgnore: false,
respectGeminiIgnore: false,
customIgnoreFilePaths: ['custom/.ignore'],
};
const service = new FileDiscoveryService(projectRoot, options);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const defaults = (service as any).defaultFilterFileOptions;
expect(defaults.respectGitIgnore).toBe(false);
expect(defaults.respectGeminiIgnore).toBe(false);
expect(defaults.customIgnoreFilePaths).toStrictEqual(['custom/.ignore']);
});
it('should use defaults when options are not provided', () => {
const service = new FileDiscoveryService(projectRoot);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const defaults = (service as any).defaultFilterFileOptions;
expect(defaults.respectGitIgnore).toBe(true);
expect(defaults.respectGeminiIgnore).toBe(true);
expect(defaults.customIgnoreFilePaths).toStrictEqual([]);
});
it('should partially override defaults', () => {
const service = new FileDiscoveryService(projectRoot, {
respectGitIgnore: false,
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const defaults = (service as any).defaultFilterFileOptions;
expect(defaults.respectGitIgnore).toBe(false);
expect(defaults.respectGeminiIgnore).toBe(true);
});
});
describe('filterFiles', () => {
beforeEach(async () => {
await fs.mkdir(path.join(projectRoot, '.git'));
await createTestFile('.gitignore', 'node_modules/\n.git/\ndist');
await createTestFile(GEMINI_IGNORE_FILE_NAME, 'logs/');
await createTestFile('.geminiignore', 'logs/');
});
it('should filter out git-ignored and gemini-ignored files by default', () => {
@@ -188,7 +140,7 @@ describe('FileDiscoveryService', () => {
beforeEach(async () => {
await fs.mkdir(path.join(projectRoot, '.git'));
await createTestFile('.gitignore', 'node_modules/');
await createTestFile(GEMINI_IGNORE_FILE_NAME, '*.log');
await createTestFile('.geminiignore', '*.log');
});
it('should return filtered paths and correct ignored count', () => {
@@ -225,7 +177,7 @@ describe('FileDiscoveryService', () => {
beforeEach(async () => {
await fs.mkdir(path.join(projectRoot, '.git'));
await createTestFile('.gitignore', 'node_modules/');
await createTestFile(GEMINI_IGNORE_FILE_NAME, '*.log');
await createTestFile('.geminiignore', '*.log');
});
it('should return true for git-ignored files', () => {
@@ -300,7 +252,7 @@ describe('FileDiscoveryService', () => {
it('should un-ignore a file in .geminiignore that is ignored in .gitignore', async () => {
await createTestFile('.gitignore', '*.txt');
await createTestFile(GEMINI_IGNORE_FILE_NAME, '!important.txt');
await createTestFile('.geminiignore', '!important.txt');
const service = new FileDiscoveryService(projectRoot);
const files = ['file.txt', 'important.txt'].map((f) =>
@@ -313,7 +265,7 @@ describe('FileDiscoveryService', () => {
it('should un-ignore a directory in .geminiignore that is ignored in .gitignore', async () => {
await createTestFile('.gitignore', 'logs/');
await createTestFile(GEMINI_IGNORE_FILE_NAME, '!logs/');
await createTestFile('.geminiignore', '!logs/');
const service = new FileDiscoveryService(projectRoot);
const files = ['logs/app.log', 'other/app.log'].map((f) =>
@@ -326,7 +278,7 @@ describe('FileDiscoveryService', () => {
it('should extend ignore rules in .geminiignore', async () => {
await createTestFile('.gitignore', '*.log');
await createTestFile(GEMINI_IGNORE_FILE_NAME, 'temp/');
await createTestFile('.geminiignore', 'temp/');
const service = new FileDiscoveryService(projectRoot);
const files = ['app.log', 'temp/file.txt'].map((f) =>
@@ -339,7 +291,7 @@ describe('FileDiscoveryService', () => {
it('should use .gitignore rules if respectGeminiIgnore is false', async () => {
await createTestFile('.gitignore', '*.txt');
await createTestFile(GEMINI_IGNORE_FILE_NAME, '!important.txt');
await createTestFile('.geminiignore', '!important.txt');
const service = new FileDiscoveryService(projectRoot);
const files = ['file.txt', 'important.txt'].map((f) =>
@@ -356,7 +308,7 @@ describe('FileDiscoveryService', () => {
it('should use .geminiignore rules if respectGitIgnore is false', async () => {
await createTestFile('.gitignore', '*.txt');
await createTestFile(GEMINI_IGNORE_FILE_NAME, '!important.txt\ntemp/');
await createTestFile('.geminiignore', '!important.txt\ntemp/');
const service = new FileDiscoveryService(projectRoot);
const files = ['file.txt', 'important.txt', 'temp/file.js'].map((f) =>
@@ -376,123 +328,4 @@ describe('FileDiscoveryService', () => {
);
});
});
describe('custom ignore file', () => {
it('should respect patterns from a custom ignore file', async () => {
const customIgnoreName = '.customignore';
await createTestFile(customIgnoreName, '*.secret');
const service = new FileDiscoveryService(projectRoot, {
customIgnoreFilePaths: [customIgnoreName],
});
const files = ['file.txt', 'file.secret'].map((f) =>
path.join(projectRoot, f),
);
const filtered = service.filterFiles(files);
expect(filtered).toEqual([path.join(projectRoot, 'file.txt')]);
});
it('should prioritize custom ignore patterns over .geminiignore patterns in git repo', async () => {
await fs.mkdir(path.join(projectRoot, '.git'));
await createTestFile('.gitignore', 'node_modules/');
await createTestFile(GEMINI_IGNORE_FILE_NAME, '*.log');
const customIgnoreName = '.customignore';
// .geminiignore ignores *.log, custom un-ignores debug.log
await createTestFile(customIgnoreName, '!debug.log');
const service = new FileDiscoveryService(projectRoot, {
customIgnoreFilePaths: [customIgnoreName],
});
const files = ['debug.log', 'error.log'].map((f) =>
path.join(projectRoot, f),
);
const filtered = service.filterFiles(files);
expect(filtered).toEqual([path.join(projectRoot, 'debug.log')]);
});
it('should prioritize custom ignore patterns over .geminiignore patterns in non-git repo', async () => {
// No .git directory created
await createTestFile(GEMINI_IGNORE_FILE_NAME, 'secret.txt');
const customIgnoreName = '.customignore';
// .geminiignore ignores secret.txt, custom un-ignores it
await createTestFile(customIgnoreName, '!secret.txt');
const service = new FileDiscoveryService(projectRoot, {
customIgnoreFilePaths: [customIgnoreName],
});
const files = ['secret.txt'].map((f) => path.join(projectRoot, f));
const filtered = service.filterFiles(files);
expect(filtered).toEqual([path.join(projectRoot, 'secret.txt')]);
});
});
describe('getIgnoreFilePaths & getAllIgnoreFilePaths', () => {
beforeEach(async () => {
await fs.mkdir(path.join(projectRoot, '.git'));
await createTestFile('.gitignore', '*.log');
await createTestFile(GEMINI_IGNORE_FILE_NAME, '*.tmp');
await createTestFile('.customignore', '*.secret');
});
it('should return .geminiignore path by default', () => {
const service = new FileDiscoveryService(projectRoot);
const paths = service.getIgnoreFilePaths();
expect(paths).toEqual([path.join(projectRoot, GEMINI_IGNORE_FILE_NAME)]);
});
it('should not return .geminiignore path if respectGeminiIgnore is false', () => {
const service = new FileDiscoveryService(projectRoot, {
respectGeminiIgnore: false,
});
const paths = service.getIgnoreFilePaths();
expect(paths).toEqual([]);
});
it('should return custom ignore file paths', () => {
const service = new FileDiscoveryService(projectRoot, {
customIgnoreFilePaths: ['.customignore'],
});
const paths = service.getIgnoreFilePaths();
expect(paths).toContain(path.join(projectRoot, GEMINI_IGNORE_FILE_NAME));
expect(paths).toContain(path.join(projectRoot, '.customignore'));
});
it('should return all ignore paths including .gitignore', () => {
const service = new FileDiscoveryService(projectRoot);
const paths = service.getAllIgnoreFilePaths();
expect(paths).toContain(path.join(projectRoot, GEMINI_IGNORE_FILE_NAME));
expect(paths).toContain(path.join(projectRoot, '.gitignore'));
});
it('should not return .gitignore if respectGitIgnore is false', () => {
const service = new FileDiscoveryService(projectRoot, {
respectGitIgnore: false,
});
const paths = service.getAllIgnoreFilePaths();
expect(paths).toContain(path.join(projectRoot, GEMINI_IGNORE_FILE_NAME));
expect(paths).not.toContain(path.join(projectRoot, '.gitignore'));
});
it('should not return .gitignore if it does not exist', async () => {
await fs.rm(path.join(projectRoot, '.gitignore'));
const service = new FileDiscoveryService(projectRoot);
const paths = service.getAllIgnoreFilePaths();
expect(paths).not.toContain(path.join(projectRoot, '.gitignore'));
expect(paths).toContain(path.join(projectRoot, GEMINI_IGNORE_FILE_NAME));
});
it('should ensure .gitignore is the first file in the list', () => {
const service = new FileDiscoveryService(projectRoot);
const paths = service.getAllIgnoreFilePaths();
expect(paths[0]).toBe(path.join(projectRoot, '.gitignore'));
});
});
});
@@ -5,18 +5,15 @@
*/
import type { GitIgnoreFilter } from '../utils/gitIgnoreParser.js';
import type { IgnoreFileFilter } from '../utils/ignoreFileParser.js';
import type { GeminiIgnoreFilter } from '../utils/geminiIgnoreParser.js';
import { GitIgnoreParser } from '../utils/gitIgnoreParser.js';
import { IgnoreFileParser } from '../utils/ignoreFileParser.js';
import { GeminiIgnoreParser } from '../utils/geminiIgnoreParser.js';
import { isGitRepository } from '../utils/gitUtils.js';
import { GEMINI_IGNORE_FILE_NAME } from '../config/constants.js';
import fs from 'node:fs';
import * as path from 'node:path';
export interface FilterFilesOptions {
respectGitIgnore?: boolean;
respectGeminiIgnore?: boolean;
customIgnoreFilePaths?: string[];
}
export interface FilterReport {
@@ -26,83 +23,32 @@ export interface FilterReport {
export class FileDiscoveryService {
private gitIgnoreFilter: GitIgnoreFilter | null = null;
private geminiIgnoreFilter: IgnoreFileFilter | null = null;
private customIgnoreFilter: IgnoreFileFilter | null = null;
private combinedIgnoreFilter: GitIgnoreFilter | IgnoreFileFilter | null =
null;
private defaultFilterFileOptions: FilterFilesOptions = {
respectGitIgnore: true,
respectGeminiIgnore: true,
customIgnoreFilePaths: [],
};
private geminiIgnoreFilter: GeminiIgnoreFilter | null = null;
private combinedIgnoreFilter: GitIgnoreFilter | null = null;
private projectRoot: string;
constructor(projectRoot: string, options?: FilterFilesOptions) {
constructor(projectRoot: string) {
this.projectRoot = path.resolve(projectRoot);
this.applyFilterFilesOptions(options);
if (isGitRepository(this.projectRoot)) {
this.gitIgnoreFilter = new GitIgnoreParser(this.projectRoot);
}
this.geminiIgnoreFilter = new IgnoreFileParser(
this.projectRoot,
GEMINI_IGNORE_FILE_NAME,
);
if (this.defaultFilterFileOptions.customIgnoreFilePaths?.length) {
this.customIgnoreFilter = new IgnoreFileParser(
this.projectRoot,
this.defaultFilterFileOptions.customIgnoreFilePaths,
);
}
this.geminiIgnoreFilter = new GeminiIgnoreParser(this.projectRoot);
if (this.gitIgnoreFilter) {
const geminiPatterns = this.geminiIgnoreFilter.getPatterns();
const customPatterns = this.customIgnoreFilter
? this.customIgnoreFilter.getPatterns()
: [];
// Create combined parser: .gitignore + .geminiignore + custom ignore
// Create combined parser: .gitignore + .geminiignore
this.combinedIgnoreFilter = new GitIgnoreParser(
this.projectRoot,
// customPatterns should go the last to ensure overwriting of geminiPatterns
[...geminiPatterns, ...customPatterns],
geminiPatterns,
);
} else {
// Create combined parser when not git repo
const geminiPatterns = this.geminiIgnoreFilter.getPatterns();
const customPatterns = this.customIgnoreFilter
? this.customIgnoreFilter.getPatterns()
: [];
this.combinedIgnoreFilter = new IgnoreFileParser(
this.projectRoot,
[...geminiPatterns, ...customPatterns],
true,
);
}
}
private applyFilterFilesOptions(options?: FilterFilesOptions): void {
if (!options) return;
if (options.respectGitIgnore !== undefined) {
this.defaultFilterFileOptions.respectGitIgnore = options.respectGitIgnore;
}
if (options.respectGeminiIgnore !== undefined) {
this.defaultFilterFileOptions.respectGeminiIgnore =
options.respectGeminiIgnore;
}
if (options.customIgnoreFilePaths) {
this.defaultFilterFileOptions.customIgnoreFilePaths =
options.customIgnoreFilePaths;
}
}
/**
* Filters a list of file paths based on ignore rules
* Filters a list of file paths based on git ignore rules
*/
filterFiles(filePaths: string[], options: FilterFilesOptions = {}): string[] {
const {
respectGitIgnore = this.defaultFilterFileOptions.respectGitIgnore,
respectGeminiIgnore = this.defaultFilterFileOptions.respectGeminiIgnore,
} = options;
const { respectGitIgnore = true, respectGeminiIgnore = true } = options;
return filePaths.filter((filePath) => {
if (
respectGitIgnore &&
@@ -112,11 +58,6 @@ export class FileDiscoveryService {
return !this.combinedIgnoreFilter.isIgnored(filePath);
}
// Always respect custom ignore filter if provided
if (this.customIgnoreFilter?.isIgnored(filePath)) {
return false;
}
if (respectGitIgnore && this.gitIgnoreFilter?.isIgnored(filePath)) {
return false;
}
@@ -156,38 +97,4 @@ export class FileDiscoveryService {
): boolean {
return this.filterFiles([filePath], options).length === 0;
}
/**
* Returns the list of ignore files being used (e.g. .geminiignore) excluding .gitignore.
*/
getIgnoreFilePaths(): string[] {
const paths: string[] = [];
if (
this.geminiIgnoreFilter &&
this.defaultFilterFileOptions.respectGeminiIgnore
) {
paths.push(...this.geminiIgnoreFilter.getIgnoreFilePaths());
}
if (this.customIgnoreFilter) {
paths.push(...this.customIgnoreFilter.getIgnoreFilePaths());
}
return paths;
}
/**
* Returns all ignore files including .gitignore if applicable.
*/
getAllIgnoreFilePaths(): string[] {
const paths: string[] = [];
if (
this.gitIgnoreFilter &&
this.defaultFilterFileOptions.respectGitIgnore
) {
const gitIgnorePath = path.join(this.projectRoot, '.gitignore');
if (fs.existsSync(gitIgnorePath)) {
paths.push(gitIgnorePath);
}
}
return paths.concat(this.getIgnoreFilePaths());
}
}
+1 -1
View File
@@ -912,7 +912,7 @@ export class EditTool
super(
EditTool.Name,
'Edit',
`Replaces text within a file. By default, replaces a single occurrence, but can replace multiple occurrences when \`expected_replacements\` is specified. This tool requires providing significant context around the change to ensure precise targeting. Always use the ${READ_FILE_TOOL_NAME} tool to examine the file's current content before attempting a text replacement.
`Replaces text within a file. By default, replaces a single occurrence, but can replace multiple occurrences when \`expected_replacements\` is specified. This tool requires providing significant context around the change to ensure precise targeting. Always use the ${READ_FILE_TOOL_NAME} tool (using 'offset' and 'limit' to get ~20 lines of context around the match) to examine the file's current content before attempting a text replacement.
The user has the ability to modify the \`new_string\` content. If modified, this will be stated in the response.
+3 -6
View File
@@ -18,10 +18,7 @@ import { createMockWorkspaceContext } from '../test-utils/mockWorkspaceContext.j
import { ToolErrorType } from './tool-error.js';
import * as glob from 'glob';
import { createMockMessageBus } from '../test-utils/mock-message-bus.js';
import {
DEFAULT_FILE_FILTERING_OPTIONS,
GEMINI_IGNORE_FILE_NAME,
} from '../config/constants.js';
import { DEFAULT_FILE_FILTERING_OPTIONS } from '../config/constants.js';
vi.mock('glob', { spy: true });
@@ -388,7 +385,7 @@ describe('GlobTool', () => {
it('should respect .geminiignore files by default', async () => {
await fs.writeFile(
path.join(tempRootDir, GEMINI_IGNORE_FILE_NAME),
path.join(tempRootDir, '.geminiignore'),
'gemini-ignored_test.txt',
);
await fs.writeFile(
@@ -426,7 +423,7 @@ describe('GlobTool', () => {
it('should not respect .geminiignore when respect_gemini_ignore is false', async () => {
await fs.writeFile(
path.join(tempRootDir, GEMINI_IGNORE_FILE_NAME),
path.join(tempRootDir, '.geminiignore'),
'gemini-ignored_test.txt',
);
await fs.writeFile(
+11 -5
View File
@@ -46,6 +46,11 @@ export interface GrepToolParams {
* File pattern to include in the search (e.g. "*.js", "*.{ts,tsx}")
*/
include?: string;
/**
* Max number of matches to return. Defaults to 20,000.
*/
limit?: number;
}
/**
@@ -184,7 +189,7 @@ class GrepToolInvocation extends BaseToolInvocation<
// Collect matches from all search directories
let allMatches: GrepMatch[] = [];
const totalMaxMatches = DEFAULT_TOTAL_MAX_MATCHES;
const totalMaxMatches = this.params.limit ?? DEFAULT_TOTAL_MAX_MATCHES;
// Create a timeout controller to prevent indefinitely hanging searches
const timeoutController = new AbortController();
@@ -352,10 +357,6 @@ class GrepToolInvocation extends BaseToolInvocation<
'--ignore-case',
pattern,
];
if (include) {
gitArgs.push('--', include);
}
try {
const generator = execStreaming('git', gitArgs, {
cwd: absolutePath,
@@ -587,6 +588,11 @@ export class GrepTool extends BaseDeclarativeTool<GrepToolParams, ToolResult> {
description: `Optional: A glob pattern to filter which files are searched (e.g., '*.js', '*.{ts,tsx}', 'src/**'). If omitted, searches all files (respecting potential global ignores).`,
type: 'string',
},
limit: {
description:
'Optional: Max number of matches to return. Defaults to 20,000.',
type: 'integer',
},
},
required: ['pattern'],
type: 'object',
+1 -5
View File
@@ -15,7 +15,6 @@ import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
import { ToolErrorType } from './tool-error.js';
import { WorkspaceContext } from '../utils/workspaceContext.js';
import { createMockMessageBus } from '../test-utils/mock-message-bus.js';
import { GEMINI_IGNORE_FILE_NAME } from '../config/constants.js';
describe('LSTool', () => {
let lsTool: LSTool;
@@ -183,10 +182,7 @@ describe('LSTool', () => {
it('should respect geminiignore patterns', async () => {
await fs.writeFile(path.join(tempRootDir, 'file1.txt'), 'content1');
await fs.writeFile(path.join(tempRootDir, 'file2.log'), 'content1');
await fs.writeFile(
path.join(tempRootDir, GEMINI_IGNORE_FILE_NAME),
'*.log',
);
await fs.writeFile(path.join(tempRootDir, '.geminiignore'), '*.log');
const invocation = lsTool.build({ dir_path: tempRootDir });
const result = await invocation.execute(abortSignal);
+1 -53
View File
@@ -19,7 +19,6 @@ import { StandardFileSystemService } from '../services/fileSystemService.js';
import { createMockWorkspaceContext } from '../test-utils/mockWorkspaceContext.js';
import { WorkspaceContext } from '../utils/workspaceContext.js';
import { createMockMessageBus } from '../test-utils/mock-message-bus.js';
import { GEMINI_IGNORE_FILE_NAME } from '../config/constants.js';
vi.mock('../telemetry/loggers.js', () => ({
logFileOperation: vi.fn(),
@@ -439,7 +438,7 @@ describe('ReadFileTool', () => {
describe('with .geminiignore', () => {
beforeEach(async () => {
await fsp.writeFile(
path.join(tempRootDir, GEMINI_IGNORE_FILE_NAME),
path.join(tempRootDir, '.geminiignore'),
['foo.*', 'ignored/'].join('\n'),
);
const mockConfigInstance = {
@@ -510,57 +509,6 @@ describe('ReadFileTool', () => {
const invocation = tool.build(params);
expect(typeof invocation).not.toBe('string');
});
it('should allow reading ignored files if respectGeminiIgnore is false', async () => {
const ignoredFilePath = path.join(tempRootDir, 'foo.bar');
await fsp.writeFile(ignoredFilePath, 'content', 'utf-8');
const configNoIgnore = {
getFileService: () => new FileDiscoveryService(tempRootDir),
getFileSystemService: () => new StandardFileSystemService(),
getTargetDir: () => tempRootDir,
getWorkspaceContext: () => new WorkspaceContext(tempRootDir),
getFileFilteringOptions: () => ({
respectGitIgnore: true,
respectGeminiIgnore: false,
}),
storage: {
getProjectTempDir: () => path.join(tempRootDir, '.temp'),
},
isInteractive: () => false,
isPathAllowed(this: Config, absolutePath: string): boolean {
const workspaceContext = this.getWorkspaceContext();
if (workspaceContext.isPathWithinWorkspace(absolutePath)) {
return true;
}
const projectTempDir = this.storage.getProjectTempDir();
return isSubpath(path.resolve(projectTempDir), absolutePath);
},
validatePathAccess(
this: Config,
absolutePath: string,
): string | null {
if (this.isPathAllowed(absolutePath)) {
return null;
}
const workspaceDirs = this.getWorkspaceContext().getDirectories();
const projectTempDir = this.storage.getProjectTempDir();
return `Path not in workspace: Attempted path "${absolutePath}" resolves outside the allowed workspace directories: ${workspaceDirs.join(', ')} or the project temp directory: ${projectTempDir}`;
},
} as unknown as Config;
const toolNoIgnore = new ReadFileTool(
configNoIgnore,
createMockMessageBus(),
);
const params: ReadFileToolParams = {
file_path: ignoredFilePath,
};
const invocation = toolNoIgnore.build(params);
expect(typeof invocation).not.toBe('string');
});
});
});
});
+4 -14
View File
@@ -22,7 +22,6 @@ import { getProgrammingLanguage } from '../telemetry/telemetry-utils.js';
import { logFileOperation } from '../telemetry/loggers.js';
import { FileOperationEvent } from '../telemetry/types.js';
import { READ_FILE_TOOL_NAME } from './tool-names.js';
import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
/**
* Parameters for the ReadFile tool
@@ -160,7 +159,6 @@ export class ReadFileTool extends BaseDeclarativeTool<
ToolResult
> {
static readonly Name = READ_FILE_TOOL_NAME;
private readonly fileDiscoveryService: FileDiscoveryService;
constructor(
private config: Config,
@@ -179,12 +177,12 @@ export class ReadFileTool extends BaseDeclarativeTool<
},
offset: {
description:
"Optional: For text files, the 0-based line number to start reading from. Requires 'limit' to be set. Use for paginating through large files.",
"Optional: For text files, the 0-based line number to start reading from. Requires 'limit' to be set. Use with 'limit' to target specific lines.",
type: 'number',
},
limit: {
description:
"Optional: For text files, maximum number of lines to read. Use with 'offset' to paginate through large files. If omitted, reads the entire file (if feasible, up to a default limit).",
"Optional: For text files, maximum number of lines to read. Use with 'offset' to paginate through large files. When checking for context or verifying changes, always set this to a small value (e.g. 50) to avoid reading the entire file.",
type: 'number',
},
},
@@ -195,10 +193,6 @@ export class ReadFileTool extends BaseDeclarativeTool<
true,
false,
);
this.fileDiscoveryService = new FileDiscoveryService(
config.getTargetDir(),
config.getFileFilteringOptions(),
);
}
protected override validateToolParamValues(
@@ -225,13 +219,9 @@ export class ReadFileTool extends BaseDeclarativeTool<
return 'Limit must be a positive number';
}
const fileService = this.config.getFileService();
const fileFilteringOptions = this.config.getFileFilteringOptions();
if (
this.fileDiscoveryService.shouldIgnoreFile(
resolvedPath,
fileFilteringOptions,
)
) {
if (fileService.shouldIgnoreFile(resolvedPath, fileFilteringOptions)) {
return `File path '${resolvedPath}' is ignored by configured ignore patterns.`;
}
@@ -23,7 +23,6 @@ import {
} from '../utils/ignorePatterns.js';
import * as glob from 'glob';
import { createMockMessageBus } from '../test-utils/mock-message-bus.js';
import { GEMINI_IGNORE_FILE_NAME } from '../config/constants.js';
vi.mock('glob', { spy: true });
@@ -71,7 +70,7 @@ describe('ReadManyFilesTool', () => {
tempDirOutsideRoot = fs.realpathSync(
fs.mkdtempSync(path.join(os.tmpdir(), 'read-many-files-external-')),
);
fs.writeFileSync(path.join(tempRootDir, GEMINI_IGNORE_FILE_NAME), 'foo.*');
fs.writeFileSync(path.join(tempRootDir, '.geminiignore'), 'foo.*');
const fileService = new FileDiscoveryService(tempRootDir);
const mockConfig = {
getFileService: () => fileService,
@@ -80,7 +79,6 @@ describe('ReadManyFilesTool', () => {
getFileFilteringOptions: () => ({
respectGitIgnore: true,
respectGeminiIgnore: true,
customIgnoreFilePaths: [],
}),
getTargetDir: () => tempRootDir,
getWorkspaceDirs: () => [tempRootDir],
@@ -518,7 +516,6 @@ describe('ReadManyFilesTool', () => {
getFileFilteringOptions: () => ({
respectGitIgnore: true,
respectGeminiIgnore: true,
customIgnoreFilePaths: [],
}),
getWorkspaceContext: () => new WorkspaceContext(tempDir1, [tempDir2]),
getTargetDir: () => tempDir1,
+3 -89
View File
@@ -21,7 +21,6 @@ import fs from 'node:fs/promises';
import os from 'node:os';
import type { Config } from '../config/config.js';
import { Storage } from '../config/storage.js';
import { GEMINI_IGNORE_FILE_NAME } from '../config/constants.js';
import { createMockWorkspaceContext } from '../test-utils/mockWorkspaceContext.js';
import type { ChildProcess } from 'node:child_process';
import { spawn } from 'node:child_process';
@@ -248,17 +247,7 @@ describe('RipGrepTool', () => {
let ripgrepBinaryPath: string;
let grepTool: RipGrepTool;
const abortSignal = new AbortController().signal;
let mockConfig = {
getTargetDir: () => tempRootDir,
getWorkspaceContext: () => createMockWorkspaceContext(tempRootDir),
getDebugMode: () => false,
getFileFilteringRespectGeminiIgnore: () => true,
getFileFilteringOptions: () => ({
respectGitIgnore: true,
respectGeminiIgnore: true,
}),
} as unknown as Config;
let mockConfig: Config;
beforeEach(async () => {
downloadRipGrepMock.mockReset();
@@ -278,10 +267,6 @@ describe('RipGrepTool', () => {
getWorkspaceContext: () => createMockWorkspaceContext(tempRootDir),
getDebugMode: () => false,
getFileFilteringRespectGeminiIgnore: () => true,
getFileFilteringOptions: () => ({
respectGitIgnore: true,
respectGeminiIgnore: true,
}),
storage: {
getProjectTempDir: vi.fn().mockReturnValue('/tmp/project'),
},
@@ -683,57 +668,6 @@ describe('RipGrepTool', () => {
expect(result.returnDisplay).toContain('(limited)');
}, 10000);
it('should filter out files based on FileDiscoveryService even if ripgrep returns them', async () => {
// Create .geminiignore to ignore 'ignored.txt'
await fs.writeFile(
path.join(tempRootDir, GEMINI_IGNORE_FILE_NAME),
'ignored.txt',
);
// Re-initialize tool so FileDiscoveryService loads the new .geminiignore
const toolWithIgnore = new RipGrepTool(
mockConfig,
createMockMessageBus(),
);
// Mock ripgrep returning both an ignored file and an allowed file
mockSpawn.mockImplementationOnce(
createMockSpawn({
outputData:
JSON.stringify({
type: 'match',
data: {
path: { text: 'ignored.txt' },
line_number: 1,
lines: { text: 'should be ignored\n' },
},
}) +
'\n' +
JSON.stringify({
type: 'match',
data: {
path: { text: 'allowed.txt' },
line_number: 1,
lines: { text: 'should be kept\n' },
},
}) +
'\n',
exitCode: 0,
}),
);
const params: RipGrepToolParams = { pattern: 'should' };
const invocation = toolWithIgnore.build(params);
const result = await invocation.execute(abortSignal);
// Verify ignored file is filtered out
expect(result.llmContent).toContain('allowed.txt');
expect(result.llmContent).toContain('should be kept');
expect(result.llmContent).not.toContain('ignored.txt');
expect(result.llmContent).not.toContain('should be ignored');
expect(result.returnDisplay).toContain('Found 1 match');
});
it('should handle regex special characters correctly', async () => {
// Setup specific mock for this test - regex pattern 'foo.*bar' should match 'const foo = "bar";'
mockSpawn.mockImplementationOnce(
@@ -845,10 +779,6 @@ describe('RipGrepTool', () => {
createMockWorkspaceContext(tempRootDir, [secondDir]),
getDebugMode: () => false,
getFileFilteringRespectGeminiIgnore: () => true,
getFileFilteringOptions: () => ({
respectGitIgnore: true,
respectGeminiIgnore: true,
}),
storage: {
getProjectTempDir: vi.fn().mockReturnValue('/tmp/project'),
},
@@ -957,10 +887,6 @@ describe('RipGrepTool', () => {
createMockWorkspaceContext(tempRootDir, [secondDir]),
getDebugMode: () => false,
getFileFilteringRespectGeminiIgnore: () => true,
getFileFilteringOptions: () => ({
respectGitIgnore: true,
respectGeminiIgnore: true,
}),
storage: {
getProjectTempDir: vi.fn().mockReturnValue('/tmp/project'),
},
@@ -1478,17 +1404,13 @@ describe('RipGrepTool', () => {
});
it('should add .geminiignore when enabled and patterns exist', async () => {
const geminiIgnorePath = path.join(tempRootDir, GEMINI_IGNORE_FILE_NAME);
const geminiIgnorePath = path.join(tempRootDir, '.geminiignore');
await fs.writeFile(geminiIgnorePath, 'ignored.log');
const configWithGeminiIgnore = {
getTargetDir: () => tempRootDir,
getWorkspaceContext: () => createMockWorkspaceContext(tempRootDir),
getDebugMode: () => false,
getFileFilteringRespectGeminiIgnore: () => true,
getFileFilteringOptions: () => ({
respectGitIgnore: true,
respectGeminiIgnore: true,
}),
storage: {
getProjectTempDir: vi.fn().mockReturnValue('/tmp/project'),
},
@@ -1543,17 +1465,13 @@ describe('RipGrepTool', () => {
});
it('should skip .geminiignore when disabled', async () => {
const geminiIgnorePath = path.join(tempRootDir, GEMINI_IGNORE_FILE_NAME);
const geminiIgnorePath = path.join(tempRootDir, '.geminiignore');
await fs.writeFile(geminiIgnorePath, 'ignored.log');
const configWithoutGeminiIgnore = {
getTargetDir: () => tempRootDir,
getWorkspaceContext: () => createMockWorkspaceContext(tempRootDir),
getDebugMode: () => false,
getFileFilteringRespectGeminiIgnore: () => false,
getFileFilteringOptions: () => ({
respectGitIgnore: true,
respectGeminiIgnore: false,
}),
storage: {
getProjectTempDir: vi.fn().mockReturnValue('/tmp/project'),
},
@@ -1700,10 +1618,6 @@ describe('RipGrepTool', () => {
getWorkspaceContext: () =>
createMockWorkspaceContext(tempRootDir, ['/another/dir']),
getDebugMode: () => false,
getFileFilteringOptions: () => ({
respectGitIgnore: true,
respectGeminiIgnore: true,
}),
storage: {
getProjectTempDir: vi.fn().mockReturnValue('/tmp/project'),
},
+21 -29
View File
@@ -23,7 +23,7 @@ import {
FileExclusions,
COMMON_DIRECTORY_EXCLUDES,
} from '../utils/ignorePatterns.js';
import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
import { GeminiIgnoreParser } from '../utils/geminiIgnoreParser.js';
import { execStreaming } from '../utils/shell-utils.js';
import {
DEFAULT_TOTAL_MAX_MATCHES,
@@ -131,6 +131,11 @@ export interface RipGrepToolParams {
* If true, does not respect .gitignore or default ignores (like build/dist).
*/
no_ignore?: boolean;
/**
* Max number of matches to return. Defaults to 20,000.
*/
limit?: number;
}
/**
@@ -148,7 +153,7 @@ class GrepToolInvocation extends BaseToolInvocation<
> {
constructor(
private readonly config: Config,
private readonly fileDiscoveryService: FileDiscoveryService,
private readonly geminiIgnoreParser: GeminiIgnoreParser,
params: RipGrepToolParams,
messageBus: MessageBus,
_toolName?: string,
@@ -204,7 +209,7 @@ class GrepToolInvocation extends BaseToolInvocation<
const searchDirDisplay = pathParam;
const totalMaxMatches = DEFAULT_TOTAL_MAX_MATCHES;
const totalMaxMatches = this.params.limit ?? DEFAULT_TOTAL_MAX_MATCHES;
if (this.config.getDebugMode()) {
debugLogger.log(`[GrepTool] Total result limit: ${totalMaxMatches}`);
}
@@ -243,21 +248,6 @@ class GrepToolInvocation extends BaseToolInvocation<
signal.removeEventListener('abort', onAbort);
}
if (!this.params.no_ignore) {
const uniqueFiles = Array.from(
new Set(allMatches.map((m) => m.filePath)),
);
const absoluteFilePaths = uniqueFiles.map((f) =>
path.resolve(searchDirAbs, f),
);
const allowedFiles =
this.fileDiscoveryService.filterFiles(absoluteFilePaths);
const allowedSet = new Set(allowedFiles);
allMatches = allMatches.filter((m) =>
allowedSet.has(path.resolve(searchDirAbs, m.filePath)),
);
}
const searchLocationDescription = `in path "${searchDirDisplay}"`;
if (allMatches.length === 0) {
const noMatchMsg = `No matches found for pattern "${this.params.pattern}" ${searchLocationDescription}${this.params.include ? ` (filter: "${this.params.include}")` : ''}.`;
@@ -376,11 +366,12 @@ class GrepToolInvocation extends BaseToolInvocation<
rgArgs.push('--glob', `!${exclude}`);
});
// Add .geminiignore and custom ignore files support (if provided/mandated)
// (ripgrep natively handles .gitignore)
const geminiIgnorePaths = this.fileDiscoveryService.getIgnoreFilePaths();
for (const ignorePath of geminiIgnorePaths) {
rgArgs.push('--ignore-file', ignorePath);
if (this.config.getFileFilteringRespectGeminiIgnore()) {
// Add .geminiignore support (ripgrep natively handles .gitignore)
const geminiIgnorePath = this.geminiIgnoreParser.getIgnoreFilePath();
if (geminiIgnorePath) {
rgArgs.push('--ignore-file', geminiIgnorePath);
}
}
}
@@ -486,7 +477,7 @@ export class RipGrepTool extends BaseDeclarativeTool<
ToolResult
> {
static readonly Name = GREP_TOOL_NAME;
private readonly fileDiscoveryService: FileDiscoveryService;
private readonly geminiIgnoreParser: GeminiIgnoreParser;
constructor(
private readonly config: Config,
@@ -544,6 +535,10 @@ export class RipGrepTool extends BaseDeclarativeTool<
'If true, searches all files including those usually ignored (like in .gitignore, build/, dist/, etc). Defaults to false if omitted.',
type: 'boolean',
},
limit: {
description: 'Max number of matches to return. Defaults to 20,000.',
type: 'integer',
},
},
required: ['pattern'],
type: 'object',
@@ -552,10 +547,7 @@ export class RipGrepTool extends BaseDeclarativeTool<
true, // isOutputMarkdown
false, // canUpdateOutput
);
this.fileDiscoveryService = new FileDiscoveryService(
config.getTargetDir(),
config.getFileFilteringOptions(),
);
this.geminiIgnoreParser = new GeminiIgnoreParser(config.getTargetDir());
}
/**
@@ -608,7 +600,7 @@ export class RipGrepTool extends BaseDeclarativeTool<
): ToolInvocation<RipGrepToolParams, ToolResult> {
return new GrepToolInvocation(
this.config,
this.fileDiscoveryService,
this.geminiIgnoreParser,
params,
messageBus ?? this.messageBus,
_toolName,
@@ -10,7 +10,6 @@ import * as path from 'node:path';
import * as os from 'node:os';
import { bfsFileSearch, bfsFileSearchSync } from './bfsFileSearch.js';
import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
import { GEMINI_IGNORE_FILE_NAME } from 'src/config/constants.js';
describe('bfsFileSearch', () => {
let testRootDir: string;
@@ -132,7 +131,6 @@ describe('bfsFileSearch', () => {
fileFilteringOptions: {
respectGitIgnore: true,
respectGeminiIgnore: true,
customIgnoreFilePaths: [],
},
});
@@ -140,7 +138,7 @@ describe('bfsFileSearch', () => {
});
it('should ignore geminiignored files', async () => {
await createTestFile('node_modules/', 'project', GEMINI_IGNORE_FILE_NAME);
await createTestFile('node_modules/', 'project', '.geminiignore');
await createTestFile('content', 'project', 'node_modules', 'target.txt');
const targetFilePath = await createTestFile(
'content',
@@ -156,7 +154,6 @@ describe('bfsFileSearch', () => {
fileFilteringOptions: {
respectGitIgnore: false,
respectGeminiIgnore: true,
customIgnoreFilePaths: [],
},
});
@@ -186,7 +183,6 @@ describe('bfsFileSearch', () => {
fileFilteringOptions: {
respectGitIgnore: false,
respectGeminiIgnore: false,
customIgnoreFilePaths: [],
},
});
@@ -320,7 +316,6 @@ describe('bfsFileSearchSync', () => {
fileFilteringOptions: {
respectGitIgnore: true,
respectGeminiIgnore: true,
customIgnoreFilePaths: [],
},
});
@@ -12,8 +12,6 @@ import { crawl } from './crawler.js';
import { createTmpDir, cleanupTmpDir } from '@google/gemini-cli-test-utils';
import type { Ignore } from './ignore.js';
import { loadIgnoreRules } from './ignore.js';
import { GEMINI_IGNORE_FILE_NAME } from '../../config/constants.js';
import { FileDiscoveryService } from '../../services/fileDiscoveryService.js';
describe('crawler', () => {
let tmpDir: string;
@@ -26,16 +24,17 @@ describe('crawler', () => {
it('should use .geminiignore rules', async () => {
tmpDir = await createTmpDir({
[GEMINI_IGNORE_FILE_NAME]: 'dist/',
'.geminiignore': 'dist/',
dist: ['ignored.js'],
src: ['not-ignored.js'],
});
const service = new FileDiscoveryService(tmpDir, {
respectGitIgnore: false,
respectGeminiIgnore: true,
const ignore = loadIgnoreRules({
projectRoot: tmpDir,
useGitignore: false,
useGeminiignore: true,
ignoreDirs: [],
});
const ignore = loadIgnoreRules(service, []);
const results = await crawl({
crawlDirectory: tmpDir,
@@ -49,7 +48,7 @@ describe('crawler', () => {
expect.arrayContaining([
'.',
'src/',
GEMINI_IGNORE_FILE_NAME,
'.geminiignore',
'src/not-ignored.js',
]),
);
@@ -57,19 +56,19 @@ describe('crawler', () => {
it('should combine .gitignore and .geminiignore rules', async () => {
tmpDir = await createTmpDir({
'.git': {},
'.gitignore': 'dist/',
[GEMINI_IGNORE_FILE_NAME]: 'build/',
'.geminiignore': 'build/',
dist: ['ignored-by-git.js'],
build: ['ignored-by-gemini.js'],
src: ['not-ignored.js'],
});
const service = new FileDiscoveryService(tmpDir, {
respectGitIgnore: true,
respectGeminiIgnore: true,
const ignore = loadIgnoreRules({
projectRoot: tmpDir,
useGitignore: true,
useGeminiignore: true,
ignoreDirs: [],
});
const ignore = loadIgnoreRules(service, []);
const results = await crawl({
crawlDirectory: tmpDir,
@@ -83,7 +82,7 @@ describe('crawler', () => {
expect.arrayContaining([
'.',
'src/',
GEMINI_IGNORE_FILE_NAME,
'.geminiignore',
'.gitignore',
'src/not-ignored.js',
]),
@@ -96,11 +95,12 @@ describe('crawler', () => {
src: ['main.js'],
});
const service = new FileDiscoveryService(tmpDir, {
respectGitIgnore: false,
respectGeminiIgnore: false,
const ignore = loadIgnoreRules({
projectRoot: tmpDir,
useGitignore: false,
useGeminiignore: false,
ignoreDirs: ['logs'],
});
const ignore = loadIgnoreRules(service, ['logs']);
const results = await crawl({
crawlDirectory: tmpDir,
@@ -117,7 +117,6 @@ describe('crawler', () => {
it('should handle negated directories', async () => {
tmpDir = await createTmpDir({
'.git': {},
'.gitignore': ['build/**', '!build/public', '!build/public/**'].join(
'\n',
),
@@ -128,11 +127,12 @@ describe('crawler', () => {
src: ['main.js'],
});
const service = new FileDiscoveryService(tmpDir, {
respectGitIgnore: true,
respectGeminiIgnore: false,
const ignore = loadIgnoreRules({
projectRoot: tmpDir,
useGitignore: true,
useGeminiignore: false,
ignoreDirs: [],
});
const ignore = loadIgnoreRules(service, []);
const results = await crawl({
crawlDirectory: tmpDir,
@@ -157,17 +157,17 @@ describe('crawler', () => {
it('should handle root-level file negation', async () => {
tmpDir = await createTmpDir({
'.git': {},
'.gitignore': ['*.mk', '!Foo.mk'].join('\n'),
'bar.mk': '',
'Foo.mk': '',
});
const service = new FileDiscoveryService(tmpDir, {
respectGitIgnore: true,
respectGeminiIgnore: false,
const ignore = loadIgnoreRules({
projectRoot: tmpDir,
useGitignore: true,
useGeminiignore: false,
ignoreDirs: [],
});
const ignore = loadIgnoreRules(service, []);
const results = await crawl({
crawlDirectory: tmpDir,
@@ -184,7 +184,6 @@ describe('crawler', () => {
it('should handle directory negation with glob', async () => {
tmpDir = await createTmpDir({
'.git': {},
'.gitignore': [
'third_party/**',
'!third_party/foo',
@@ -201,11 +200,12 @@ describe('crawler', () => {
},
});
const service = new FileDiscoveryService(tmpDir, {
respectGitIgnore: true,
respectGeminiIgnore: false,
const ignore = loadIgnoreRules({
projectRoot: tmpDir,
useGitignore: true,
useGeminiignore: false,
ignoreDirs: [],
});
const ignore = loadIgnoreRules(service, []);
const results = await crawl({
crawlDirectory: tmpDir,
@@ -229,17 +229,17 @@ describe('crawler', () => {
it('should correctly handle negated patterns in .gitignore', async () => {
tmpDir = await createTmpDir({
'.git': {},
'.gitignore': ['dist/**', '!dist/keep.js'].join('\n'),
dist: ['ignore.js', 'keep.js'],
src: ['main.js'],
});
const service = new FileDiscoveryService(tmpDir, {
respectGitIgnore: true,
respectGeminiIgnore: false,
const ignore = loadIgnoreRules({
projectRoot: tmpDir,
useGitignore: true,
useGeminiignore: false,
ignoreDirs: [],
});
const ignore = loadIgnoreRules(service, []);
const results = await crawl({
crawlDirectory: tmpDir,
@@ -266,11 +266,12 @@ describe('crawler', () => {
src: ['file1.js'],
});
const service = new FileDiscoveryService(tmpDir, {
respectGitIgnore: true,
respectGeminiIgnore: true,
const ignore = loadIgnoreRules({
projectRoot: tmpDir,
useGitignore: true,
useGeminiignore: true,
ignoreDirs: [],
});
const ignore = loadIgnoreRules(service, []);
const results = await crawl({
crawlDirectory: tmpDir,
@@ -286,16 +287,16 @@ describe('crawler', () => {
it('should handle empty or commented-only ignore files', async () => {
tmpDir = await createTmpDir({
'.git': {},
'.gitignore': '# This is a comment\n\n \n',
src: ['main.js'],
});
const service = new FileDiscoveryService(tmpDir, {
respectGitIgnore: true,
respectGeminiIgnore: false,
const ignore = loadIgnoreRules({
projectRoot: tmpDir,
useGitignore: true,
useGeminiignore: false,
ignoreDirs: [],
});
const ignore = loadIgnoreRules(service, []);
const results = await crawl({
crawlDirectory: tmpDir,
@@ -316,11 +317,12 @@ describe('crawler', () => {
src: ['main.js'],
});
const service = new FileDiscoveryService(tmpDir, {
respectGitIgnore: false,
respectGeminiIgnore: false,
const ignore = loadIgnoreRules({
projectRoot: tmpDir,
useGitignore: false,
useGeminiignore: false,
ignoreDirs: [],
});
const ignore = loadIgnoreRules(service, []);
const results = await crawl({
crawlDirectory: tmpDir,
@@ -347,11 +349,12 @@ describe('crawler', () => {
it('should hit the cache for subsequent crawls', async () => {
tmpDir = await createTmpDir({ 'file1.js': '' });
const service = new FileDiscoveryService(tmpDir, {
respectGitIgnore: false,
respectGeminiIgnore: false,
const ignore = loadIgnoreRules({
projectRoot: tmpDir,
useGitignore: false,
useGeminiignore: false,
ignoreDirs: [],
});
const ignore = loadIgnoreRules(service, []);
const options = {
crawlDirectory: tmpDir,
cwd: tmpDir,
@@ -379,19 +382,17 @@ describe('crawler', () => {
it('should miss the cache when ignore rules change', async () => {
tmpDir = await createTmpDir({
'.git': {},
'.gitignore': 'a.txt',
'a.txt': '',
'b.txt': '',
});
const getIgnore = () =>
loadIgnoreRules(
new FileDiscoveryService(tmpDir, {
respectGitIgnore: true,
respectGeminiIgnore: false,
}),
[],
);
loadIgnoreRules({
projectRoot: tmpDir,
useGitignore: true,
useGeminiignore: false,
ignoreDirs: [],
});
const getOptions = (ignore: Ignore) => ({
crawlDirectory: tmpDir,
cwd: tmpDir,
@@ -420,11 +421,12 @@ describe('crawler', () => {
it('should miss the cache after TTL expires', async () => {
tmpDir = await createTmpDir({ 'file1.js': '' });
const service = new FileDiscoveryService(tmpDir, {
respectGitIgnore: false,
respectGeminiIgnore: false,
const ignore = loadIgnoreRules({
projectRoot: tmpDir,
useGitignore: false,
useGeminiignore: false,
ignoreDirs: [],
});
const ignore = loadIgnoreRules(service, []);
const options = {
crawlDirectory: tmpDir,
cwd: tmpDir,
@@ -450,11 +452,12 @@ describe('crawler', () => {
it('should miss the cache when maxDepth changes', async () => {
tmpDir = await createTmpDir({ 'file1.js': '' });
const service = new FileDiscoveryService(tmpDir, {
respectGitIgnore: false,
respectGeminiIgnore: false,
const ignore = loadIgnoreRules({
projectRoot: tmpDir,
useGitignore: false,
useGeminiignore: false,
ignoreDirs: [],
});
const ignore = loadIgnoreRules(service, []);
const getOptions = (maxDepth?: number) => ({
crawlDirectory: tmpDir,
cwd: tmpDir,
@@ -501,11 +504,12 @@ describe('crawler', () => {
});
const getCrawlResults = async (maxDepth?: number) => {
const service = new FileDiscoveryService(tmpDir, {
respectGitIgnore: false,
respectGeminiIgnore: false,
const ignore = loadIgnoreRules({
projectRoot: tmpDir,
useGitignore: false,
useGeminiignore: false,
ignoreDirs: [],
});
const ignore = loadIgnoreRules(service, []);
const paths = await crawl({
crawlDirectory: tmpDir,
cwd: tmpDir,
@@ -576,11 +580,12 @@ describe('crawler', () => {
'file3.js': '',
});
const service = new FileDiscoveryService(tmpDir, {
respectGitIgnore: false,
respectGeminiIgnore: false,
const ignore = loadIgnoreRules({
projectRoot: tmpDir,
useGitignore: false,
useGeminiignore: false,
ignoreDirs: [],
});
const ignore = loadIgnoreRules(service, []);
const paths = await crawl({
crawlDirectory: tmpDir,
@@ -8,8 +8,6 @@ import { describe, it, expect, afterEach, vi } from 'vitest';
import { FileSearchFactory, AbortError, filter } from './fileSearch.js';
import { createTmpDir, cleanupTmpDir } from '@google/gemini-cli-test-utils';
import * as crawler from './crawler.js';
import { GEMINI_IGNORE_FILE_NAME } from '../../config/constants.js';
import { FileDiscoveryService } from '../../services/fileDiscoveryService.js';
describe('FileSearch', () => {
let tmpDir: string;
@@ -22,17 +20,15 @@ describe('FileSearch', () => {
it('should use .geminiignore rules', async () => {
tmpDir = await createTmpDir({
[GEMINI_IGNORE_FILE_NAME]: 'dist/',
'.geminiignore': 'dist/',
dist: ['ignored.js'],
src: ['not-ignored.js'],
});
const fileSearch = FileSearchFactory.create({
projectRoot: tmpDir,
fileDiscoveryService: new FileDiscoveryService(tmpDir, {
respectGitIgnore: false,
respectGeminiIgnore: true,
}),
useGitignore: false,
useGeminiignore: true,
ignoreDirs: [],
cache: false,
cacheTtl: 0,
@@ -43,18 +39,13 @@ describe('FileSearch', () => {
await fileSearch.initialize();
const results = await fileSearch.search('');
expect(results).toEqual([
'src/',
GEMINI_IGNORE_FILE_NAME,
'src/not-ignored.js',
]);
expect(results).toEqual(['src/', '.geminiignore', 'src/not-ignored.js']);
});
it('should combine .gitignore and .geminiignore rules', async () => {
tmpDir = await createTmpDir({
'.git': {},
'.gitignore': 'dist/',
[GEMINI_IGNORE_FILE_NAME]: 'build/',
'.geminiignore': 'build/',
dist: ['ignored-by-git.js'],
build: ['ignored-by-gemini.js'],
src: ['not-ignored.js'],
@@ -62,10 +53,8 @@ describe('FileSearch', () => {
const fileSearch = FileSearchFactory.create({
projectRoot: tmpDir,
fileDiscoveryService: new FileDiscoveryService(tmpDir, {
respectGitIgnore: true,
respectGeminiIgnore: true,
}),
useGitignore: true,
useGeminiignore: true,
ignoreDirs: [],
cache: false,
cacheTtl: 0,
@@ -78,7 +67,7 @@ describe('FileSearch', () => {
expect(results).toEqual([
'src/',
GEMINI_IGNORE_FILE_NAME,
'.geminiignore',
'.gitignore',
'src/not-ignored.js',
]);
@@ -92,10 +81,8 @@ describe('FileSearch', () => {
const fileSearch = FileSearchFactory.create({
projectRoot: tmpDir,
fileDiscoveryService: new FileDiscoveryService(tmpDir, {
respectGitIgnore: false,
respectGeminiIgnore: false,
}),
useGitignore: false,
useGeminiignore: false,
ignoreDirs: ['logs'],
cache: false,
cacheTtl: 0,
@@ -111,7 +98,6 @@ describe('FileSearch', () => {
it('should handle negated directories', async () => {
tmpDir = await createTmpDir({
'.git': {},
'.gitignore': ['build/**', '!build/public', '!build/public/**'].join(
'\n',
),
@@ -124,10 +110,8 @@ describe('FileSearch', () => {
const fileSearch = FileSearchFactory.create({
projectRoot: tmpDir,
fileDiscoveryService: new FileDiscoveryService(tmpDir, {
respectGitIgnore: true,
respectGeminiIgnore: false,
}),
useGitignore: true,
useGeminiignore: false,
ignoreDirs: [],
cache: false,
cacheTtl: 0,
@@ -159,10 +143,8 @@ describe('FileSearch', () => {
const fileSearch = FileSearchFactory.create({
projectRoot: tmpDir,
fileDiscoveryService: new FileDiscoveryService(tmpDir, {
respectGitIgnore: false,
respectGeminiIgnore: false,
}),
useGitignore: false,
useGeminiignore: false,
ignoreDirs: [],
cache: false,
cacheTtl: 0,
@@ -178,7 +160,6 @@ describe('FileSearch', () => {
it('should handle root-level file negation', async () => {
tmpDir = await createTmpDir({
'.git': {},
'.gitignore': ['*.mk', '!Foo.mk'].join('\n'),
'bar.mk': '',
'Foo.mk': '',
@@ -186,10 +167,8 @@ describe('FileSearch', () => {
const fileSearch = FileSearchFactory.create({
projectRoot: tmpDir,
fileDiscoveryService: new FileDiscoveryService(tmpDir, {
respectGitIgnore: true,
respectGeminiIgnore: false,
}),
useGitignore: true,
useGeminiignore: false,
ignoreDirs: [],
cache: false,
cacheTtl: 0,
@@ -205,7 +184,6 @@ describe('FileSearch', () => {
it('should handle directory negation with glob', async () => {
tmpDir = await createTmpDir({
'.git': {},
'.gitignore': [
'third_party/**',
'!third_party/foo',
@@ -224,10 +202,8 @@ describe('FileSearch', () => {
const fileSearch = FileSearchFactory.create({
projectRoot: tmpDir,
fileDiscoveryService: new FileDiscoveryService(tmpDir, {
respectGitIgnore: true,
respectGeminiIgnore: false,
}),
useGitignore: true,
useGeminiignore: false,
ignoreDirs: [],
cache: false,
cacheTtl: 0,
@@ -249,7 +225,6 @@ describe('FileSearch', () => {
it('should correctly handle negated patterns in .gitignore', async () => {
tmpDir = await createTmpDir({
'.git': {},
'.gitignore': ['dist/**', '!dist/keep.js'].join('\n'),
dist: ['ignore.js', 'keep.js'],
src: ['main.js'],
@@ -257,10 +232,8 @@ describe('FileSearch', () => {
const fileSearch = FileSearchFactory.create({
projectRoot: tmpDir,
fileDiscoveryService: new FileDiscoveryService(tmpDir, {
respectGitIgnore: true,
respectGeminiIgnore: false,
}),
useGitignore: true,
useGeminiignore: false,
ignoreDirs: [],
cache: false,
cacheTtl: 0,
@@ -289,10 +262,8 @@ describe('FileSearch', () => {
const fileSearch = FileSearchFactory.create({
projectRoot: tmpDir,
fileDiscoveryService: new FileDiscoveryService(tmpDir, {
respectGitIgnore: true,
respectGeminiIgnore: true,
}),
useGitignore: true,
useGeminiignore: true,
ignoreDirs: [],
cache: false,
cacheTtl: 0,
@@ -318,10 +289,8 @@ describe('FileSearch', () => {
const fileSearch = FileSearchFactory.create({
projectRoot: tmpDir,
fileDiscoveryService: new FileDiscoveryService(tmpDir, {
respectGitIgnore: false,
respectGeminiIgnore: false,
}),
useGitignore: false,
useGeminiignore: false,
ignoreDirs: [],
cache: false,
cacheTtl: 0,
@@ -346,10 +315,8 @@ describe('FileSearch', () => {
const fileSearch = FileSearchFactory.create({
projectRoot: tmpDir,
fileDiscoveryService: new FileDiscoveryService(tmpDir, {
respectGitIgnore: false,
respectGeminiIgnore: false,
}),
useGitignore: false,
useGeminiignore: false,
ignoreDirs: [],
cache: false,
cacheTtl: 0,
@@ -374,10 +341,8 @@ describe('FileSearch', () => {
const fileSearch = FileSearchFactory.create({
projectRoot: tmpDir,
fileDiscoveryService: new FileDiscoveryService(tmpDir, {
respectGitIgnore: false,
respectGeminiIgnore: false,
}),
useGitignore: false,
useGeminiignore: false,
ignoreDirs: [],
cache: false,
cacheTtl: 0,
@@ -402,10 +367,8 @@ describe('FileSearch', () => {
const fileSearch = FileSearchFactory.create({
projectRoot: tmpDir,
fileDiscoveryService: new FileDiscoveryService(tmpDir, {
respectGitIgnore: false,
respectGeminiIgnore: false,
}),
useGitignore: false,
useGeminiignore: false,
ignoreDirs: [],
cache: false,
cacheTtl: 0,
@@ -428,10 +391,8 @@ describe('FileSearch', () => {
const fileSearch = FileSearchFactory.create({
projectRoot: tmpDir,
fileDiscoveryService: new FileDiscoveryService(tmpDir, {
respectGitIgnore: false,
respectGeminiIgnore: false,
}),
useGitignore: false,
useGeminiignore: false,
ignoreDirs: [],
cache: false,
cacheTtl: 0,
@@ -461,10 +422,8 @@ describe('FileSearch', () => {
tmpDir = await createTmpDir({});
const fileSearch = FileSearchFactory.create({
projectRoot: tmpDir,
fileDiscoveryService: new FileDiscoveryService(tmpDir, {
respectGitIgnore: false,
respectGeminiIgnore: false,
}),
useGitignore: false,
useGeminiignore: false,
ignoreDirs: [],
cache: false,
cacheTtl: 0,
@@ -479,17 +438,14 @@ describe('FileSearch', () => {
it('should handle empty or commented-only ignore files', async () => {
tmpDir = await createTmpDir({
'.git': {},
'.gitignore': '# This is a comment\n\n \n',
src: ['main.js'],
});
const fileSearch = FileSearchFactory.create({
projectRoot: tmpDir,
fileDiscoveryService: new FileDiscoveryService(tmpDir, {
respectGitIgnore: true,
respectGeminiIgnore: false,
}),
useGitignore: true,
useGeminiignore: false,
ignoreDirs: [],
cache: false,
cacheTtl: 0,
@@ -511,10 +467,8 @@ describe('FileSearch', () => {
const fileSearch = FileSearchFactory.create({
projectRoot: tmpDir,
fileDiscoveryService: new FileDiscoveryService(tmpDir, {
respectGitIgnore: false, // Explicitly disable .gitignore to isolate this rule
respectGeminiIgnore: false,
}),
useGitignore: false, // Explicitly disable .gitignore to isolate this rule
useGeminiignore: false,
ignoreDirs: [],
cache: false,
cacheTtl: 0,
@@ -537,10 +491,8 @@ describe('FileSearch', () => {
const fileSearch = FileSearchFactory.create({
projectRoot: tmpDir,
fileDiscoveryService: new FileDiscoveryService(tmpDir, {
respectGitIgnore: false,
respectGeminiIgnore: false,
}),
useGitignore: false,
useGeminiignore: false,
ignoreDirs: [],
cache: false,
cacheTtl: 0,
@@ -566,10 +518,8 @@ describe('FileSearch', () => {
const fileSearch = FileSearchFactory.create({
projectRoot: tmpDir,
fileDiscoveryService: new FileDiscoveryService(tmpDir, {
respectGitIgnore: false,
respectGeminiIgnore: false,
}),
useGitignore: false,
useGeminiignore: false,
ignoreDirs: [],
cache: false,
cacheTtl: 0,
@@ -605,10 +555,8 @@ describe('FileSearch', () => {
const fileSearch = FileSearchFactory.create({
projectRoot: tmpDir,
fileDiscoveryService: new FileDiscoveryService(tmpDir, {
respectGitIgnore: false,
respectGeminiIgnore: false,
}),
useGitignore: false,
useGeminiignore: false,
ignoreDirs: [],
cache: true, // Enable caching for this test
cacheTtl: 0,
@@ -647,10 +595,8 @@ describe('FileSearch', () => {
const fileSearch = FileSearchFactory.create({
projectRoot: tmpDir,
fileDiscoveryService: new FileDiscoveryService(tmpDir, {
respectGitIgnore: false,
respectGeminiIgnore: false,
}),
useGitignore: false,
useGeminiignore: false,
ignoreDirs: [],
cache: false,
cacheTtl: 0,
@@ -693,10 +639,8 @@ describe('FileSearch', () => {
const fileSearch = FileSearchFactory.create({
projectRoot: tmpDir,
fileDiscoveryService: new FileDiscoveryService(tmpDir, {
respectGitIgnore: false,
respectGeminiIgnore: false,
}),
useGitignore: false,
useGeminiignore: false,
ignoreDirs: [],
cache: true, // Ensure caching is enabled
cacheTtl: 10000,
@@ -733,10 +677,8 @@ describe('FileSearch', () => {
const fileSearch = FileSearchFactory.create({
projectRoot: tmpDir,
fileDiscoveryService: new FileDiscoveryService(tmpDir, {
respectGitIgnore: false,
respectGeminiIgnore: false,
}),
useGitignore: false,
useGeminiignore: false,
ignoreDirs: [],
cache: false,
cacheTtl: 0,
@@ -765,10 +707,8 @@ describe('FileSearch', () => {
const fileSearch = FileSearchFactory.create({
projectRoot: tmpDir,
fileDiscoveryService: new FileDiscoveryService(tmpDir, {
respectGitIgnore: false,
respectGeminiIgnore: false,
}),
useGitignore: false,
useGeminiignore: false,
ignoreDirs: [],
cache: false,
cacheTtl: 0,
@@ -792,10 +732,8 @@ describe('FileSearch', () => {
const fileSearch = FileSearchFactory.create({
projectRoot: tmpDir,
fileDiscoveryService: new FileDiscoveryService(tmpDir, {
respectGitIgnore: false,
respectGeminiIgnore: false,
}),
useGitignore: false,
useGeminiignore: false,
ignoreDirs: [],
cache: false,
cacheTtl: 0,
@@ -819,10 +757,8 @@ describe('FileSearch', () => {
const fileSearch = FileSearchFactory.create({
projectRoot: tmpDir,
fileDiscoveryService: new FileDiscoveryService(tmpDir, {
respectGitIgnore: false,
respectGeminiIgnore: false,
}),
useGitignore: false,
useGeminiignore: false,
ignoreDirs: [],
cache: false,
cacheTtl: 0,
@@ -837,7 +773,6 @@ describe('FileSearch', () => {
it('should respect ignore rules', async () => {
tmpDir = await createTmpDir({
'.git': {},
'.gitignore': '*.js',
'file1.js': '',
'file2.ts': '',
@@ -845,10 +780,8 @@ describe('FileSearch', () => {
const fileSearch = FileSearchFactory.create({
projectRoot: tmpDir,
fileDiscoveryService: new FileDiscoveryService(tmpDir, {
respectGitIgnore: true,
respectGeminiIgnore: false,
}),
useGitignore: true,
useGeminiignore: false,
ignoreDirs: [],
cache: false,
cacheTtl: 0,
@@ -13,12 +13,12 @@ import { crawl } from './crawler.js';
import type { FzfResultItem } from 'fzf';
import { AsyncFzf } from 'fzf';
import { unescapePath } from '../paths.js';
import type { FileDiscoveryService } from '../../services/fileDiscoveryService.js';
export interface FileSearchOptions {
projectRoot: string;
ignoreDirs: string[];
fileDiscoveryService: FileDiscoveryService;
useGitignore: boolean;
useGeminiignore: boolean;
cache: boolean;
cacheTtl: number;
enableRecursiveFileSearch: boolean;
@@ -101,10 +101,7 @@ class RecursiveFileSearch implements FileSearch {
constructor(private readonly options: FileSearchOptions) {}
async initialize(): Promise<void> {
this.ignore = loadIgnoreRules(
this.options.fileDiscoveryService,
this.options.ignoreDirs,
);
this.ignore = loadIgnoreRules(this.options);
this.allFiles = await crawl({
crawlDirectory: this.options.projectRoot,
@@ -203,10 +200,7 @@ class DirectoryFileSearch implements FileSearch {
constructor(private readonly options: FileSearchOptions) {}
async initialize(): Promise<void> {
this.ignore = loadIgnoreRules(
this.options.fileDiscoveryService,
this.options.ignoreDirs,
);
this.ignore = loadIgnoreRules(this.options);
}
async search(
@@ -7,8 +7,6 @@
import { describe, it, expect, afterEach } from 'vitest';
import { Ignore, loadIgnoreRules } from './ignore.js';
import { createTmpDir, cleanupTmpDir } from '@google/gemini-cli-test-utils';
import { GEMINI_IGNORE_FILE_NAME } from '../../config/constants.js';
import { FileDiscoveryService } from '../../services/fileDiscoveryService.js';
describe('Ignore', () => {
describe('getDirectoryFilter', () => {
@@ -78,14 +76,14 @@ describe('loadIgnoreRules', () => {
it('should load rules from .gitignore', async () => {
tmpDir = await createTmpDir({
'.git': {},
'.gitignore': '*.log',
});
const service = new FileDiscoveryService(tmpDir, {
respectGitIgnore: true,
respectGeminiIgnore: false,
const ignore = loadIgnoreRules({
projectRoot: tmpDir,
useGitignore: true,
useGeminiignore: false,
ignoreDirs: [],
});
const ignore = loadIgnoreRules(service, []);
const fileFilter = ignore.getFileFilter();
expect(fileFilter('test.log')).toBe(true);
expect(fileFilter('test.txt')).toBe(false);
@@ -93,13 +91,14 @@ describe('loadIgnoreRules', () => {
it('should load rules from .geminiignore', async () => {
tmpDir = await createTmpDir({
[GEMINI_IGNORE_FILE_NAME]: '*.log',
'.geminiignore': '*.log',
});
const service = new FileDiscoveryService(tmpDir, {
respectGitIgnore: false,
respectGeminiIgnore: true,
const ignore = loadIgnoreRules({
projectRoot: tmpDir,
useGitignore: false,
useGeminiignore: true,
ignoreDirs: [],
});
const ignore = loadIgnoreRules(service, []);
const fileFilter = ignore.getFileFilter();
expect(fileFilter('test.log')).toBe(true);
expect(fileFilter('test.txt')).toBe(false);
@@ -107,15 +106,15 @@ describe('loadIgnoreRules', () => {
it('should combine rules from .gitignore and .geminiignore', async () => {
tmpDir = await createTmpDir({
'.git': {},
'.gitignore': '*.log',
[GEMINI_IGNORE_FILE_NAME]: '*.txt',
'.geminiignore': '*.txt',
});
const service = new FileDiscoveryService(tmpDir, {
respectGitIgnore: true,
respectGeminiIgnore: true,
const ignore = loadIgnoreRules({
projectRoot: tmpDir,
useGitignore: true,
useGeminiignore: true,
ignoreDirs: [],
});
const ignore = loadIgnoreRules(service, []);
const fileFilter = ignore.getFileFilter();
expect(fileFilter('test.log')).toBe(true);
expect(fileFilter('test.txt')).toBe(true);
@@ -124,11 +123,12 @@ describe('loadIgnoreRules', () => {
it('should add ignoreDirs', async () => {
tmpDir = await createTmpDir({});
const service = new FileDiscoveryService(tmpDir, {
respectGitIgnore: false,
respectGeminiIgnore: false,
const ignore = loadIgnoreRules({
projectRoot: tmpDir,
useGitignore: false,
useGeminiignore: false,
ignoreDirs: ['logs/'],
});
const ignore = loadIgnoreRules(service, ['logs/']);
const dirFilter = ignore.getDirectoryFilter();
expect(dirFilter('logs/')).toBe(true);
expect(dirFilter('src/')).toBe(false);
@@ -136,22 +136,24 @@ describe('loadIgnoreRules', () => {
it('should handle missing ignore files gracefully', async () => {
tmpDir = await createTmpDir({});
const service = new FileDiscoveryService(tmpDir, {
respectGitIgnore: true,
respectGeminiIgnore: true,
const ignore = loadIgnoreRules({
projectRoot: tmpDir,
useGitignore: true,
useGeminiignore: true,
ignoreDirs: [],
});
const ignore = loadIgnoreRules(service, []);
const fileFilter = ignore.getFileFilter();
expect(fileFilter('anyfile.txt')).toBe(false);
});
it('should always add .git to the ignore list', async () => {
tmpDir = await createTmpDir({});
const service = new FileDiscoveryService(tmpDir, {
respectGitIgnore: false,
respectGeminiIgnore: false,
const ignore = loadIgnoreRules({
projectRoot: tmpDir,
useGitignore: false,
useGeminiignore: false,
ignoreDirs: [],
});
const ignore = loadIgnoreRules(service, []);
const dirFilter = ignore.getDirectoryFilter();
expect(dirFilter('.git/')).toBe(true);
});
+22 -12
View File
@@ -5,28 +5,38 @@
*/
import fs from 'node:fs';
import path from 'node:path';
import ignore from 'ignore';
import picomatch from 'picomatch';
import type { FileDiscoveryService } from '../../services/fileDiscoveryService.js';
const hasFileExtension = picomatch('**/*[*.]*');
export function loadIgnoreRules(
service: FileDiscoveryService,
ignoreDirs: string[] = [],
): Ignore {
const ignorer = new Ignore();
const ignoreFiles = service.getAllIgnoreFilePaths();
export interface LoadIgnoreRulesOptions {
projectRoot: string;
useGitignore: boolean;
useGeminiignore: boolean;
ignoreDirs: string[];
}
for (const filePath of ignoreFiles) {
if (fs.existsSync(filePath)) {
ignorer.add(fs.readFileSync(filePath, 'utf8'));
export function loadIgnoreRules(options: LoadIgnoreRulesOptions): Ignore {
const ignorer = new Ignore();
if (options.useGitignore) {
const gitignorePath = path.join(options.projectRoot, '.gitignore');
if (fs.existsSync(gitignorePath)) {
ignorer.add(fs.readFileSync(gitignorePath, 'utf8'));
}
}
const allIgnoreDirs = ['.git', ...ignoreDirs];
if (options.useGeminiignore) {
const geminiignorePath = path.join(options.projectRoot, '.geminiignore');
if (fs.existsSync(geminiignorePath)) {
ignorer.add(fs.readFileSync(geminiignorePath, 'utf8'));
}
}
const ignoreDirs = ['.git', ...options.ignoreDirs];
ignorer.add(
allIgnoreDirs.map((dir) => {
ignoreDirs.map((dir) => {
if (dir.endsWith('/')) {
return dir;
}
@@ -0,0 +1,134 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
import { GeminiIgnoreParser } from './geminiIgnoreParser.js';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import * as os from 'node:os';
describe('GeminiIgnoreParser', () => {
let projectRoot: string;
async function createTestFile(filePath: string, content = '') {
const fullPath = path.join(projectRoot, filePath);
await fs.mkdir(path.dirname(fullPath), { recursive: true });
await fs.writeFile(fullPath, content);
}
beforeEach(async () => {
projectRoot = await fs.mkdtemp(
path.join(os.tmpdir(), 'geminiignore-test-'),
);
});
afterEach(async () => {
await fs.rm(projectRoot, { recursive: true, force: true });
vi.restoreAllMocks();
});
describe('when .geminiignore exists', () => {
beforeEach(async () => {
await createTestFile(
'.geminiignore',
'ignored.txt\n# A comment\n/ignored_dir/\n',
);
await createTestFile('ignored.txt', 'ignored');
await createTestFile('not_ignored.txt', 'not ignored');
await createTestFile(
path.join('ignored_dir', 'file.txt'),
'in ignored dir',
);
await createTestFile(
path.join('subdir', 'not_ignored.txt'),
'not ignored',
);
});
it('should ignore files specified in .geminiignore', () => {
const parser = new GeminiIgnoreParser(projectRoot);
expect(parser.getPatterns()).toEqual(['ignored.txt', '/ignored_dir/']);
expect(parser.isIgnored('ignored.txt')).toBe(true);
expect(parser.isIgnored('not_ignored.txt')).toBe(false);
expect(parser.isIgnored(path.join('ignored_dir', 'file.txt'))).toBe(true);
expect(parser.isIgnored(path.join('subdir', 'not_ignored.txt'))).toBe(
false,
);
});
it('should return ignore file path when patterns exist', () => {
const parser = new GeminiIgnoreParser(projectRoot);
expect(parser.getIgnoreFilePath()).toBe(
path.join(projectRoot, '.geminiignore'),
);
});
it('should return true for hasPatterns when patterns exist', () => {
const parser = new GeminiIgnoreParser(projectRoot);
expect(parser.hasPatterns()).toBe(true);
});
it('should return false for hasPatterns when .geminiignore is deleted', async () => {
const parser = new GeminiIgnoreParser(projectRoot);
await fs.rm(path.join(projectRoot, '.geminiignore'));
expect(parser.hasPatterns()).toBe(false);
expect(parser.getIgnoreFilePath()).toBeNull();
});
});
describe('when .geminiignore does not exist', () => {
it('should not load any patterns and not ignore any files', () => {
const parser = new GeminiIgnoreParser(projectRoot);
expect(parser.getPatterns()).toEqual([]);
expect(parser.isIgnored('any_file.txt')).toBe(false);
});
it('should return null for getIgnoreFilePath when no patterns exist', () => {
const parser = new GeminiIgnoreParser(projectRoot);
expect(parser.getIgnoreFilePath()).toBeNull();
});
it('should return false for hasPatterns when no patterns exist', () => {
const parser = new GeminiIgnoreParser(projectRoot);
expect(parser.hasPatterns()).toBe(false);
});
});
describe('when .geminiignore is empty', () => {
beforeEach(async () => {
await createTestFile('.geminiignore', '');
});
it('should return null for getIgnoreFilePath', () => {
const parser = new GeminiIgnoreParser(projectRoot);
expect(parser.getIgnoreFilePath()).toBeNull();
});
it('should return false for hasPatterns', () => {
const parser = new GeminiIgnoreParser(projectRoot);
expect(parser.hasPatterns()).toBe(false);
});
});
describe('when .geminiignore only has comments', () => {
beforeEach(async () => {
await createTestFile(
'.geminiignore',
'# This is a comment\n# Another comment\n',
);
});
it('should return null for getIgnoreFilePath', () => {
const parser = new GeminiIgnoreParser(projectRoot);
expect(parser.getIgnoreFilePath()).toBeNull();
});
it('should return false for hasPatterns', () => {
const parser = new GeminiIgnoreParser(projectRoot);
expect(parser.hasPatterns()).toBe(false);
});
});
});
@@ -0,0 +1,105 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import * as fs from 'node:fs';
import * as path from 'node:path';
import ignore from 'ignore';
export interface GeminiIgnoreFilter {
isIgnored(filePath: string): boolean;
getPatterns(): string[];
getIgnoreFilePath(): string | null;
hasPatterns(): boolean;
}
export class GeminiIgnoreParser implements GeminiIgnoreFilter {
private projectRoot: string;
private patterns: string[] = [];
private ig = ignore();
constructor(projectRoot: string) {
this.projectRoot = path.resolve(projectRoot);
this.loadPatterns();
}
private loadPatterns(): void {
const patternsFilePath = path.join(this.projectRoot, '.geminiignore');
let content: string;
try {
content = fs.readFileSync(patternsFilePath, 'utf-8');
} catch (_error) {
// ignore file not found
return;
}
this.patterns = (content ?? '')
.split('\n')
.map((p) => p.trim())
.filter((p) => p !== '' && !p.startsWith('#'));
this.ig.add(this.patterns);
}
isIgnored(filePath: string): boolean {
if (this.patterns.length === 0) {
return false;
}
if (!filePath || typeof filePath !== 'string') {
return false;
}
if (
filePath.startsWith('\\') ||
filePath === '/' ||
filePath.includes('\0')
) {
return false;
}
const resolved = path.resolve(this.projectRoot, filePath);
const relativePath = path.relative(this.projectRoot, resolved);
if (relativePath === '' || relativePath.startsWith('..')) {
return false;
}
// Even in windows, Ignore expects forward slashes.
const normalizedPath = relativePath.replace(/\\/g, '/');
if (normalizedPath.startsWith('/') || normalizedPath === '') {
return false;
}
return this.ig.ignores(normalizedPath);
}
getPatterns(): string[] {
return this.patterns;
}
/**
* Returns the path to .geminiignore file if it exists and has patterns.
* Useful for tools like ripgrep that support --ignore-file flag.
*/
getIgnoreFilePath(): string | null {
if (!this.hasPatterns()) {
return null;
}
return path.join(this.projectRoot, '.geminiignore');
}
/**
* Returns true if .geminiignore exists and has patterns.
*/
hasPatterns(): boolean {
if (this.patterns.length === 0) {
return false;
}
const ignoreFilePath = path.join(this.projectRoot, '.geminiignore');
return fs.existsSync(ignoreFilePath);
}
}
@@ -12,7 +12,6 @@ import { getFolderStructure } from './getFolderStructure.js';
import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
import * as path from 'node:path';
import { GEMINI_DIR } from './paths.js';
import { GEMINI_IGNORE_FILE_NAME } from 'src/config/constants.js';
describe('getFolderStructure', () => {
let testRootDir: string;
@@ -286,7 +285,6 @@ ${testRootDir}${path.sep}
fileFilteringOptions: {
respectGeminiIgnore: false,
respectGitIgnore: false,
customIgnoreFilePaths: [],
},
});
@@ -298,7 +296,7 @@ ${testRootDir}${path.sep}
describe('with geminiignore', () => {
it('should ignore geminiignore files by default', async () => {
await fsPromises.writeFile(
nodePath.join(testRootDir, GEMINI_IGNORE_FILE_NAME),
nodePath.join(testRootDir, '.geminiignore'),
'ignored.txt\nnode_modules/\n.gemini/\n!/.gemini/config.yaml',
);
await createTestFile('file1.txt');
@@ -318,7 +316,7 @@ ${testRootDir}${path.sep}
it('should not ignore files if respectGeminiIgnore is false', async () => {
await fsPromises.writeFile(
nodePath.join(testRootDir, GEMINI_IGNORE_FILE_NAME),
nodePath.join(testRootDir, '.geminiignore'),
'ignored.txt\nnode_modules/\n.gemini/\n!/.gemini/config.yaml',
);
await createTestFile('file1.txt');
@@ -333,7 +331,6 @@ ${testRootDir}${path.sep}
fileFilteringOptions: {
respectGeminiIgnore: false,
respectGitIgnore: true, // Explicitly disable gemini ignore only
customIgnoreFilePaths: [],
},
});
expect(structure).toContain('ignored.txt');
+1 -1
View File
@@ -175,7 +175,7 @@ export class GitIgnoreParser implements GitIgnoreFilter {
const normalizedRelativeDir = relativeDir.replace(/\\/g, '/');
const igPlusExtras = ignore()
.add(ig)
.add(this.processedExtraPatterns); // takes priority over ig patterns
.add(this.processedExtraPatterns);
if (igPlusExtras.ignores(normalizedRelativeDir)) {
// This directory is ignored by an ancestor's .gitignore.
// According to git behavior, we don't need to process this
@@ -1,219 +0,0 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
import { IgnoreFileParser } from './ignoreFileParser.js';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import * as os from 'node:os';
import { GEMINI_IGNORE_FILE_NAME } from '../config/constants.js';
describe('GeminiIgnoreParser', () => {
let projectRoot: string;
async function createTestFile(filePath: string, content = '') {
const fullPath = path.join(projectRoot, filePath);
await fs.mkdir(path.dirname(fullPath), { recursive: true });
await fs.writeFile(fullPath, content);
}
beforeEach(async () => {
projectRoot = await fs.mkdtemp(
path.join(os.tmpdir(), 'geminiignore-test-'),
);
});
afterEach(async () => {
await fs.rm(projectRoot, { recursive: true, force: true });
vi.restoreAllMocks();
});
describe('when .geminiignore exists', () => {
beforeEach(async () => {
await createTestFile(
GEMINI_IGNORE_FILE_NAME,
'ignored.txt\n# A comment\n/ignored_dir/\n',
);
await createTestFile('ignored.txt', 'ignored');
await createTestFile('not_ignored.txt', 'not ignored');
await createTestFile(
path.join('ignored_dir', 'file.txt'),
'in ignored dir',
);
await createTestFile(
path.join('subdir', 'not_ignored.txt'),
'not ignored',
);
});
it('should ignore files specified in .geminiignore', () => {
const parser = new IgnoreFileParser(projectRoot, GEMINI_IGNORE_FILE_NAME);
expect(parser.getPatterns()).toEqual(['ignored.txt', '/ignored_dir/']);
expect(parser.isIgnored('ignored.txt')).toBe(true);
expect(parser.isIgnored('not_ignored.txt')).toBe(false);
expect(parser.isIgnored(path.join('ignored_dir', 'file.txt'))).toBe(true);
expect(parser.isIgnored(path.join('subdir', 'not_ignored.txt'))).toBe(
false,
);
});
it('should return ignore file path when patterns exist', () => {
const parser = new IgnoreFileParser(projectRoot, GEMINI_IGNORE_FILE_NAME);
expect(parser.getIgnoreFilePaths()).toEqual([
path.join(projectRoot, GEMINI_IGNORE_FILE_NAME),
]);
});
it('should return true for hasPatterns when patterns exist', () => {
const parser = new IgnoreFileParser(projectRoot, GEMINI_IGNORE_FILE_NAME);
expect(parser.hasPatterns()).toBe(true);
});
it('should maintain patterns in memory when .geminiignore is deleted', async () => {
const parser = new IgnoreFileParser(projectRoot, GEMINI_IGNORE_FILE_NAME);
await fs.rm(path.join(projectRoot, GEMINI_IGNORE_FILE_NAME));
expect(parser.hasPatterns()).toBe(true);
expect(parser.getIgnoreFilePaths()).toEqual([]);
});
});
describe('when .geminiignore does not exist', () => {
it('should not load any patterns and not ignore any files', () => {
const parser = new IgnoreFileParser(projectRoot, GEMINI_IGNORE_FILE_NAME);
expect(parser.getPatterns()).toEqual([]);
expect(parser.isIgnored('any_file.txt')).toBe(false);
});
it('should return empty array for getIgnoreFilePaths when no patterns exist', () => {
const parser = new IgnoreFileParser(projectRoot, GEMINI_IGNORE_FILE_NAME);
expect(parser.getIgnoreFilePaths()).toEqual([]);
});
it('should return false for hasPatterns when no patterns exist', () => {
const parser = new IgnoreFileParser(projectRoot, GEMINI_IGNORE_FILE_NAME);
expect(parser.hasPatterns()).toBe(false);
});
});
describe('when .geminiignore is empty', () => {
beforeEach(async () => {
await createTestFile(GEMINI_IGNORE_FILE_NAME, '');
});
it('should return file path for getIgnoreFilePaths', () => {
const parser = new IgnoreFileParser(projectRoot, GEMINI_IGNORE_FILE_NAME);
expect(parser.getIgnoreFilePaths()).toEqual([
path.join(projectRoot, GEMINI_IGNORE_FILE_NAME),
]);
});
it('should return false for hasPatterns', () => {
const parser = new IgnoreFileParser(projectRoot, GEMINI_IGNORE_FILE_NAME);
expect(parser.hasPatterns()).toBe(false);
});
});
describe('when .geminiignore only has comments', () => {
beforeEach(async () => {
await createTestFile(
GEMINI_IGNORE_FILE_NAME,
'# This is a comment\n# Another comment\n',
);
});
it('should return file path for getIgnoreFilePaths', () => {
const parser = new IgnoreFileParser(projectRoot, GEMINI_IGNORE_FILE_NAME);
expect(parser.getIgnoreFilePaths()).toEqual([
path.join(projectRoot, GEMINI_IGNORE_FILE_NAME),
]);
});
it('should return false for hasPatterns', () => {
const parser = new IgnoreFileParser(projectRoot, GEMINI_IGNORE_FILE_NAME);
expect(parser.hasPatterns()).toBe(false);
});
});
describe('when multiple ignore files are provided', () => {
const primaryFile = 'primary.ignore';
const secondaryFile = 'secondary.ignore';
beforeEach(async () => {
await createTestFile(primaryFile, '# Primary\n!important.txt\n');
await createTestFile(secondaryFile, '# Secondary\n*.txt\n');
await createTestFile('important.txt', 'important');
await createTestFile('other.txt', 'other');
});
it('should combine patterns from all files', () => {
const parser = new IgnoreFileParser(projectRoot, [
primaryFile,
secondaryFile,
]);
expect(parser.isIgnored('other.txt')).toBe(true);
});
it('should respect priority (first file overrides second)', () => {
const parser = new IgnoreFileParser(projectRoot, [
primaryFile,
secondaryFile,
]);
expect(parser.isIgnored('important.txt')).toBe(false);
});
it('should return all existing file paths in reverse order', () => {
const parser = new IgnoreFileParser(projectRoot, [
'nonexistent.ignore',
primaryFile,
secondaryFile,
]);
expect(parser.getIgnoreFilePaths()).toEqual([
path.join(projectRoot, secondaryFile),
path.join(projectRoot, primaryFile),
]);
});
});
describe('when patterns are passed directly', () => {
it('should ignore files matching the passed patterns', () => {
const parser = new IgnoreFileParser(projectRoot, ['*.log'], true);
expect(parser.isIgnored('debug.log')).toBe(true);
expect(parser.isIgnored('src/index.ts')).toBe(false);
});
it('should handle multiple patterns', () => {
const parser = new IgnoreFileParser(
projectRoot,
['*.log', 'temp/'],
true,
);
expect(parser.isIgnored('debug.log')).toBe(true);
expect(parser.isIgnored('temp/file.txt')).toBe(true);
expect(parser.isIgnored('src/index.ts')).toBe(false);
});
it('should respect precedence (later patterns override earlier ones)', () => {
const parser = new IgnoreFileParser(
projectRoot,
['*.txt', '!important.txt'],
true,
);
expect(parser.isIgnored('file.txt')).toBe(true);
expect(parser.isIgnored('important.txt')).toBe(false);
});
it('should return empty array for getIgnoreFilePaths', () => {
const parser = new IgnoreFileParser(projectRoot, ['*.log'], true);
expect(parser.getIgnoreFilePaths()).toEqual([]);
});
it('should return patterns via getPatterns', () => {
const patterns = ['*.log', '!debug.log'];
const parser = new IgnoreFileParser(projectRoot, patterns, true);
expect(parser.getPatterns()).toEqual(patterns);
});
});
});
-129
View File
@@ -1,129 +0,0 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import * as fs from 'node:fs';
import * as path from 'node:path';
import ignore from 'ignore';
import { debugLogger } from './debugLogger.js';
export interface IgnoreFileFilter {
isIgnored(filePath: string): boolean;
getPatterns(): string[];
getIgnoreFilePaths(): string[];
hasPatterns(): boolean;
}
/**
* An ignore file parser that reads the ignore files from the project root.
*/
export class IgnoreFileParser implements IgnoreFileFilter {
private projectRoot: string;
private patterns: string[] = [];
private ig = ignore();
private readonly fileNames: string[];
constructor(
projectRoot: string,
// The order matters: files listed earlier have higher priority.
// It can be a single file name/pattern or an array of file names/patterns.
input: string | string[],
isPatterns = false,
) {
this.projectRoot = path.resolve(projectRoot);
if (isPatterns) {
this.fileNames = [];
const patterns = Array.isArray(input) ? input : [input];
this.patterns.push(...patterns);
this.ig.add(patterns);
} else {
this.fileNames = Array.isArray(input) ? input : [input];
this.loadPatternsFromFiles();
}
}
private loadPatternsFromFiles(): void {
// Iterate in reverse order so that the first file in the list is processed last.
// This gives the first file the highest priority, as patterns added later override earlier ones.
for (const fileName of [...this.fileNames].reverse()) {
const patterns = this.parseIgnoreFile(fileName);
this.patterns.push(...patterns);
this.ig.add(patterns);
}
}
private parseIgnoreFile(fileName: string): string[] {
const patternsFilePath = path.join(this.projectRoot, fileName);
let content: string;
try {
content = fs.readFileSync(patternsFilePath, 'utf-8');
} catch (_error) {
debugLogger.debug(
`Ignore file not found: ${patternsFilePath}, continue without it.`,
);
return [];
}
debugLogger.debug(`Loading ignore patterns from: ${patternsFilePath}`);
return (content ?? '')
.split('\n')
.map((p) => p.trim())
.filter((p) => p !== '' && !p.startsWith('#'));
}
isIgnored(filePath: string): boolean {
if (this.patterns.length === 0) {
return false;
}
if (!filePath || typeof filePath !== 'string') {
return false;
}
if (
filePath.startsWith('\\') ||
filePath === '/' ||
filePath.includes('\0')
) {
return false;
}
const resolved = path.resolve(this.projectRoot, filePath);
const relativePath = path.relative(this.projectRoot, resolved);
if (relativePath === '' || relativePath.startsWith('..')) {
return false;
}
// Even in windows, Ignore expects forward slashes.
const normalizedPath = relativePath.replace(/\\/g, '/');
if (normalizedPath.startsWith('/') || normalizedPath === '') {
return false;
}
return this.ig.ignores(normalizedPath);
}
getPatterns(): string[] {
return this.patterns;
}
getIgnoreFilePaths(): string[] {
return this.fileNames
.slice()
.reverse()
.map((fileName) => path.join(this.projectRoot, fileName))
.filter((filePath) => fs.existsSync(filePath));
}
/**
* Returns true if at least one ignore file exists and has patterns.
*/
hasPatterns(): boolean {
return this.patterns.length > 0;
}
}
@@ -436,7 +436,6 @@ Subdir memory
{
respectGitIgnore: true,
respectGeminiIgnore: true,
customIgnoreFilePaths: [],
},
200, // maxDirs parameter
);
@@ -473,7 +472,6 @@ My code memory
{
respectGitIgnore: true,
respectGeminiIgnore: true,
customIgnoreFilePaths: [],
},
1, // maxDirs
);
+11 -11
View File
@@ -101,33 +101,33 @@ describe('retryWithBackoff', () => {
expect(mockFn).toHaveBeenCalledTimes(3);
});
it('should default to 3 maxAttempts if no options are provided', async () => {
// This function will fail more than 3 times to ensure all retries are used.
const mockFn = createFailingFunction(5);
it('should default to 10 maxAttempts if no options are provided', async () => {
// This function will fail more than 10 times to ensure all retries are used.
const mockFn = createFailingFunction(15);
const promise = retryWithBackoff(mockFn);
await Promise.all([
expect(promise).rejects.toThrow('Simulated error attempt 3'),
expect(promise).rejects.toThrow('Simulated error attempt 10'),
vi.runAllTimersAsync(),
]);
expect(mockFn).toHaveBeenCalledTimes(3);
expect(mockFn).toHaveBeenCalledTimes(10);
});
it('should default to 3 maxAttempts if options.maxAttempts is undefined', async () => {
// This function will fail more than 3 times to ensure all retries are used.
const mockFn = createFailingFunction(5);
it('should default to 10 maxAttempts if options.maxAttempts is undefined', async () => {
// This function will fail more than 10 times to ensure all retries are used.
const mockFn = createFailingFunction(15);
const promise = retryWithBackoff(mockFn, { maxAttempts: undefined });
// Expect it to fail with the error from the 3rd attempt.
// Expect it to fail with the error from the 10th attempt.
await Promise.all([
expect(promise).rejects.toThrow('Simulated error attempt 3'),
expect(promise).rejects.toThrow('Simulated error attempt 10'),
vi.runAllTimersAsync(),
]);
expect(mockFn).toHaveBeenCalledTimes(3);
expect(mockFn).toHaveBeenCalledTimes(10);
});
it('should not retry if shouldRetry returns false', async () => {
+1 -1
View File
@@ -40,7 +40,7 @@ export interface RetryOptions {
}
const DEFAULT_RETRY_OPTIONS: RetryOptions = {
maxAttempts: 3,
maxAttempts: 10,
initialDelayMs: 5000,
maxDelayMs: 30000, // 30 seconds
shouldRetryOnError: isRetryableError,
+1 -5
View File
@@ -84,11 +84,7 @@ describe('doesToolInvocationMatch', () => {
});
describe('for non-shell tools', () => {
const mockConfig = {
getTargetDir: () => '/tmp',
getFileFilteringOptions: () => ({}),
} as unknown as Config;
const readFileTool = new ReadFileTool(mockConfig, createMockMessageBus());
const readFileTool = new ReadFileTool({} as Config, createMockMessageBus());
const invocation = {
params: { file: 'test.txt' },
} as AnyToolInvocation;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-test-utils",
"version": "0.27.0-preview.5",
"version": "0.27.0-nightly.20260121.97aac696f",
"private": true,
"main": "src/index.ts",
"license": "Apache-2.0",
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "gemini-cli-vscode-ide-companion",
"displayName": "Gemini CLI Companion",
"description": "Enable Gemini CLI with direct access to your IDE workspace.",
"version": "0.27.0-preview.5",
"version": "0.27.0-nightly.20260121.97aac696f",
"publisher": "google",
"icon": "assets/icon.png",
"repository": {
+1 -11
View File
@@ -1030,16 +1030,6 @@
"markdownDescription": "Enable fuzzy search when searching for files.\n\n- Category: `Context`\n- Requires restart: `yes`\n- Default: `true`",
"default": true,
"type": "boolean"
},
"customIgnoreFilePaths": {
"title": "Custom Ignore File Paths",
"description": "Additional ignore file paths to respect. These files take precedence over .geminiignore and .gitignore. Files earlier in the array take precedence over files later in the array, e.g. the first file takes precedence over the second one.",
"markdownDescription": "Additional ignore file paths to respect. These files take precedence over .geminiignore and .gitignore. Files earlier in the array take precedence over files later in the array, e.g. the first file takes precedence over the second one.\n\n- Category: `Context`\n- Requires restart: `yes`\n- Default: `[]`",
"default": [],
"type": "array",
"items": {
"type": "string"
}
}
},
"additionalProperties": false
@@ -1537,7 +1527,7 @@
"enabled": {
"title": "Enable Hooks",
"description": "Canonical toggle for the hooks system. When disabled, no hooks will be executed.",
"markdownDescription": "Canonical toggle for the hooks system. When disabled, no hooks will be executed.\n\n- Category: `Advanced`\n- Requires restart: `yes`\n- Default: `true`",
"markdownDescription": "Canonical toggle for the hooks system. When disabled, no hooks will be executed.\n\n- Category: `Advanced`\n- Requires restart: `no`\n- Default: `true`",
"default": true,
"type": "boolean"
},