mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-10 01:39:25 +00:00
fix: align v2 grep behavior with v1
This commit is contained in:
parent
ca237a0264
commit
d1a275de8f
7 changed files with 163 additions and 62 deletions
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -442,7 +442,8 @@ export class GrepTool implements BuiltinTool<GrepInput> {
|
|||
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.
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: `<line-number>\t<content>` per line.
|
||||
|
|
|
|||
|
|
@ -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 `<KIMI_CODE_HOME|~/.kimi-code>/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 `<KIMI_CODE_HOME|~/.kimi-code>/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<RgResolution | undefined> {
|
||||
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<boolean> {
|
||||
const run = await probe.exec([path, '--version']).catch(() => ({ exitCode: -1 }));
|
||||
return run.exitCode === 0;
|
||||
async function findRgOnPath(): Promise<string | undefined> {
|
||||
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<boolean> {
|
||||
try {
|
||||
return (await stat(path)).isFile();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function detectTarget(): string | undefined {
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -57,12 +57,20 @@ function deferred<T>(): {
|
|||
|
||||
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<Response>(() => {}));
|
||||
globalThis.fetch = fetchMock as unknown as typeof fetch;
|
||||
const cacheProbe = deferred<{ readonly exitCode: number }>();
|
||||
const probe: RgProbe & { exec: ReturnType<typeof vi.fn> } = {
|
||||
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();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue