From d1a275de8f893c3cd3eade90fc7ca1c8626aa4da Mon Sep 17 00:00:00 2001 From: _Kerman Date: Wed, 8 Jul 2026 18:00:44 +0800 Subject: [PATCH] fix: align v2 grep behavior with v1 --- .../src/app/agentProfileCatalog/system.md | 2 +- .../src/os/backends/node-local/tools/bash.md | 10 +-- .../src/os/backends/node-local/tools/grep.ts | 3 +- .../src/os/backends/node-local/tools/read.md | 4 +- .../os/backends/node-local/tools/rgLocator.ts | 45 ++++++---- .../agent-core-v2/test/fileTools/grep.test.ts | 85 +++++++++++++++++-- .../test/fileTools/rgLocator.test.ts | 76 ++++++++++------- 7 files changed, 163 insertions(+), 62 deletions(-) diff --git a/packages/agent-core-v2/src/app/agentProfileCatalog/system.md b/packages/agent-core-v2/src/app/agentProfileCatalog/system.md index d1102d395..1d22556c0 100644 --- a/packages/agent-core-v2/src/app/agentProfileCatalog/system.md +++ b/packages/agent-core-v2/src/app/agentProfileCatalog/system.md @@ -84,7 +84,7 @@ The current working directory is `{{ KIMI_WORK_DIR }}`. This should be considere Use this as your basic understanding of the project structure. The tree only shows the first two levels for normal directories; entries marked "... and N more" indicate additional contents. Hidden directories are shown as entries only; their contents are intentionally omitted to reduce noise. -To inspect hidden paths the tree leaves out, prefer the dedicated tools over `ls -A`. `Glob` matches dotfiles by default — use `.*` for top-level dotfiles, or anchor on a directory such as `.github/**` or `.agents/**` to walk it; avoid bare `.git/**` or `node_modules/**`, which `Glob` traverses in full and will hit its result cap. Use `Read` for a known hidden file and `Grep` to search hidden file contents. `Grep` searches hidden files by default but skips VCS metadata (`.git` and the like) and filters secrets out of its results; `Read`, `Write`, and `Edit` refuse a fixed set of well-known secret files — `.env`, SSH private keys, and a few credential files — by design; that guard does not recognize every secret format, so judge other credential-bearing files yourself. `Bash` enforces none of these path or secret guards — it runs whatever command you give it — so the same discipline is on you there: do not use shell commands (`cat`, `cp`, `curl`, and the like) to read, copy, or transmit secret files, and stay inside the working directory unless the user has explicitly directed otherwise. +To inspect hidden paths the tree leaves out, prefer the dedicated tools over `ls -A`. `Glob` matches dotfiles by default — use `.*` for top-level dotfiles, or anchor on a directory such as `.github/**` or `.agents/**` to walk it; avoid bare `node_modules/**`-style dependency walks, which can flood the result cap; `.git/**` returns nothing at all — `Glob`, like `Grep`, always skips VCS metadata. Use `Read` for a known hidden file and `Grep` to search hidden file contents. `Grep` searches hidden files by default but skips VCS metadata (`.git` and the like) and filters secrets out of its results; `Read`, `Write`, and `Edit` refuse a fixed set of well-known secret files — `.env`, SSH private keys, and a few credential files — by design; that guard does not recognize every secret format, so judge other credential-bearing files yourself. `Bash` enforces none of these path or secret guards — it runs whatever command you give it — so the same discipline is on you there: do not use shell commands (`cat`, `cp`, `curl`, and the like) to read, copy, or transmit secret files, and stay inside the working directory unless the user has explicitly directed otherwise. The directory listing of current working directory is: diff --git a/packages/agent-core-v2/src/os/backends/node-local/tools/bash.md b/packages/agent-core-v2/src/os/backends/node-local/tools/bash.md index a2afff7b4..cb237f917 100644 --- a/packages/agent-core-v2/src/os/backends/node-local/tools/bash.md +++ b/packages/agent-core-v2/src/os/backends/node-local/tools/bash.md @@ -11,19 +11,19 @@ Execute a `{{ SHELL_NAME }}` command. Use this for shell semantics — pipes, en The dedicated tools render in the per-tool permission UI and keep raw stdout out of the conversation; that is why they are worth reaching for whenever one fits. **Output:** -The stdout and stderr will be combined and returned as a string. The output may be truncated if it is too long. If the command failed, the output will end with a `Command failed with exit code: N` line stating the non-zero exit code. +The stdout and stderr will be combined and returned as a string. The output may be truncated if it is too long. If the command exits non-zero, the output ends with a `Command failed with exit code: N` line; a command killed by its timeout or interrupted by the user ends with its own message instead. -If `run_in_background=true`, the command will be started as a background task and this tool will return a task ID instead of waiting for command completion. When doing that, you must provide a short `description`. Background commands default to a {{ DEFAULT_BACKGROUND_TIMEOUT_S }}s timeout and `timeout` is capped at {{ MAX_BACKGROUND_TIMEOUT_S }}s; set `disable_timeout=true` only when the task should run without a timeout. You will be automatically notified when the task completes. Use `TaskOutput` for a non-blocking status/output snapshot, and only set `block=true` when you explicitly want to wait for completion. Use `TaskStop` only if the task must be cancelled. If a human user wants to inspect background tasks themselves, point them to the `/tasks` command, which opens an interactive panel; it has no subcommands. +If `run_in_background=true`, the command will be started as a background task and this tool will return a task ID instead of waiting for command completion. When doing that, you must provide a short `description`. Background commands default to a {{ DEFAULT_BACKGROUND_TIMEOUT_S }}s timeout and `timeout` is capped at {{ MAX_BACKGROUND_TIMEOUT_S }}s; set `disable_timeout=true` only when the task should run without a timeout. You will be automatically notified when the task completes. After starting one, default to returning control to the user instead of immediately waiting on it. Use `TaskOutput` for a non-blocking status/output snapshot, and only set `block=true` when you explicitly want to wait for completion. Use `TaskStop` only if the task must be cancelled. If a human user wants to inspect background tasks themselves, point them to the `/tasks` command, which opens an interactive panel; it has no subcommands. **Guidelines for safety and security:** -- Each shell tool call will be executed in a fresh shell environment. The shell variables, current working directory changes, and the shell history is not preserved between calls. +- Each shell tool call will be executed in a fresh shell environment. The shell variables, current working directory changes, and the shell history is not preserved between calls. To run a command in a particular directory, pass the `cwd` argument (or use absolute paths) rather than relying on a `cd` from an earlier call. - The tool call will return after the command is finished. You shall not use this tool to execute an interactive command or a command that may run forever. For possibly long-running foreground commands, set the `timeout` argument in seconds. Foreground commands default to {{ DEFAULT_TIMEOUT_S }}s and allow up to {{ MAX_TIMEOUT_S }}s. - Avoid using `..` to access files or directories outside of the working directory. - Avoid modifying files outside of the working directory unless explicitly instructed to do so. - Never run commands that require superuser privileges unless explicitly instructed to do so. **Guidelines for efficiency:** -- For multiple related commands, use `&&` to chain them in a single call, e.g. `cd /path && ls -la` +- Use `&&` to chain commands that genuinely depend on each other, e.g. `npm install && npm test`. Independent read-only commands (separate `git show`, `ls`, or status checks) should be issued as separate parallel Bash calls in one response, not chained into a single call — chaining serializes their execution and mixes their output. Do not stitch outputs together with `echo` separators. - Use `;` to run commands sequentially regardless of success/failure - Use `||` for conditional execution (run second command only if first fails) - Use pipe operations (`|`) and redirections (`>`, `>>`) to chain input and output between commands @@ -38,6 +38,6 @@ The following common command categories are usually available. Availability stil - Text and data processing: `wc`, `sort`, `uniq`, `cut`, `tr`, `diff`, `xargs` - Archives and compression: `tar`, `gzip`, `gunzip`, `zip`, `unzip` - Networking and transfer: `curl`, `wget`, `ping`, `ssh`, `scp` -- Version control: `git` +- Version control: `git`; for GitHub-hosted work (PRs, issues, CI runs, API queries) prefer the `gh` CLI when installed — it carries the user's GitHub auth and can return structured JSON - Process and system: `ps`, `kill`, `top`, `env`, `date`, `uname`, `whoami` - Language and package toolchains: `node`, `npm`, `pnpm`, `yarn`, `python`, `pip` (use whichever the project actually relies on) diff --git a/packages/agent-core-v2/src/os/backends/node-local/tools/grep.ts b/packages/agent-core-v2/src/os/backends/node-local/tools/grep.ts index 90c306a3d..761069cf7 100644 --- a/packages/agent-core-v2/src/os/backends/node-local/tools/grep.ts +++ b/packages/agent-core-v2/src/os/backends/node-local/tools/grep.ts @@ -442,7 +442,8 @@ export class GrepTool implements BuiltinTool { let mtime = 0; if (path !== undefined) { try { - mtime = (await this.fs.stat(path)).mtimeMs ?? 0; + const mtimeMs = (await this.fs.stat(path)).mtimeMs ?? 0; + mtime = Math.trunc(mtimeMs / 1000); } catch { // Keep stat failures visible; use mtime=0 so they sort after known files. } diff --git a/packages/agent-core-v2/src/os/backends/node-local/tools/read.md b/packages/agent-core-v2/src/os/backends/node-local/tools/read.md index 79bdda810..6fbaeaef0 100644 --- a/packages/agent-core-v2/src/os/backends/node-local/tools/read.md +++ b/packages/agent-core-v2/src/os/backends/node-local/tools/read.md @@ -1,13 +1,13 @@ Read a text file from the local filesystem. -If the user provides a concrete file path to a text file, call Read directly. Do not `Glob`, `ls`, or otherwise pre-check known text file paths; missing or invalid file paths return errors you can handle. Do not use Read for directories; use `ls` via Bash for a known directory, or Glob when you need files/directories matching a pattern. Use `Grep` only when the task is to search for unknown content or locations. +If the user provides a concrete file path to a text file, call Read directly. Do not `Glob`, `ls`, or otherwise pre-check known text file paths; missing or invalid file paths return errors you can handle. Do not use Read for directories; use `ls` via Bash for a known directory, or Glob when you need files matching a name pattern (Glob lists files only, never directories). Use `Grep` only when the task is to search for unknown content or locations. When you need several files, prefer to read them in parallel: emit multiple `Read` calls in a single response instead of reading one file per turn. - Relative paths resolve against the working directory; a path outside the working directory must be absolute. - Returns up to {{ MAX_LINES }} lines or {{ MAX_BYTES_KB }} KB per call, whichever comes first; lines longer than {{ MAX_LINE_LENGTH }} chars are truncated mid-line. - Page larger files with `line_offset` (1-based start line) and `n_lines`. Omit `n_lines` to read up to the {{ MAX_LINES }}-line cap. -- Sensitive files (`.env` files, credential stores, SSH keys, and similar secrets) are refused to protect secrets; do not attempt to read them. +- Sensitive files (`.env` files, credential stores, SSH private keys, and similar secrets) are refused to protect secrets; do not attempt to read them. Templates and public keys are exempt: `.env.example` / `.env.sample` / `.env.template` and public SSH keys such as `id_rsa.pub` read normally. - Only UTF-8 text files can be read. Non-UTF-8 encodings, binary files, and files containing NUL bytes are refused; use `ReadMediaFile` for images or video, and Bash or an MCP tool for other binary formats. - Negative line_offset reads from the end of the file (for example, -100 reads the last 100 lines); the absolute value cannot exceed {{ MAX_LINES }}. - Output format: `\t` per line. diff --git a/packages/agent-core-v2/src/os/backends/node-local/tools/rgLocator.ts b/packages/agent-core-v2/src/os/backends/node-local/tools/rgLocator.ts index e18bff523..40f970569 100644 --- a/packages/agent-core-v2/src/os/backends/node-local/tools/rgLocator.ts +++ b/packages/agent-core-v2/src/os/backends/node-local/tools/rgLocator.ts @@ -1,16 +1,16 @@ /** * `fileTools` domain — shared ripgrep (`rg`) binary locator. * - * Resolves the `rg` command used by Glob and Grep through a caller-supplied - * process probe, preferring the execution-environment PATH, then the vendor - * hook, then the app cache, and finally bootstrapping a pinned ripgrep - * archive into `/bin` when the caller permits it. - * Keeps callers that own a no-download fallback path opt-in-compatible. + * Resolves the `rg` command used by Glob and Grep, preferring a file found on + * PATH, then the vendor hook, then the app cache, and finally bootstrapping a + * pinned ripgrep archive into `/bin` when the + * caller permits it. File lookup intentionally avoids spawning `rg --version` + * so tool resolution has the same observable shape as v1. */ import { createHash } from 'node:crypto'; import { createWriteStream, existsSync } from 'node:fs'; -import { chmod, copyFile, mkdir, mkdtemp, readFile, rename, rm } from 'node:fs/promises'; +import { chmod, copyFile, mkdir, mkdtemp, readFile, rename, rm, stat } from 'node:fs/promises'; import { homedir, tmpdir } from 'node:os'; import { Readable } from 'node:stream'; import { pipeline } from 'node:stream/promises'; @@ -105,22 +105,20 @@ async function resolveRgPath( } export async function findExistingRg( - probe: RgProbe, + _probe: RgProbe, shareDir: string = getShareDir(), allowCachedFallback = true, ): Promise { - const system = await probe.exec(['rg', '--version']).catch(() => ({ exitCode: -1 })); - if (system.exitCode === 0) { - return { path: 'rg', source: 'system-path' }; - } + const system = await findRgOnPath(); + if (system !== undefined) return { path: system, source: 'system-path' }; if (allowCachedFallback) { const vendorPath = getVendorRgPath(rgBinaryName()); - if (vendorPath !== undefined && (await isUsableRg(probe, vendorPath))) { + if (vendorPath !== undefined && (await isExecutableFile(vendorPath))) { return { path: vendorPath, source: 'vendor' }; } const cachePath = join(shareDir, 'bin', rgBinaryName()); - if (await isUsableRg(probe, cachePath)) { + if (await isExecutableFile(cachePath)) { return { path: cachePath, source: 'share-bin-cached' }; } } @@ -148,9 +146,24 @@ function getVendorRgPath(_binName: string): string | undefined { return undefined; } -async function isUsableRg(probe: RgProbe, path: string): Promise { - const run = await probe.exec([path, '--version']).catch(() => ({ exitCode: -1 })); - return run.exitCode === 0; +async function findRgOnPath(): Promise { + const pathEnv = process.env['PATH'] ?? ''; + const sep = process.platform === 'win32' ? ';' : ':'; + const binName = rgBinaryName(); + for (const dir of pathEnv.split(sep)) { + if (dir === '') continue; + const candidate = join(dir, binName); + if (await isExecutableFile(candidate)) return candidate; + } + return undefined; +} + +async function isExecutableFile(path: string): Promise { + try { + return (await stat(path)).isFile(); + } catch { + return false; + } } export function detectTarget(): string | undefined { diff --git a/packages/agent-core-v2/test/fileTools/grep.test.ts b/packages/agent-core-v2/test/fileTools/grep.test.ts index 1c76eb157..7a8c7ac1f 100644 --- a/packages/agent-core-v2/test/fileTools/grep.test.ts +++ b/packages/agent-core-v2/test/fileTools/grep.test.ts @@ -2,20 +2,34 @@ import { Readable, type Writable } from 'node:stream'; import { afterEach, describe, expect, it, vi } from 'vitest'; +import { DisposableStore } from '#/_base/di/lifecycle'; +import { createServices } from '#/_base/di/test'; import type { ExecutableTool, ExecutableToolContext, ExecutableToolResult, ToolExecution, } from '#/agent/tool/toolContract'; -import { noopTelemetryService, type ITelemetryService } from '#/app/telemetry/telemetry'; +import { + AgentBuiltinToolsRegistrar, + IAgentBuiltinToolsRegistrar, +} from '#/agent/toolRegistry/builtinToolsRegistrar'; +import { + _clearToolContributionsForTests, + getToolContributions, + registerTool, +} from '#/agent/toolRegistry/toolContribution'; +import { IAgentToolRegistryService } from '#/agent/toolRegistry/toolRegistry'; +import { AgentToolRegistryService } from '#/agent/toolRegistry/toolRegistryService'; +import { ITelemetryService, noopTelemetryService } from '#/app/telemetry/telemetry'; import type { PathClass } from '#/_base/execEnv/environmentProbe'; import { PathSecurityError } from '#/_base/tools/policies/path-access'; import { SENSITIVE_DOT_VARIANT_SUFFIXES } from '#/_base/tools/policies/sensitive'; import type { WorkspaceConfig } from '#/_base/tools/support/workspace'; -import type { IHostEnvironment } from '#/os/interface/hostEnvironment'; -import type { HostFileStat, IHostFileSystem } from '#/os/interface/hostFileSystem'; -import type { IHostProcess, IHostProcessService } from '#/os/interface/hostProcess'; +import { IHostEnvironment } from '#/os/interface/hostEnvironment'; +import { IHostFileSystem, type HostFileStat } from '#/os/interface/hostFileSystem'; +import { IHostProcessService, type IHostProcess } from '#/os/interface/hostProcess'; +import { ISessionWorkspaceContext } from '#/session/workspaceContext/workspaceContext'; import { type GrepInput, GrepInputSchema, @@ -177,7 +191,7 @@ function statResult(mtime: number): HostFileStat { isFile: true, isDirectory: false, size: 0, - mtimeMs: mtime, + mtimeMs: mtime * 1000, }; } @@ -264,6 +278,41 @@ afterEach(() => { }); describe('GrepTool', () => { + it('registers through the production tool contribution and DI path', () => { + const savedContributions = [...getToolContributions()]; + const disposables = new DisposableStore(); + try { + _clearToolContributionsForTests(); + registerTool(ProductionGrepTool); + + const ix = createServices(disposables, { + strict: true, + additionalServices: (reg) => { + const kaos = createFakeKaos(); + reg.defineInstance(IHostProcessService, createTestProcessService(kaos)); + reg.defineInstance(IHostFileSystem, createTestFs(kaos)); + reg.defineInstance(IHostEnvironment, createTestEnv(kaos)); + reg.defineInstance(ISessionWorkspaceContext, stubWorkspaceContext('/workspace')); + reg.defineInstance(ITelemetryService, noopTelemetryService); + reg.define(IAgentToolRegistryService, AgentToolRegistryService); + reg.define(IAgentBuiltinToolsRegistrar, AgentBuiltinToolsRegistrar); + }, + }); + + ix.get(IAgentBuiltinToolsRegistrar); + const tool = ix.get(IAgentToolRegistryService).resolve('Grep'); + + expect(tool).toBeInstanceOf(ProductionGrepTool); + expect(tool?.name).toBe('Grep'); + } finally { + disposables.dispose(); + _clearToolContributionsForTests(); + for (const contribution of savedContributions) { + registerTool(contribution.ctor, contribution.options); + } + } + }); + it('exposes current metadata and schema', () => { const tool = new GrepTool(createFakeKaos(), workspace); @@ -511,6 +560,32 @@ describe('GrepTool', () => { expect(stat).toHaveBeenCalledWith('/workspace/src/new.ts'); }); + it('keeps v1 tie order for files modified within the same second', async () => { + const stdout = ['/workspace/src/a.ts', '/workspace/src/b.ts', '/workspace/src/c.ts', ''].join( + '\n', + ); + const stat = vi.fn(async (path: string) => { + if (path === '/workspace/src/a.ts') { + return { ...statResult(1), mtimeMs: 1000 }; + } + if (path === '/workspace/src/b.ts') { + return { ...statResult(1), mtimeMs: 1500 }; + } + if (path === '/workspace/src/c.ts') { + return { ...statResult(2), mtimeMs: 2000 }; + } + throw new Error(`unexpected stat: ${path}`); + }); + const tool = new GrepTool( + createFakeKaos({ exec: vi.fn().mockResolvedValue(processWithOutput(stdout)), stat }), + { workspaceDir: '/workspace', additionalDirs: [] }, + ); + + const result = await executeTool(tool, context({ pattern: 'hit', head_limit: 0 })); + + expect(toolContentString(result)).toBe(['src/c.ts', 'src/a.ts', 'src/b.ts'].join('\n')); + }); + it('limits concurrent mtime stats while sorting files_with_matches', async () => { const filePaths = Array.from( { length: 40 }, diff --git a/packages/agent-core-v2/test/fileTools/rgLocator.test.ts b/packages/agent-core-v2/test/fileTools/rgLocator.test.ts index 523005abc..3c1e277c4 100644 --- a/packages/agent-core-v2/test/fileTools/rgLocator.test.ts +++ b/packages/agent-core-v2/test/fileTools/rgLocator.test.ts @@ -57,12 +57,20 @@ function deferred(): { describe('findExistingRg', () => { let fakeShare: string; + let savedPath: string | undefined; beforeEach(() => { fakeShare = join(tmpdir(), `kimi-rg-${String(Date.now())}-${String(Math.random()).slice(2)}`); mkdirSync(join(fakeShare, 'bin'), { recursive: true }); + savedPath = process.env['PATH']; + process.env['PATH'] = ''; }); afterEach(() => { rmSync(fakeShare, { recursive: true, force: true }); + if (savedPath === undefined) { + delete process.env['PATH']; + } else { + process.env['PATH'] = savedPath; + } }); it('returns undefined when no rg anywhere', async () => { @@ -72,18 +80,27 @@ describe('findExistingRg', () => { it('resolves from share-dir when cached', async () => { const cached = join(fakeShare, 'bin', process.platform === 'win32' ? 'rg.exe' : 'rg'); - const probe = probeWith((args) => (args[0] === cached ? 0 : -1)); + writeFileSync(cached, 'fake rg'); + const probe = noRgProbe(); const result = await findExistingRg(probe, fakeShare); expect(result).toEqual({ path: cached, source: 'share-bin-cached' }); + expect(probe.exec).not.toHaveBeenCalled(); }); it('prefers system PATH over share-dir when both are available', async () => { - const probe = probeWith((args) => (args[0] === 'rg' ? 0 : -1)); + const binDir = join(fakeShare, 'path-bin'); + mkdirSync(binDir, { recursive: true }); + const systemRg = join(binDir, process.platform === 'win32' ? 'rg.exe' : 'rg'); + const cached = join(fakeShare, 'bin', process.platform === 'win32' ? 'rg.exe' : 'rg'); + writeFileSync(systemRg, 'fake system rg'); + writeFileSync(cached, 'fake cached rg'); + process.env['PATH'] = binDir; + const probe = noRgProbe(); const result = await findExistingRg(probe, fakeShare); - expect(result).toEqual({ path: 'rg', source: 'system-path' }); - expect(probe.exec).toHaveBeenCalledTimes(1); + expect(result).toEqual({ path: systemRg, source: 'system-path' }); + expect(probe.exec).not.toHaveBeenCalled(); }); }); @@ -181,6 +198,7 @@ describe('verifyArchiveChecksum', () => { describe('ensureRgPath download branch', () => { let fakeShare: string; let savedFetch: typeof globalThis.fetch | undefined; + let savedPath: string | undefined; beforeEach(() => { fakeShare = join( tmpdir(), @@ -188,6 +206,8 @@ describe('ensureRgPath download branch', () => { ); mkdirSync(join(fakeShare, 'bin'), { recursive: true }); savedFetch = globalThis.fetch; + savedPath = process.env['PATH']; + process.env['PATH'] = ''; }); afterEach(() => { rmSync(fakeShare, { recursive: true, force: true }); @@ -196,6 +216,11 @@ describe('ensureRgPath download branch', () => { } else { globalThis.fetch = savedFetch; } + if (savedPath === undefined) { + delete process.env['PATH']; + } else { + process.env['PATH'] = savedPath; + } vi.restoreAllMocks(); }); @@ -231,36 +256,15 @@ describe('ensureRgPath download branch', () => { expect(fetchMock).not.toHaveBeenCalled(); }); - it('does not start bootstrap work when aborted after lookup misses', async () => { - const controller = new AbortController(); - const fetchMock = vi.fn(() => new Promise(() => {})); - globalThis.fetch = fetchMock as unknown as typeof fetch; - const cacheProbe = deferred<{ readonly exitCode: number }>(); - const probe: RgProbe & { exec: ReturnType } = { - exec: vi - .fn() - .mockResolvedValueOnce({ exitCode: -1 }) - .mockReturnValueOnce(cacheProbe.promise), - }; + it('does not run probe subprocesses while lookup misses', async () => { + globalThis.fetch = vi.fn().mockRejectedValue(new Error('network unreachable')) as typeof fetch; + const probe = noRgProbe(); - const resultPromise = ensureRgPath(probe, { - shareDir: fakeShare, - signal: controller.signal, - allowCachedFallback: true, - }); + await expect( + ensureRgPath(probe, { shareDir: fakeShare, allowCachedFallback: true }), + ).rejects.toThrow(/network unreachable/); - await vi.waitFor(() => { - expect(probe.exec).toHaveBeenCalledTimes(2); - }); - controller.abort(); - await expect(resultPromise).rejects.toHaveProperty('name', 'AbortError'); - - cacheProbe.resolve({ exitCode: -1 }); - await new Promise((resolve) => { - setTimeout(resolve, 20); - }); - - expect(fetchMock).not.toHaveBeenCalled(); + expect(probe.exec).not.toHaveBeenCalled(); }); it('aborts the current caller wait while shared bootstrap work continues', async () => { @@ -376,6 +380,7 @@ describe('ensureRgPath Windows download branch', () => { let savedFetch: typeof globalThis.fetch | undefined; let savedArch: string; let savedPlatform: string; + let savedPath: string | undefined; beforeEach(() => { fakeShare = join( tmpdir(), @@ -383,6 +388,8 @@ describe('ensureRgPath Windows download branch', () => { ); mkdirSync(join(fakeShare, 'bin'), { recursive: true }); savedFetch = globalThis.fetch; + savedPath = process.env['PATH']; + process.env['PATH'] = ''; savedArch = process.arch; savedPlatform = process.platform; Object.defineProperty(process, 'arch', { value: 'x64' }); @@ -395,6 +402,11 @@ describe('ensureRgPath Windows download branch', () => { } else { globalThis.fetch = savedFetch; } + if (savedPath === undefined) { + delete process.env['PATH']; + } else { + process.env['PATH'] = savedPath; + } Object.defineProperty(process, 'arch', { value: savedArch }); Object.defineProperty(process, 'platform', { value: savedPlatform }); vi.restoreAllMocks();