buster/packages/ai/src/tools/file-tools/grep-search-tool/grep-search-tool.int.test.ts

465 lines
15 KiB
TypeScript

import { createSandbox } from '@buster/sandbox';
import { RuntimeContext } from '@mastra/core/runtime-context';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { DocsAgentContextKeys } from '../../../agents/docs-agent/docs-agent-context';
import { grepSearch } from './grep-search-tool';
describe.sequential('grep-search-tool integration test', () => {
const hasApiKey = !!process.env.DAYTONA_API_KEY;
let sharedSandbox: any;
beforeAll(async () => {
if (hasApiKey) {
sharedSandbox = await createSandbox({
language: 'typescript',
});
}
}, 120000);
afterAll(async () => {
if (sharedSandbox) {
await sharedSandbox.delete();
}
}, 65000);
function getTestDir() {
return `test-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
(hasApiKey ? it : it.skip)(
'should execute ripgrep commands in sandbox environment',
async () => {
const testDir = getTestDir();
// First, create test files with searchable content
const createFilesCode = `
const fs = require('fs');
// Create and enter test directory
fs.mkdirSync('${testDir}', { recursive: true });
process.chdir('${testDir}');
fs.writeFileSync('test1.txt', 'Hello world\\nThis is a test file\\nGoodbye world');
fs.writeFileSync('test2.txt', 'Another test file\\nHello again\\nMore content here');
fs.mkdirSync('subdir', { recursive: true });
fs.writeFileSync('subdir/test3.txt', 'Nested file\\nHello from subdirectory\\nEnd of file');
console.log('Files created in ' + process.cwd());
`;
await sharedSandbox.process.codeRun(createFilesCode);
const runtimeContext = new RuntimeContext();
runtimeContext.set(DocsAgentContextKeys.Sandbox, sharedSandbox);
const result = await grepSearch.execute({
context: {
commands: [`cd ${testDir} && rg -n "test" test1.txt`, `cd ${testDir} && rg -n "Hello" .`],
},
runtimeContext,
});
expect(result.results).toHaveLength(2);
// Check first command results
const firstResult = result.results[0];
expect(firstResult?.success).toBe(true);
expect(firstResult?.command).toBe(`cd ${testDir} && rg -n "test" test1.txt`);
expect(firstResult?.stdout).toContain('2:This is a test file');
// Check second command results (searches all files)
const secondResult = result.results[1];
expect(secondResult?.success).toBe(true);
expect(secondResult?.command).toBe(`cd ${testDir} && rg -n "Hello" .`);
expect(secondResult?.stdout).toContain('test1.txt:1:Hello world');
expect(secondResult?.stdout).toContain('test2.txt:2:Hello again');
expect(secondResult?.stdout).toContain('test3.txt:2:Hello from subdirectory');
},
65000
);
(hasApiKey ? it : it.skip)(
'should handle case-insensitive searches',
async () => {
const testDir = getTestDir();
// Create a test file with mixed case content
const createFileCode = `
const fs = require('fs');
// Create and enter test directory
fs.mkdirSync('${testDir}', { recursive: true });
process.chdir('${testDir}');
fs.writeFileSync('case-test.txt', 'HELLO World\\nhello world\\nHeLLo WoRLd');
console.log('File created in ' + process.cwd());
`;
await sharedSandbox.process.codeRun(createFileCode);
const runtimeContext = new RuntimeContext();
runtimeContext.set(DocsAgentContextKeys.Sandbox, sharedSandbox);
const result = await grepSearch.execute({
context: {
commands: [`cd ${testDir} && rg -i -n "hello" case-test.txt`],
},
runtimeContext,
});
expect(result.results).toHaveLength(1);
const search = result.results[0];
expect(search?.success).toBe(true);
expect(search?.stdout).toContain('1:HELLO World');
expect(search?.stdout).toContain('2:hello world');
expect(search?.stdout).toContain('3:HeLLo WoRLd');
},
65000
);
(hasApiKey ? it : it.skip)(
'should handle whole word matches',
async () => {
const testDir = getTestDir();
// Create a test file
const createFileCode = `
const fs = require('fs');
// Create and enter test directory
fs.mkdirSync('${testDir}', { recursive: true });
process.chdir('${testDir}');
fs.writeFileSync('word-test.txt', 'test testing\\ntested tester\\ntest');
console.log('File created in ' + process.cwd());
`;
await sharedSandbox.process.codeRun(createFileCode);
const runtimeContext = new RuntimeContext();
runtimeContext.set(DocsAgentContextKeys.Sandbox, sharedSandbox);
const result = await grepSearch.execute({
context: {
commands: [`cd ${testDir} && rg -w -n "test" word-test.txt`],
},
runtimeContext,
});
expect(result.results).toHaveLength(1);
const search = result.results[0];
expect(search?.success).toBe(true);
expect(search?.stdout).toContain('1:test testing');
expect(search?.stdout).toContain('3:test');
expect(search?.stdout).not.toContain('tester'); // Should not match partial words
},
65000
);
(hasApiKey ? it : it.skip)(
'should handle fixed string searches (literal)',
async () => {
const testDir = getTestDir();
// Create a test file with regex special characters
const createFileCode = `
const fs = require('fs');
// Create and enter test directory
fs.mkdirSync('${testDir}', { recursive: true });
process.chdir('${testDir}');
fs.writeFileSync('regex-test.txt', 'Price: $10.99\\nPattern: test.*\\nArray: [1,2,3]');
console.log('File created in ' + process.cwd());
`;
await sharedSandbox.process.codeRun(createFileCode);
const runtimeContext = new RuntimeContext();
runtimeContext.set(DocsAgentContextKeys.Sandbox, sharedSandbox);
const result = await grepSearch.execute({
context: {
commands: [`cd ${testDir} && rg -F -n "$10.99" regex-test.txt`],
},
runtimeContext,
});
expect(result.results).toHaveLength(1);
const search = result.results[0];
expect(search?.success).toBe(true);
expect(search?.stdout).toContain('1:Price: $10.99');
},
65000
);
(hasApiKey ? it : it.skip)(
'should handle inverted matches',
async () => {
const testDir = getTestDir();
// Create a test file
const createFileCode = `
const fs = require('fs');
// Create and enter test directory
fs.mkdirSync('${testDir}', { recursive: true });
process.chdir('${testDir}');
fs.writeFileSync('invert-test.txt', 'line with test\\nline without\\nanother test line\\nno match here');
console.log('File created in ' + process.cwd());
`;
await sharedSandbox.process.codeRun(createFileCode);
const runtimeContext = new RuntimeContext();
runtimeContext.set(DocsAgentContextKeys.Sandbox, sharedSandbox);
const result = await grepSearch.execute({
context: {
commands: [`cd ${testDir} && rg -v -n "test" invert-test.txt`],
},
runtimeContext,
});
expect(result.results).toHaveLength(1);
const search = result.results[0];
expect(search?.success).toBe(true);
expect(search?.stdout).toContain('2:line without');
expect(search?.stdout).toContain('4:no match here');
},
65000
);
(hasApiKey ? it : it.skip)(
'should handle max count option',
async () => {
const testDir = getTestDir();
// Create a test file with multiple matches
const createFileCode = `
const fs = require('fs');
// Create and enter test directory
fs.mkdirSync('${testDir}', { recursive: true });
process.chdir('${testDir}');
fs.writeFileSync('many-matches.txt', 'test 1\\ntest 2\\ntest 3\\ntest 4\\ntest 5');
console.log('File created in ' + process.cwd());
`;
await sharedSandbox.process.codeRun(createFileCode);
const runtimeContext = new RuntimeContext();
runtimeContext.set(DocsAgentContextKeys.Sandbox, sharedSandbox);
const result = await grepSearch.execute({
context: {
commands: [`cd ${testDir} && rg -m 3 -n "test" many-matches.txt`],
},
runtimeContext,
});
expect(result.results).toHaveLength(1);
const search = result.results[0];
expect(search?.success).toBe(true);
const lines = search?.stdout?.trim().split('\n') || [];
expect(lines).toHaveLength(3); // Should only return 3 matches
},
65000
);
(hasApiKey ? it : it.skip)(
'should handle no matches found',
async () => {
const testDir = getTestDir();
// Create a test file with no matching content
const createFileCode = `
const fs = require('fs');
// Create and enter test directory
fs.mkdirSync('${testDir}', { recursive: true });
process.chdir('${testDir}');
fs.writeFileSync('no-match.txt', 'This file has no matches\\nNothing to find here');
console.log('File created in ' + process.cwd());
`;
await sharedSandbox.process.codeRun(createFileCode);
const runtimeContext = new RuntimeContext();
runtimeContext.set(DocsAgentContextKeys.Sandbox, sharedSandbox);
const result = await grepSearch.execute({
context: {
commands: [`cd ${testDir} && rg -n "nonexistent" no-match.txt`],
},
runtimeContext,
});
expect(result.results).toHaveLength(1);
const search = result.results[0];
expect(search?.success).toBe(true); // Exit code 1 is treated as success for rg
expect(search?.stdout).toBe('');
},
65000
);
(hasApiKey ? it : it.skip)(
'should handle multiple commands',
async () => {
const testDir = getTestDir();
// Create test files
const createFilesCode = `
const fs = require('fs');
// Create and enter test directory
fs.mkdirSync('${testDir}', { recursive: true });
process.chdir('${testDir}');
fs.writeFileSync('file1.txt', 'First file with test');
fs.writeFileSync('file2.txt', 'Second file with test');
console.log('Files created in ' + process.cwd());
`;
await sharedSandbox.process.codeRun(createFilesCode);
const runtimeContext = new RuntimeContext();
runtimeContext.set(DocsAgentContextKeys.Sandbox, sharedSandbox);
const result = await grepSearch.execute({
context: {
commands: [
`cd ${testDir} && rg -n "test" file1.txt`,
`cd ${testDir} && rg -n "test" file2.txt`,
`cd ${testDir} && rg -n "nonexistent" file1.txt`,
],
},
runtimeContext,
});
expect(result.results).toHaveLength(3);
expect(result.results[0]?.stdout).toContain('First file with test');
expect(result.results[1]?.stdout).toContain('Second file with test');
expect(result.results[2]?.stdout).toBe(''); // No match
},
65000
);
(hasApiKey ? it : it.skip)(
'should handle file not found error',
async () => {
const runtimeContext = new RuntimeContext();
runtimeContext.set(DocsAgentContextKeys.Sandbox, sharedSandbox);
const result = await grepSearch.execute({
context: {
commands: ['rg "test" /nonexistent/path/file.txt'],
},
runtimeContext,
});
expect(result.results).toHaveLength(1);
const search = result.results[0];
expect(search?.success).toBe(false);
expect(search?.error).toBeDefined();
expect(search?.stderr).toBeDefined();
},
65000
);
(hasApiKey ? it : it.skip)(
'should handle complex rg commands with multiple flags',
async () => {
const testDir = getTestDir();
// Create test files
const createFilesCode = `
const fs = require('fs');
// Create and enter test directory
fs.mkdirSync('${testDir}', { recursive: true });
process.chdir('${testDir}');
fs.mkdirSync('src', { recursive: true });
fs.writeFileSync('src/main.ts', 'TODO: implement feature\\nconsole.log("test");');
fs.writeFileSync('src/utils.ts', 'TODO: fix bug\\nexport function test() {}');
fs.writeFileSync('src/readme.md', 'TODO: update docs');
console.log('Files created in ' + process.cwd());
`;
await sharedSandbox.process.codeRun(createFilesCode);
const runtimeContext = new RuntimeContext();
runtimeContext.set(DocsAgentContextKeys.Sandbox, sharedSandbox);
const result = await grepSearch.execute({
context: {
commands: [`cd ${testDir} && rg --type ts --color never -n "TODO" src/`],
},
runtimeContext,
});
expect(result.results).toHaveLength(1);
const search = result.results[0];
expect(search?.success).toBe(true);
expect(search?.stdout).toContain('src/main.ts:1:TODO: implement feature');
expect(search?.stdout).toContain('src/utils.ts:1:TODO: fix bug');
expect(search?.stdout).not.toContain('readme.md'); // Should not match .md files
},
65000
);
(hasApiKey ? it : it.skip)(
'should handle JSON output from rg',
async () => {
const testDir = getTestDir();
// Create a test file
const createFileCode = `
const fs = require('fs');
// Create and enter test directory
fs.mkdirSync('${testDir}', { recursive: true });
process.chdir('${testDir}');
fs.writeFileSync('json-test.txt', 'Line with test\\nAnother line');
console.log('File created in ' + process.cwd());
`;
await sharedSandbox.process.codeRun(createFileCode);
const runtimeContext = new RuntimeContext();
runtimeContext.set(DocsAgentContextKeys.Sandbox, sharedSandbox);
const result = await grepSearch.execute({
context: {
commands: [`cd ${testDir} && rg --json "test" json-test.txt`],
},
runtimeContext,
});
expect(result.results).toHaveLength(1);
const search = result.results[0];
expect(search?.success).toBe(true);
// Parse JSON output
const jsonLines = search?.stdout?.trim().split('\n') || [];
expect(jsonLines.length).toBeGreaterThan(0);
const firstLine = JSON.parse(jsonLines[0] || '{}');
expect(firstLine.type).toBe('begin');
// Find the match line
const matchLine = jsonLines.find((line) => {
const parsed = JSON.parse(line);
return parsed.type === 'match';
});
expect(matchLine).toBeDefined();
},
65000
);
});