fix(agent-core-v2): restore glob rg bootstrap

This commit is contained in:
_Kerman 2026-07-08 15:17:33 +08:00
parent 69df799810
commit 9dd8ab35a0
6 changed files with 739 additions and 62 deletions

View file

@ -80,6 +80,7 @@
"retry": "0.13.1",
"smol-toml": "^1.6.1",
"socks": "^2.8.9",
"tar": "^7.5.13",
"ulid": "^3.0.1",
"undici": "^7.27.1",
"yauzl": "^3.3.0",
@ -95,6 +96,7 @@
"@types/react-dom": "^19.1.2",
"@types/retry": "0.12.0",
"@types/sinon": "^21.0.1",
"@types/tar": "^7.0.87",
"@types/yauzl": "^2.10.3",
"@types/yazl": "^2.4.6",
"@vitejs/plugin-react": "^4.4.1",

View file

@ -1,6 +1,6 @@
Find files by glob pattern, sorted by modification time (most recent first).
Powered by ripgrep. Respects `.gitignore`, `.ignore`, and `.rgignore` by default — set `include_ignored` to also match ignored files (e.g. build outputs, `node_modules`). Sensitive files (such as `.env`) are always filtered out.
Powered by ripgrep. Respects `.gitignore`, `.ignore`, and `.rgignore` by default — set `include_ignored` to also match ignored files (e.g. build outputs, `node_modules`). Sensitive files (such as `.env`) are always filtered out. Matches are files only — directories themselves are never listed; to find a directory, glob for a file inside it (e.g. `**/fixtures/**`).
Good patterns:
- `*.ts` — all files matching an extension, at any depth below the search root (a bare pattern without `/` matches recursively)

View file

@ -82,13 +82,13 @@ export const GlobInputSchema = z.object({
.string()
.optional()
.describe(
'Absolute path to the directory to search in. Defaults to the current working directory.',
'Directory to search. Accepts an absolute path, or a path relative to the current working directory. Defaults to the current working directory.',
),
include_ignored: z
.boolean()
.optional()
.describe(
'Also match files excluded by ignore files such as `.gitignore`, `.ignore`, and `.rgignore` (for example `node_modules` or build outputs). Sensitive files (such as `.env`) remain filtered out for safety. Defaults to false.',
'Also match files excluded by ignore files such as `.gitignore`, `.ignore`, and `.rgignore` (for example `node_modules` or build outputs). Sensitive files (such as `.env`) remain filtered out for safety. VCS metadata directories (`.git` and similar) are always skipped, even when this is true. Defaults to false.',
),
include_dirs: z
.boolean()

View file

@ -1,61 +1,62 @@
/**
* `fileTools` domain shared ripgrep (`rg`) binary locator.
*
* Single place that decides which `rg` the Glob and Grep paths run. The lookup
* mirrors v1's `ensureRgPath` intent (bundled-or-system, graceful degradation)
* but is driven through a caller-supplied {@link RgProbe} so it works against
* whatever execution environment the caller has Glob/Grep probe through the
* host `IHostProcessService`. Both run `rg --version` and treat exit code 0 as
* "available".
*
* Lookup order (first hit wins):
* 1. System `rg` on the execution-environment PATH (`rg --version`).
* 2. Persistent cache at `<KIMI_CODE_HOME|~/.kimi-code>/bin/rg` where a
* previously bootstrapped or manually dropped static binary lives. Only
* attempted when `allowCachedFallback` is set (Glob); Grep keeps its own
* pure-node fallback and opts out so its "rg missing → node fallback"
* path stays deterministic.
*
* If nothing resolves, {@link ensureRgPath} throws and callers surface
* {@link rgUnavailableMessage} instead of a naked `spawn rg ENOENT`.
*
* Ported from `session/sessionFs/rgLocator` onto the os tools: the probe is now
* backed by `IHostProcessService` instead of the session process runner.
* 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.
*/
import { homedir } from 'node:os';
import { join } from 'node:path';
import { createHash } from 'node:crypto';
import { createWriteStream, existsSync } from 'node:fs';
import { chmod, copyFile, mkdir, mkdtemp, readFile, rename, rm } from 'node:fs/promises';
import { homedir, tmpdir } from 'node:os';
import { Readable } from 'node:stream';
import { pipeline } from 'node:stream/promises';
/** Where the resolved `rg` came from. Used for fallback telemetry. */
export type RgResolutionSource = 'system-path' | 'share-bin-cached';
import { extract as extractTar } from 'tar';
import { type Entry, fromBuffer as yauzlFromBuffer } from 'yauzl';
import { basename, join } from 'pathe';
import { abortable } from '#/_base/utils/abort';
const RG_VERSION = '15.0.0';
const RG_BASE_URL = 'https://code.kimi.com/kimi-code/rg';
const DOWNLOAD_TIMEOUT_MS = 600_000;
const RG_ARCHIVE_SHA256: Record<string, string> = {
'ripgrep-15.0.0-aarch64-apple-darwin.tar.gz':
'98bb2e61e7277ba0ea72d2ae2592497fd8d2940934a16b122448d302a6637e3b',
'ripgrep-15.0.0-aarch64-pc-windows-msvc.zip':
'572709c8770cb7f9385d725cb06d2bcd9537ec24d4dd17b1be1d65a876f8b591',
'ripgrep-15.0.0-aarch64-unknown-linux-gnu.tar.gz':
'15f8cc2fab12d88491c54d49f38589922a9d6a7353c29b0a0856727bcdf80754',
'ripgrep-15.0.0-x86_64-apple-darwin.tar.gz':
'44128c733d127ddbda461e01225a68b5f9997cfe7635242a797f645ca674a71a',
'ripgrep-15.0.0-x86_64-pc-windows-msvc.zip':
'21a98bf42c4da97ca543c010e764cc6dec8b9b7538d05f8d21874016385e0860',
'ripgrep-15.0.0-x86_64-unknown-linux-musl.tar.gz':
'253ad0fd5fef0d64cba56c70dccdacc1916d4ed70ad057cc525fcdb0c3bbd2a7',
};
export type RgResolutionSource =
| 'system-path'
| 'vendor'
| 'share-bin-cached'
| 'share-bin-downloaded';
export interface RgResolution {
/** Command or absolute path to pass as argv[0] when spawning `rg`. */
readonly path: string;
readonly source: RgResolutionSource;
}
/**
* Minimal probe surface the locator runs against. Lets the same locator run
* over Glob's and Grep's `IHostProcessService` without depending on it
* directly.
*/
export interface RgProbe {
/** Run `argv` and resolve with the process exit code. */
exec(args: readonly string[]): Promise<{ readonly exitCode: number }>;
}
export interface EnsureRgPathOptions {
/**
* Cancels this caller's wait. Checked between probe steps; an aborted signal
* makes {@link ensureRgPath} throw an `AbortError`.
*/
readonly signal?: AbortSignal;
/**
* When true, fall back to the cached binary at `<share>/bin/rg` if `rg` is
* not on PATH. Defaults to false so callers with their own fallback (Grep's
* node walker) keep deterministic behavior.
*/
readonly shareDir?: string | undefined;
readonly signal?: AbortSignal | undefined;
readonly allowCachedFallback?: boolean;
}
@ -69,7 +70,6 @@ function getShareDir(): string {
return join(homedir(), '.kimi-code');
}
/** Absolute path of the cached `rg` binary, if one has been installed. */
export function getShareBinRgPath(): string {
return join(getShareDir(), 'bin', rgBinaryName());
}
@ -80,44 +80,245 @@ function throwIfAborted(signal: AbortSignal | undefined): void {
}
}
/**
* Resolve a usable `rg`. Probes `rg --version` through `probe`; on a non-zero
* exit (and only when `allowCachedFallback` is set) tries the cached binary
* before giving up. Throws when no working `rg` can be found.
*/
export async function ensureRgPath(
probe: RgProbe,
options: EnsureRgPathOptions = {},
): Promise<RgResolution> {
throwIfAborted(options.signal);
const shareDir = options.shareDir ?? getShareDir();
const resolution = resolveRgPath(probe, shareDir, options);
return options.signal === undefined ? resolution : abortable(resolution, options.signal);
}
async function resolveRgPath(
probe: RgProbe,
shareDir: string,
options: EnsureRgPathOptions,
): Promise<RgResolution> {
const existing = await findExistingRg(probe, shareDir, options.allowCachedFallback === true);
if (existing) return existing;
throwIfAborted(options.signal);
if (options.allowCachedFallback === true) {
return downloadRgWithLock(probe, shareDir);
}
throw new Error('ripgrep (rg) is not available on PATH');
}
export async function findExistingRg(
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' };
}
if (options.allowCachedFallback === true) {
throwIfAborted(options.signal);
const cached = getShareBinRgPath();
const cachedRun = await probe.exec([cached, '--version']).catch(() => ({ exitCode: -1 }));
if (cachedRun.exitCode === 0) {
return { path: cached, source: 'share-bin-cached' };
if (allowCachedFallback) {
const vendorPath = getVendorRgPath(rgBinaryName());
if (vendorPath !== undefined && (await isUsableRg(probe, vendorPath))) {
return { path: vendorPath, source: 'vendor' };
}
const cachePath = join(shareDir, 'bin', rgBinaryName());
if (await isUsableRg(probe, cachePath)) {
return { path: cachePath, source: 'share-bin-cached' };
}
}
throw new Error('ripgrep (rg) is not available on PATH');
return undefined;
}
let downloadPromise: Promise<RgResolution> | undefined;
async function downloadRgWithLock(probe: RgProbe, shareDir: string): Promise<RgResolution> {
if (downloadPromise !== undefined) return downloadPromise;
downloadPromise = (async () => {
try {
const existing = await findExistingRg(probe, shareDir, true);
if (existing) return existing;
const binPath = await downloadAndInstallRg(shareDir);
return { path: binPath, source: 'share-bin-downloaded' };
} finally {
downloadPromise = undefined;
}
})();
return downloadPromise;
}
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;
}
export function detectTarget(): string | undefined {
const arch = process.arch === 'x64' ? 'x86_64' : process.arch === 'arm64' ? 'aarch64' : undefined;
if (arch === undefined) return undefined;
if (process.platform === 'darwin') return `${arch}-apple-darwin`;
if (process.platform === 'linux') {
return arch === 'x86_64' ? 'x86_64-unknown-linux-musl' : 'aarch64-unknown-linux-gnu';
}
if (process.platform === 'win32') return `${arch}-pc-windows-msvc`;
return undefined;
}
async function downloadAndInstallRg(shareDir: string): Promise<string> {
const target = detectTarget();
if (target === undefined) {
throw new Error(
`Unsupported platform/arch for ripgrep download: ${process.platform}/${process.arch}`,
);
}
const isWindows = target.includes('windows');
const archiveExt = isWindows ? 'zip' : 'tar.gz';
const archiveName = `ripgrep-${RG_VERSION}-${target}.${archiveExt}`;
const expectedSha256 = RG_ARCHIVE_SHA256[archiveName];
if (expectedSha256 === undefined) {
throw new Error(`No pinned SHA-256 is configured for ripgrep archive ${archiveName}`);
}
const url = `${RG_BASE_URL}/${archiveName}`;
const binDir = join(shareDir, 'bin');
await mkdir(binDir, { recursive: true });
const destination = join(binDir, rgBinaryName());
const tmp = await mkdtemp(join(tmpdir(), 'kimi-rg-'));
try {
const archivePath = join(tmp, archiveName);
const controller = new AbortController();
const timeoutHandle = setTimeout(() => {
controller.abort();
}, DOWNLOAD_TIMEOUT_MS);
let resp: Response;
try {
resp = await fetch(url, { signal: controller.signal });
} finally {
clearTimeout(timeoutHandle);
}
if (!resp.ok || resp.body === null) {
throw new Error(`Failed to download ripgrep: HTTP ${String(resp.status)} ${resp.statusText}`);
}
const write = createWriteStream(archivePath);
await pipeline(Readable.fromWeb(resp.body as never), write);
await verifyArchiveChecksum(archivePath, archiveName, expectedSha256);
if (isWindows) {
await extractRgFromZip(archivePath, destination);
} else {
const extractDir = join(tmp, 'extract');
await mkdir(extractDir, { recursive: true });
await extractTar({
file: archivePath,
cwd: extractDir,
gzip: true,
filter: (entryPath: string) => entryPath.endsWith(`/${rgBinaryName()}`),
});
const extracted = join(extractDir, `ripgrep-${RG_VERSION}-${target}`, rgBinaryName());
if (!existsSync(extracted)) {
throw new Error(
`Ripgrep archive did not contain expected binary at ${extracted}. ` +
'CDN content may have changed.',
);
}
const installDir = await mkdtemp(join(binDir, '.rg-install-'));
const staged = join(installDir, rgBinaryName());
try {
await copyFile(extracted, staged);
await chmod(staged, 0o755);
await rename(staged, destination);
} finally {
await rm(installDir, { recursive: true, force: true });
}
}
return destination;
} finally {
await rm(tmp, { recursive: true, force: true });
}
}
export async function verifyArchiveChecksum(
archivePath: string,
archiveName: string,
expectedSha256: string,
): Promise<void> {
const actualSha256 = createHash('sha256')
.update(await readFile(archivePath))
.digest('hex');
if (actualSha256 !== expectedSha256) {
throw new Error(
`Ripgrep archive checksum mismatch for ${archiveName}: expected ${expectedSha256}, ` +
`got ${actualSha256}. CDN content may have changed.`,
);
}
}
export async function extractRgFromZip(archivePath: string, destination: string): Promise<void> {
const buf = await readFile(archivePath);
const binName = rgBinaryName();
await new Promise<void>((resolve, reject) => {
yauzlFromBuffer(buf, { lazyEntries: true }, (openErr, zipfile) => {
if (openErr !== null || zipfile === undefined) {
reject(new Error(`Failed to open ripgrep archive: ${openErr?.message ?? 'unknown error'}`));
return;
}
let found = false;
const onEntry = (entry: Entry): void => {
if (basename(entry.fileName) !== binName) {
zipfile.readEntry();
return;
}
found = true;
zipfile.openReadStream(entry, (streamErr, stream) => {
if (streamErr !== null) {
reject(
new Error(`Failed to read ${entry.fileName} from archive: ${streamErr.message}`),
);
zipfile.close();
return;
}
const out = createWriteStream(destination);
void (async () => {
try {
await pipeline(stream, out);
zipfile.close();
resolve();
} catch (error) {
zipfile.close();
reject(error instanceof Error ? error : new Error(String(error)));
}
})();
});
};
zipfile.on('entry', onEntry);
zipfile.on('end', () => {
if (!found) {
reject(
new Error(
`Ripgrep archive did not contain expected binary '${binName}'. ` +
'CDN content may have changed.',
),
);
}
});
zipfile.on('error', (err: Error) => {
reject(err);
});
zipfile.readEntry();
});
});
}
/**
* User-facing message when {@link ensureRgPath} throws. Kept in one place so
* the Glob / Grep plumbing surfaces the same actionable hint.
*/
export function rgUnavailableMessage(cause: unknown): string {
const detail =
cause instanceof Error ? cause.message : typeof cause === 'string' ? cause : 'unknown error';
const shareBin = getShareBinRgPath();
return (
`ripgrep (rg) is not available.\n` +
`ripgrep (rg) is not available and the automatic bootstrap failed.\n` +
`\n` +
`Error: ${detail}\n` +
`\n` +

View file

@ -0,0 +1,468 @@
/**
* Covers: rg-locator (ripgrep hybrid binary resolution).
*
* Pure-lookup pins (no real CDN download):
* - `findExistingRg` returns undefined when PATH + share-bin are both empty
* - Resolves from `<shareDir>/bin/rg` when that binary exists
* - Prefers system PATH over share-dir cache when both are available
* - `rgUnavailableMessage` surfaces the underlying cause + install hints
*/
import { createHash } from 'node:crypto';
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { extract as extractTar } from 'tar';
import { ZipFile } from 'yazl';
import { join } from 'pathe';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
detectTarget,
ensureRgPath,
extractRgFromZip,
findExistingRg,
rgUnavailableMessage,
verifyArchiveChecksum,
type RgProbe,
} from '#/os/backends/node-local/tools/rgLocator';
vi.mock('tar', () => ({ extract: vi.fn() }));
function probeWith(
resolveExitCode: (args: readonly string[]) => number,
): RgProbe & { exec: ReturnType<typeof vi.fn> } {
return {
exec: vi.fn(async (args: readonly string[]) => ({ exitCode: resolveExitCode(args) })),
};
}
function noRgProbe(): RgProbe & { exec: ReturnType<typeof vi.fn> } {
return probeWith(() => -1);
}
function deferred<T>(): {
readonly promise: Promise<T>;
readonly resolve: (value: T) => void;
readonly reject: (error: unknown) => void;
} {
let resolve!: (value: T) => void;
let reject!: (error: unknown) => void;
const promise = new Promise<T>((res, rej) => {
resolve = res;
reject = rej;
});
return { promise, resolve, reject };
}
describe('findExistingRg', () => {
let fakeShare: string;
beforeEach(() => {
fakeShare = join(tmpdir(), `kimi-rg-${String(Date.now())}-${String(Math.random()).slice(2)}`);
mkdirSync(join(fakeShare, 'bin'), { recursive: true });
});
afterEach(() => {
rmSync(fakeShare, { recursive: true, force: true });
});
it('returns undefined when no rg anywhere', async () => {
const result = await findExistingRg(noRgProbe(), fakeShare);
expect(result).toBeUndefined();
});
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));
const result = await findExistingRg(probe, fakeShare);
expect(result).toEqual({ path: cached, source: 'share-bin-cached' });
});
it('prefers system PATH over share-dir when both are available', async () => {
const probe = probeWith((args) => (args[0] === 'rg' ? 0 : -1));
const result = await findExistingRg(probe, fakeShare);
expect(result).toEqual({ path: 'rg', source: 'system-path' });
expect(probe.exec).toHaveBeenCalledTimes(1);
});
});
describe('detectTarget', () => {
let savedArch: string;
let savedPlatform: string;
beforeEach(() => {
savedArch = process.arch;
savedPlatform = process.platform;
});
afterEach(() => {
Object.defineProperty(process, 'arch', { value: savedArch });
Object.defineProperty(process, 'platform', { value: savedPlatform });
});
function setPlatform(arch: string, platform: string): void {
Object.defineProperty(process, 'arch', { value: arch });
Object.defineProperty(process, 'platform', { value: platform });
}
it('darwin arm64 -> aarch64-apple-darwin', () => {
setPlatform('arm64', 'darwin');
expect(detectTarget()).toBe('aarch64-apple-darwin');
});
it('darwin x64 -> x86_64-apple-darwin', () => {
setPlatform('x64', 'darwin');
expect(detectTarget()).toBe('x86_64-apple-darwin');
});
it('linux x64 -> x86_64-unknown-linux-musl', () => {
setPlatform('x64', 'linux');
expect(detectTarget()).toBe('x86_64-unknown-linux-musl');
});
it('linux arm64 -> aarch64-unknown-linux-gnu', () => {
setPlatform('arm64', 'linux');
expect(detectTarget()).toBe('aarch64-unknown-linux-gnu');
});
it('win32 x64 -> x86_64-pc-windows-msvc', () => {
setPlatform('x64', 'win32');
expect(detectTarget()).toBe('x86_64-pc-windows-msvc');
});
it('unsupported arch -> undefined', () => {
setPlatform('mips', 'linux');
expect(detectTarget()).toBeUndefined();
});
});
describe('rgUnavailableMessage', () => {
it('surfaces the underlying cause and install hints', () => {
const msg = rgUnavailableMessage(new Error('fetch failed'));
expect(msg).toContain('automatic bootstrap failed');
expect(msg).toContain('fetch failed');
expect(msg).toContain('brew install ripgrep');
expect(msg).toContain('https://github.com/BurntSushi/ripgrep');
});
it('handles non-Error causes (string, unknown)', () => {
const a = rgUnavailableMessage('boom');
expect(a).toContain('boom');
const b = rgUnavailableMessage(42);
expect(b).toContain('unknown error');
});
});
describe('verifyArchiveChecksum', () => {
let fakeDir: string;
beforeEach(() => {
fakeDir = join(tmpdir(), `kimi-rg-sha-${String(Date.now())}-${String(Math.random()).slice(2)}`);
mkdirSync(fakeDir, { recursive: true });
});
afterEach(() => {
rmSync(fakeDir, { recursive: true, force: true });
});
it('accepts a file whose SHA-256 matches the expected digest', async () => {
const archivePath = join(fakeDir, 'archive.tar.gz');
const payload = Buffer.from('trusted archive bytes', 'utf8');
writeFileSync(archivePath, payload);
const expectedSha256 = createHash('sha256').update(payload).digest('hex');
await expect(
verifyArchiveChecksum(archivePath, 'archive.tar.gz', expectedSha256),
).resolves.toBeUndefined();
});
it('rejects a file whose SHA-256 differs from the expected digest', async () => {
const archivePath = join(fakeDir, 'archive.tar.gz');
writeFileSync(archivePath, 'tampered archive bytes');
await expect(
verifyArchiveChecksum(archivePath, 'archive.tar.gz', '0'.repeat(64)),
).rejects.toThrow(/checksum mismatch/);
});
});
describe('ensureRgPath download branch', () => {
let fakeShare: string;
let savedFetch: typeof globalThis.fetch | undefined;
beforeEach(() => {
fakeShare = join(
tmpdir(),
`kimi-rg-dl-${String(Date.now())}-${String(Math.random()).slice(2)}`,
);
mkdirSync(join(fakeShare, 'bin'), { recursive: true });
savedFetch = globalThis.fetch;
});
afterEach(() => {
rmSync(fakeShare, { recursive: true, force: true });
if (savedFetch === undefined) {
delete (globalThis as unknown as { fetch?: typeof fetch }).fetch;
} else {
globalThis.fetch = savedFetch;
}
vi.restoreAllMocks();
});
it('does not bootstrap when allowCachedFallback is false', async () => {
const fetchMock = vi.fn();
globalThis.fetch = fetchMock as unknown as typeof fetch;
await expect(ensureRgPath(noRgProbe(), { shareDir: fakeShare })).rejects.toThrow(/on PATH/);
expect(fetchMock).not.toHaveBeenCalled();
});
it('surfaces a network error when fetch rejects', async () => {
globalThis.fetch = vi.fn().mockRejectedValue(new Error('network unreachable')) as typeof fetch;
await expect(
ensureRgPath(noRgProbe(), { shareDir: fakeShare, allowCachedFallback: true }),
).rejects.toThrow(/network unreachable/);
});
it('does not start bootstrap work when the caller is already aborted', async () => {
const controller = new AbortController();
controller.abort();
const fetchMock = vi.fn();
globalThis.fetch = fetchMock as unknown as typeof fetch;
await expect(
ensureRgPath(noRgProbe(), {
shareDir: fakeShare,
signal: controller.signal,
allowCachedFallback: true,
}),
).rejects.toHaveProperty('name', 'AbortError');
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),
};
const resultPromise = ensureRgPath(probe, {
shareDir: fakeShare,
signal: controller.signal,
allowCachedFallback: true,
});
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();
});
it('aborts the current caller wait while shared bootstrap work continues', async () => {
const controller = new AbortController();
const fetchResponse = deferred<{
readonly ok: false;
readonly status: number;
readonly statusText: string;
readonly body: null;
}>();
globalThis.fetch = vi.fn(() => fetchResponse.promise) as unknown as typeof fetch;
const resultPromise = ensureRgPath(noRgProbe(), {
shareDir: fakeShare,
signal: controller.signal,
allowCachedFallback: true,
});
await vi.waitFor(() => {
expect(globalThis.fetch).toHaveBeenCalledTimes(1);
});
controller.abort();
await expect(resultPromise).rejects.toHaveProperty('name', 'AbortError');
fetchResponse.resolve({ ok: false, status: 499, statusText: 'Client Closed', body: null });
await expect(
ensureRgPath(noRgProbe(), { shareDir: fakeShare, allowCachedFallback: true }),
).rejects.toThrow(/HTTP 499 Client Closed/);
});
it('surfaces HTTP failure (non-2xx response) with status + statusText', async () => {
globalThis.fetch = vi.fn().mockResolvedValue({
ok: false,
status: 404,
statusText: 'Not Found',
body: null,
}) as unknown as typeof fetch;
await expect(
ensureRgPath(noRgProbe(), { shareDir: fakeShare, allowCachedFallback: true }),
).rejects.toThrow(/HTTP 404 Not Found/);
});
it('fetches ripgrep over HTTPS', async () => {
const body = bodyFromBuffer(Buffer.from('not a real archive', 'utf8'));
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
status: 200,
statusText: 'OK',
body,
});
globalThis.fetch = fetchMock as unknown as typeof fetch;
await expect(
ensureRgPath(noRgProbe(), { shareDir: fakeShare, allowCachedFallback: true }),
).rejects.toThrow();
const [url] = fetchMock.mock.calls[0] as [string];
expect(new URL(url).protocol).toBe('https:');
});
it('rejects archives that do not match the pinned SHA-256 before extraction', async () => {
const tarMock = vi.mocked(extractTar);
tarMock.mockClear();
const body = bodyFromBuffer(Buffer.from('tampered archive', 'utf8'));
globalThis.fetch = vi.fn().mockResolvedValue({
ok: true,
status: 200,
statusText: 'OK',
body,
}) as unknown as typeof fetch;
await expect(
ensureRgPath(noRgProbe(), { shareDir: fakeShare, allowCachedFallback: true }),
).rejects.toThrow(/checksum/i);
expect(tarMock).not.toHaveBeenCalled();
expect(existsSync(join(fakeShare, 'bin', process.platform === 'win32' ? 'rg.exe' : 'rg'))).toBe(
false,
);
});
});
function buildFixtureZip(entries: Array<{ name: string; content: Buffer }>): Promise<Buffer> {
return new Promise((resolve, reject) => {
const zip = new ZipFile();
for (const { name, content } of entries) {
zip.addBuffer(content, name);
}
zip.end();
const chunks: Buffer[] = [];
zip.outputStream.on('data', (c: Buffer) => {
chunks.push(c);
});
zip.outputStream.on('end', () => {
resolve(Buffer.concat(chunks));
});
zip.outputStream.on('error', reject);
});
}
function bodyFromBuffer(buf: Buffer): ReadableStream<Uint8Array> {
return new ReadableStream<Uint8Array>({
start(controller) {
controller.enqueue(new Uint8Array(buf));
controller.close();
},
});
}
describe('ensureRgPath Windows download branch', () => {
let fakeShare: string;
let savedFetch: typeof globalThis.fetch | undefined;
let savedArch: string;
let savedPlatform: string;
beforeEach(() => {
fakeShare = join(
tmpdir(),
`kimi-rg-win-${String(Date.now())}-${String(Math.random()).slice(2)}`,
);
mkdirSync(join(fakeShare, 'bin'), { recursive: true });
savedFetch = globalThis.fetch;
savedArch = process.arch;
savedPlatform = process.platform;
Object.defineProperty(process, 'arch', { value: 'x64' });
Object.defineProperty(process, 'platform', { value: 'win32' });
});
afterEach(() => {
rmSync(fakeShare, { recursive: true, force: true });
if (savedFetch === undefined) {
delete (globalThis as unknown as { fetch?: typeof fetch }).fetch;
} else {
globalThis.fetch = savedFetch;
}
Object.defineProperty(process, 'arch', { value: savedArch });
Object.defineProperty(process, 'platform', { value: savedPlatform });
vi.restoreAllMocks();
});
it('fetches the .zip URL (not .tar.gz) on Windows target', async () => {
const zipBuf = await buildFixtureZip([
{
name: 'ripgrep-15.0.0-x86_64-pc-windows-msvc/rg.exe',
content: Buffer.from('MZfake-pe-bytes', 'utf8'),
},
]);
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
status: 200,
statusText: 'OK',
body: bodyFromBuffer(zipBuf),
});
globalThis.fetch = fetchMock as unknown as typeof fetch;
await expect(
ensureRgPath(noRgProbe(), { shareDir: fakeShare, allowCachedFallback: true }),
).rejects.toThrow(/checksum mismatch/);
const [url] = fetchMock.mock.calls[0] as [string];
expect(url).toMatch(/ripgrep-15\.0\.0-x86_64-pc-windows-msvc\.zip$/);
});
it('extracts rg.exe into <shareDir>/bin/rg.exe', async () => {
const payload = Buffer.from('MZfake-pe-bytes-extracted', 'utf8');
const zipBuf = await buildFixtureZip([
{
name: 'ripgrep-15.0.0-x86_64-pc-windows-msvc/rg.exe',
content: payload,
},
]);
const archivePath = join(fakeShare, 'fixture.zip');
const installed = join(fakeShare, 'bin', 'rg.exe');
writeFileSync(archivePath, zipBuf);
await extractRgFromZip(archivePath, installed);
expect(existsSync(installed)).toBe(true);
expect(readFileSync(installed)).toEqual(payload);
});
it('throws with "CDN content may have changed" when the zip omits rg.exe', async () => {
const zipBuf = await buildFixtureZip([{ name: 'README.md', content: Buffer.from('readme') }]);
const archivePath = join(fakeShare, 'fixture.zip');
const installed = join(fakeShare, 'bin', 'rg.exe');
writeFileSync(archivePath, zipBuf);
await expect(extractRgFromZip(archivePath, installed)).rejects.toThrow(
/CDN content may have changed/,
);
});
it('surfaces HTTP failure on Windows with status + statusText', async () => {
globalThis.fetch = vi.fn().mockResolvedValue({
ok: false,
status: 502,
statusText: 'Bad Gateway',
body: null,
}) as unknown as typeof fetch;
await expect(
ensureRgPath(noRgProbe(), { shareDir: fakeShare, allowCachedFallback: true }),
).rejects.toThrow(/HTTP 502 Bad Gateway/);
});
});

6
pnpm-lock.yaml generated
View file

@ -547,6 +547,9 @@ importers:
socks:
specifier: ^2.8.9
version: 2.8.9
tar:
specifier: ^7.5.13
version: 7.5.13
ulid:
specifier: ^3.0.1
version: 3.0.2
@ -587,6 +590,9 @@ importers:
'@types/sinon':
specifier: ^21.0.1
version: 21.0.1
'@types/tar':
specifier: ^7.0.87
version: 7.0.87
'@types/yauzl':
specifier: ^2.10.3
version: 2.10.3