| /* |
| * 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; |
| } |
| } |