This commit is contained in:
223
packages/agent/tests/enhanced-grep.test.ts
Normal file
223
packages/agent/tests/enhanced-grep.test.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { createSystemTools } from '../src/system-tools.js';
|
||||
import type { ToolDefinition } from '../src/tools.js';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import * as os from 'node:os';
|
||||
|
||||
describe('enhanced search_content', () => {
|
||||
let workspace: string;
|
||||
let tools: ToolDefinition[];
|
||||
let searchContent: ToolDefinition;
|
||||
|
||||
beforeEach(() => {
|
||||
workspace = fs.mkdtempSync(path.join(os.tmpdir(), 'waggle-grep-test-'));
|
||||
tools = createSystemTools(workspace);
|
||||
searchContent = tools.find((t) => t.name === 'search_content')!;
|
||||
|
||||
// Create test files
|
||||
fs.writeFileSync(
|
||||
path.join(workspace, 'app.ts'),
|
||||
'import React from "react";\nconst App = () => {\n return <div>Hello</div>;\n};\nexport default App;',
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(workspace, 'utils.ts'),
|
||||
'export function add(a: number, b: number) {\n return a + b;\n}\n\nexport function multiply(a: number, b: number) {\n return a * b;\n}',
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(workspace, 'readme.md'),
|
||||
'# Project\n\nThis is a sample project.\n\nIt does things.',
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(workspace, 'data.json'),
|
||||
'{"name": "test", "value": 42}',
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
try {
|
||||
fs.rmSync(workspace, { recursive: true, force: true });
|
||||
return;
|
||||
} catch {
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
describe('default content mode (backward compatible)', () => {
|
||||
it('returns file:line: match format', async () => {
|
||||
const result = await searchContent.execute({ pattern: 'Hello' });
|
||||
expect(result).toContain('app.ts');
|
||||
expect(result).toContain('Hello');
|
||||
});
|
||||
});
|
||||
|
||||
describe('context_before / context_after', () => {
|
||||
it('shows lines before a match', async () => {
|
||||
const result = await searchContent.execute({
|
||||
pattern: 'return a \\+ b',
|
||||
context_before: 1,
|
||||
context_after: 0,
|
||||
});
|
||||
expect(result).toContain('function add');
|
||||
expect(result).toContain('return a + b');
|
||||
});
|
||||
|
||||
it('shows lines after a match', async () => {
|
||||
const result = await searchContent.execute({
|
||||
pattern: 'const App',
|
||||
context_before: 0,
|
||||
context_after: 1,
|
||||
});
|
||||
expect(result).toContain('const App');
|
||||
expect(result).toContain('return <div>Hello</div>');
|
||||
});
|
||||
|
||||
it('shows context before and after', async () => {
|
||||
const result = await searchContent.execute({
|
||||
pattern: 'return a \\+ b',
|
||||
context_before: 1,
|
||||
context_after: 1,
|
||||
});
|
||||
expect(result).toContain('function add');
|
||||
expect(result).toContain('return a + b');
|
||||
expect(result).toContain('}');
|
||||
});
|
||||
|
||||
it('marks the matching line with >', async () => {
|
||||
const result = await searchContent.execute({
|
||||
pattern: 'return a \\+ b',
|
||||
context_before: 1,
|
||||
});
|
||||
// The matching line gets '>' marker, context lines get ' '
|
||||
expect(result).toMatch(/:\s /); // context line with space marker
|
||||
expect(result).toMatch(/:>/); // matching line with > marker
|
||||
});
|
||||
|
||||
it('uses --- separator between context groups', async () => {
|
||||
const result = await searchContent.execute({
|
||||
pattern: 'return',
|
||||
context_before: 1,
|
||||
glob: '**/*.ts',
|
||||
});
|
||||
expect(result).toContain('---');
|
||||
});
|
||||
});
|
||||
|
||||
describe('output_mode: files', () => {
|
||||
it('returns only unique file paths', async () => {
|
||||
const result = await searchContent.execute({
|
||||
pattern: 'return',
|
||||
output_mode: 'files',
|
||||
});
|
||||
// Should contain file paths, not line content
|
||||
expect(result).toContain('app.ts');
|
||||
expect(result).toContain('utils.ts');
|
||||
// Should not contain line numbers or content
|
||||
expect(result).not.toContain(':');
|
||||
});
|
||||
|
||||
it('deduplicates file paths', async () => {
|
||||
const result = await searchContent.execute({
|
||||
pattern: 'return',
|
||||
output_mode: 'files',
|
||||
glob: '**/*.ts',
|
||||
});
|
||||
const lines = result.split('\n');
|
||||
const unique = new Set(lines);
|
||||
expect(lines.length).toBe(unique.size);
|
||||
});
|
||||
});
|
||||
|
||||
describe('output_mode: count', () => {
|
||||
it('returns match counts per file', async () => {
|
||||
const result = await searchContent.execute({
|
||||
pattern: 'return',
|
||||
output_mode: 'count',
|
||||
glob: '**/*.ts',
|
||||
});
|
||||
// utils.ts has 2 returns, app.ts has 1
|
||||
expect(result).toContain('utils.ts: 2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('file_type filter', () => {
|
||||
it('filters by file extension', async () => {
|
||||
const result = await searchContent.execute({
|
||||
pattern: '.',
|
||||
output_mode: 'files',
|
||||
file_type: 'ts',
|
||||
});
|
||||
expect(result).toContain('app.ts');
|
||||
expect(result).toContain('utils.ts');
|
||||
expect(result).not.toContain('readme.md');
|
||||
expect(result).not.toContain('data.json');
|
||||
});
|
||||
|
||||
it('returns no matches for unused extension', async () => {
|
||||
const result = await searchContent.execute({
|
||||
pattern: '.',
|
||||
file_type: 'py',
|
||||
});
|
||||
expect(result).toBe('No matches found.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('max_results', () => {
|
||||
it('limits total content results', async () => {
|
||||
const result = await searchContent.execute({
|
||||
pattern: 'return',
|
||||
max_results: 1,
|
||||
glob: '**/*.ts',
|
||||
});
|
||||
// Should only have 1 match, not all 3
|
||||
const matchLines = result.split('\n').filter((l) => l.includes('return'));
|
||||
expect(matchLines.length).toBe(1);
|
||||
});
|
||||
|
||||
it('limits file results in files mode', async () => {
|
||||
const result = await searchContent.execute({
|
||||
pattern: '.',
|
||||
output_mode: 'files',
|
||||
max_results: 1,
|
||||
});
|
||||
const lines = result.split('\n').filter(Boolean);
|
||||
expect(lines.length).toBe(1);
|
||||
});
|
||||
|
||||
it('limits count results', async () => {
|
||||
const result = await searchContent.execute({
|
||||
pattern: '.',
|
||||
output_mode: 'count',
|
||||
max_results: 1,
|
||||
});
|
||||
const lines = result.split('\n').filter(Boolean);
|
||||
expect(lines.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('combined options', () => {
|
||||
it('file_type + output_mode: count', async () => {
|
||||
const result = await searchContent.execute({
|
||||
pattern: 'return',
|
||||
file_type: 'ts',
|
||||
output_mode: 'count',
|
||||
});
|
||||
expect(result).toContain('.ts');
|
||||
expect(result).not.toContain('.md');
|
||||
});
|
||||
|
||||
it('context + max_results', async () => {
|
||||
const result = await searchContent.execute({
|
||||
pattern: 'return',
|
||||
context_before: 1,
|
||||
max_results: 1,
|
||||
glob: '**/*.ts',
|
||||
});
|
||||
// Only 1 match with context
|
||||
const separators = result.split('---').length - 1;
|
||||
expect(separators).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user