blob: 5b55ea7f5e38b761c29718a951e133e40f521887 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { describe, test } from 'node:test';
import assert from 'node:assert/strict';
import { chmod, mkdir, mkdtemp, readFile, rm, truncate, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { LocalWorkspaceExecutor } from '../workspace-executor.js';
import { createBoundaryFilesystemExecutor } from '../filesystem-executor.js';
const ONE_PIXEL_IMAGES = [
[
'image.PNG',
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==',
'image/png',
],
[
'image.jpg',
'/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAP//////////////////////////////////////////////////////////////////////////////////////2wBDAf//////////////////////////////////////////////////////////////////////////////////////wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAf/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIQAxAAAAF//8QAFBABAAAAAAAAAAAAAAAAAAAAAP/aAAgBAQABBQJ//8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAgBAwEBPwF//8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAgBAgEBPwF//8QAFBABAAAAAAAAAAAAAAAAAAAAAP/aAAgBAQAGPwJ//8QAFBABAAAAAAAAAAAAAAAAAAAAAP/aAAgBAQABPyF//9k=',
'image/jpeg',
],
['image.jpeg', 'R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==', 'image/gif'],
['image.webp', 'UklGRiIAAABXRUJQVlA4IBYAAAAwAQCdASoBAAEAAUAmJaQAA3AA/vuUAAA=', 'image/webp'],
] as const;
const ONE_PIXEL_PNG = Buffer.from(ONE_PIXEL_IMAGES[0][1], 'base64');
describe('LocalWorkspaceExecutor exec', () => {
test('runs commands in the provided cwd and streams stdout/stderr', async () => {
const cwd = await mkdtemp(join(tmpdir(), 'maka-workspace-exec-'));
await writeFile(join(cwd, 'marker.txt'), 'from-cwd', 'utf8');
const executor = new LocalWorkspaceExecutor();
const events: Array<{ stream: 'stdout' | 'stderr'; chunk: string }> = [];
const result = await executor.exec({
command: 'printf "$(cat marker.txt)"; printf "err-data" >&2',
cwd,
timeoutMs: 5_000,
emitOutput: (stream, chunk) => events.push({ stream, chunk }),
});
assert.partialDeepStrictEqual(result, {
exitCode: 0,
stdout: 'from-cwd',
stderr: 'err-data',
});
assert.strictEqual(
events.some((event) => event.stream === 'stdout' && event.chunk.includes('from-cwd')),
true,
);
assert.strictEqual(
events.some((event) => event.stream === 'stderr' && event.chunk.includes('err-data')),
true,
);
});
test('reports non-zero exit without throwing so tools can preserve their own error contract', async () => {
const cwd = await mkdtemp(join(tmpdir(), 'maka-workspace-exec-'));
const executor = new LocalWorkspaceExecutor();
const result = await executor.exec({
command: 'printf "out-data"; printf "err-data" >&2; exit 7',
cwd,
timeoutMs: 5_000,
});
assert.partialDeepStrictEqual(result, {
exitCode: 7,
stdout: 'out-data',
stderr: 'err-data',
});
assert.strictEqual(result.timedOut, false);
assert.strictEqual(result.aborted, false);
});
test('runs argv commands without routing through the host shell', async () => {
const cwd = await mkdtemp(join(tmpdir(), 'maka-workspace-exec-argv-'));
const executor = new LocalWorkspaceExecutor();
const result = await executor.exec({
command: 'ignored display command',
argv: [
process.execPath,
'-e',
'process.stdout.write(process.argv[1])',
'literal $HOME && ok',
],
cwd,
timeoutMs: 5_000,
});
assert.partialDeepStrictEqual(result, {
exitCode: 0,
stdout: 'literal $HOME && ok',
stderr: '',
});
});
test('reports timeout with captured output', async () => {
const cwd = await mkdtemp(join(tmpdir(), 'maka-workspace-exec-'));
const executor = new LocalWorkspaceExecutor();
const result = await executor.exec({
command: 'printf "before-timeout"; sleep 5',
cwd,
timeoutMs: 200,
});
assert.strictEqual(result.exitCode, 124);
assert.strictEqual(result.timedOut, true);
assert.strictEqual(result.stdout, 'before-timeout');
});
test('reports abort with captured output', async () => {
const cwd = await mkdtemp(join(tmpdir(), 'maka-workspace-exec-'));
const executor = new LocalWorkspaceExecutor();
const controller = new AbortController();
const resultPromise = executor.exec({
command: 'printf "before-abort"; sleep 5; printf "after-abort"',
cwd,
timeoutMs: 5_000,
abortSignal: controller.signal,
});
setTimeout(() => controller.abort(), 100);
const result = await resultPromise;
assert.strictEqual(result.exitCode, 130);
assert.strictEqual(result.aborted, true);
assert.strictEqual(result.timedOut, false);
assert.strictEqual(result.stdout, 'before-abort');
});
});
describe('LocalWorkspaceExecutor file operations', () => {
test('reads valid PNG, JPEG, GIF, and WebP images by magic bytes', async () => {
const cwd = await mkdtemp(join(tmpdir(), 'maka-workspace-images-'));
const executor = new LocalWorkspaceExecutor();
for (const [name, base64, mimeType] of ONE_PIXEL_IMAGES) {
const file = join(cwd, name);
const bytes = Buffer.from(base64, 'base64');
await writeFile(file, bytes);
const result = await executor.readFile({ cwd, path: file });
if (!('bytes' in result)) throw new Error('expected image result');
assert.strictEqual(result.mimeType, mimeType);
assert.deepStrictEqual([...result.bytes], [...bytes]);
}
});
test('ignores text windows when reading an image', async () => {
const cwd = await mkdtemp(join(tmpdir(), 'maka-workspace-images-'));
const executor = new LocalWorkspaceExecutor();
const file = join(cwd, 'image.png');
await writeFile(file, ONE_PIXEL_PNG);
const result = await executor.readFile({ cwd, path: file, offset: 1, limit: 1 });
if (!('bytes' in result)) throw new Error('expected image result');
assert.deepStrictEqual([...result.bytes], [...ONE_PIXEL_PNG]);
});
test('rejects extension-only and over-limit image files', async () => {
const cwd = await mkdtemp(join(tmpdir(), 'maka-workspace-images-'));
const executor = new LocalWorkspaceExecutor();
const fake = join(cwd, 'fake.png');
const huge = join(cwd, 'huge.webp');
await writeFile(fake, 'not an image');
await writeFile(huge, 'RIFF0000WEBP');
await truncate(huge, 5 * 1024 * 1024 + 1);
await assert.rejects(
executor.readFile({ cwd, path: fake }),
/^Error: Image content is not a supported PNG, JPEG, GIF, or WebP file\.$/,
);
await assert.rejects(
executor.readFile({ cwd, path: huge }),
/exceeds the 5MB model input limit/,
);
});
test('reads and writes text files by absolute path', async () => {
const cwd = await mkdtemp(join(tmpdir(), 'maka-workspace-files-'));
const executor = new LocalWorkspaceExecutor();
const file = join(cwd, 'data.txt');
const writeResult = await executor.writeFile({ cwd, path: file, content: 'hello' });
const readResult = await executor.readFile({ cwd, path: file });
assert.partialDeepStrictEqual(writeResult, {
ok: true,
path: file,
bytes: 5,
});
assert.partialDeepStrictEqual(readResult, { content: 'hello' });
assert.strictEqual(await readFile(file, 'utf8'), 'hello');
});
test('applies read offset and limit at the executor boundary', async () => {
const cwd = await mkdtemp(join(tmpdir(), 'maka-workspace-files-'));
const executor = new LocalWorkspaceExecutor();
const file = join(cwd, 'data.txt');
await writeFile(file, 'line1\nline2\nline3\nline4', 'utf8');
const readResult = await executor.readFile({ cwd, path: file, offset: 1, limit: 2 });
assert.partialDeepStrictEqual(readResult, { content: 'line2\nline3' });
});
test('globs files from the provided cwd with a result cap', async () => {
const cwd = await mkdtemp(join(tmpdir(), 'maka-workspace-glob-'));
await mkdir(join(cwd, 'src'), { recursive: true });
await writeFile(join(cwd, 'src', 'a.ts'), 'a', 'utf8');
await writeFile(join(cwd, 'src', 'b.ts'), 'b', 'utf8');
await writeFile(join(cwd, 'src', 'c.js'), 'c', 'utf8');
const executor = new LocalWorkspaceExecutor();
const result = await executor.globFiles({ cwd, pattern: 'src/*.*', limit: 2 });
assert.equal(result.files.length, 2);
assert.equal(new Set(result.files).size, 2);
assert.ok(result.files.every((file) => ['src/a.ts', 'src/b.ts', 'src/c.js'].includes(file)));
});
test('greps file contents with rg-compatible no-match behavior', async () => {
const cwd = await mkdtemp(join(tmpdir(), 'maka-workspace-grep-'));
await mkdir(join(cwd, 'src'), { recursive: true });
await writeFile(join(cwd, 'src', 'main.ts'), 'export const token = 1; // --flag\n', 'utf8');
const executor = new LocalWorkspaceExecutor();
const hit = await executor.grepFiles({
cwd,
pattern: 'token',
path: join(cwd, 'src'),
maxCountPerFile: 50,
limit: 200,
timeoutMs: 5_000,
});
const miss = await executor.grepFiles({
cwd,
pattern: 'absent',
path: join(cwd, 'src'),
maxCountPerFile: 50,
limit: 200,
timeoutMs: 5_000,
});
const optionLikePattern = await executor.grepFiles({
cwd,
pattern: '--flag',
path: join(cwd, 'src'),
maxCountPerFile: 50,
limit: 200,
timeoutMs: 5_000,
});
assert.deepStrictEqual(hit.matches, [
`${join(cwd, 'src', 'main.ts')}:1:export const token = 1; // --flag`,
]);
assert.deepStrictEqual((miss as { matches: string[] }).matches, []);
assert.deepStrictEqual(optionLikePattern.matches, [
`${join(cwd, 'src', 'main.ts')}:1:export const token = 1; // --flag`,
]);
});
test('reports a missing ripgrep as grep_unavailable with an install hint (#5167)', async () => {
const cwd = await mkdtemp(join(tmpdir(), 'maka-workspace-grep-no-rg-'));
const emptyBin = await mkdtemp(join(tmpdir(), 'maka-workspace-grep-empty-path-'));
const executor = new LocalWorkspaceExecutor({ rgCandidates: [] });
await withPath(emptyBin, () =>
assert.rejects(
executor.grepFiles({
cwd,
pattern: 'token',
path: cwd,
maxCountPerFile: 50,
limit: 200,
timeoutMs: 5_000,
}),
(error: NodeJS.ErrnoException) => {
assert.equal(error.code, 'grep_unavailable');
assert.match(error.message, /ripgrep/);
assert.match(error.message, /BurntSushi\/ripgrep/);
assert.match(error.message, /then retry/);
return true;
},
),
);
});
test('keeps a missing working directory distinct from a missing ripgrep', async () => {
// Node reports a missing spawn cwd exactly like a missing executable
// (`spawn rg ENOENT`), so the command name alone cannot tell them apart.
const parent = await mkdtemp(join(tmpdir(), 'maka-workspace-grep-gone-cwd-'));
const cwd = join(parent, 'deleted');
await mkdir(cwd);
await writeFile(join(parent, 'kept.ts'), 'token', 'utf8');
await rm(cwd, { recursive: true });
const executor = new LocalWorkspaceExecutor();
await assert.rejects(
executor.grepFiles({
cwd,
pattern: 'token',
path: parent,
maxCountPerFile: 50,
limit: 200,
timeoutMs: 5_000,
}),
(error: NodeJS.ErrnoException) => {
assert.equal(error.code, 'ENOENT');
assert.notEqual(error.name, 'RipgrepUnavailableError');
return true;
},
);
});
test('a bypass Grep finds ripgrep installed after Host startup outside its inherited PATH', {
skip: process.platform === 'win32' ? 'POSIX executable fixture' : false,
}, async () => {
const cwd = await mkdtemp(join(tmpdir(), 'maka-workspace-bypass-grep-'));
const emptyBin = await mkdtemp(join(tmpdir(), 'maka-workspace-bypass-old-path-'));
const localAppData = await mkdtemp(join(tmpdir(), 'maka-workspace-bypass-local-app-data-'));
try {
const executable = join(localAppData, 'Microsoft', 'WinGet', 'Links', 'rg.exe');
const filesystem = createBoundaryFilesystemExecutor({
workspace: new LocalWorkspaceExecutor({
platform: 'win32',
hostEnv: { PATH: emptyBin, LOCALAPPDATA: localAppData },
}),
});
const request = {
operation: {
kind: 'grep' as const,
pattern: 'token',
path: cwd,
maxCountPerFile: 50,
limit: 200,
timeoutMs: 5_000,
},
cwd,
executionBoundary: { kind: 'bypass' as const, revision: 1 },
};
await withPath(emptyBin, async () => {
await assert.rejects(filesystem.execute(request), { code: 'grep_unavailable' });
await mkdir(join(localAppData, 'Microsoft', 'WinGet', 'Links'), { recursive: true });
await writeFile(
executable,
`#!/bin/sh\nprintf '%s\\n' '{"type":"summary","data":{"stats":{"matched_lines":0}}}'\n`,
'utf8',
);
await chmod(executable, 0o755);
assert.deepEqual(await filesystem.execute(request), {
kind: 'grep',
matches: [],
matchedLines: 0,
returnedLines: 0,
omittedLines: 0,
truncated: false,
});
});
} finally {
await Promise.all(
[cwd, emptyBin, localAppData].map((path) => rm(path, { recursive: true, force: true })),
);
}
});
test('leaves other spawn failures, such as a non-executable rg, untouched', {
skip: process.platform === 'win32' ? 'POSIX execute permissions' : false,
}, async () => {
const cwd = await mkdtemp(join(tmpdir(), 'maka-workspace-grep-eacces-'));
const bin = await mkdtemp(join(tmpdir(), 'maka-workspace-grep-noexec-bin-'));
await writeFile(join(bin, 'rg'), '#!/bin/sh\n', 'utf8');
await chmod(join(bin, 'rg'), 0o644);
const executor = new LocalWorkspaceExecutor({ rgCandidates: [join(bin, 'rg')] });
await withPath(bin, () =>
assert.rejects(
executor.grepFiles({
cwd,
pattern: 'token',
path: cwd,
maxCountPerFile: 50,
limit: 200,
timeoutMs: 5_000,
}),
{ code: 'EACCES' },
),
);
});
});
async function withPath<T>(path: string, run: () => Promise<T>): Promise<T> {
const original = process.env.PATH;
process.env.PATH = path;
try {
return await run();
} finally {
if (original === undefined) delete process.env.PATH;
else process.env.PATH = original;
}
}