161 lines
5.1 KiB
TypeScript
161 lines
5.1 KiB
TypeScript
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
import { findFilesTool, grepContentLocally, grepTool } from '../src/modules/netaclaw/tools/builtin/search.js';
|
|
|
|
describe('search tools', () => {
|
|
const tempDirs: string[] = [];
|
|
|
|
afterEach(() => {
|
|
tempDirs.splice(0).forEach(dir => fs.rmSync(dir, { recursive: true, force: true }));
|
|
});
|
|
|
|
const createFixture = () => {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'neta-search-tools-'));
|
|
tempDirs.push(root);
|
|
fs.mkdirSync(path.join(root, 'src'), { recursive: true });
|
|
fs.mkdirSync(path.join(root, 'node_modules', 'ignored'), { recursive: true });
|
|
fs.writeFileSync(path.join(root, 'src', 'alpha.ts'), 'export const targetValue = 1;\n', 'utf8');
|
|
fs.writeFileSync(path.join(root, 'src', 'beta.vue'), '<template>targetValue</template>\n', 'utf8');
|
|
fs.writeFileSync(path.join(root, '.hidden.ts'), 'const hiddenValue = true;\n', 'utf8');
|
|
fs.writeFileSync(path.join(root, 'node_modules', 'ignored', 'skip.ts'), 'targetValue\n', 'utf8');
|
|
return root;
|
|
};
|
|
|
|
it('finds files by glob while skipping dependency directories', async () => {
|
|
const root = createFixture();
|
|
|
|
const result = await findFilesTool.execute('find-1', {
|
|
path: root,
|
|
pattern: '**/*.ts',
|
|
limit: 10,
|
|
});
|
|
|
|
expect((result as any).text).toContain('src/alpha.ts');
|
|
expect((result as any).text).not.toContain('node_modules');
|
|
});
|
|
|
|
it('finds files from runtime session cwd when path is omitted', async () => {
|
|
const root = createFixture();
|
|
|
|
const result = await findFilesTool.execute('find-1', {
|
|
pattern: '**/*.vue',
|
|
limit: 10,
|
|
_netaRuntime: { sessionCwd: root },
|
|
} as any);
|
|
|
|
expect((result as any).text).toContain('src/beta.vue');
|
|
});
|
|
|
|
it('greps file contents with optional glob filtering', async () => {
|
|
const root = createFixture();
|
|
|
|
const result = await grepTool.execute('grep-1', {
|
|
path: root,
|
|
pattern: 'targetValue',
|
|
glob: '**/*.vue',
|
|
literal: true,
|
|
limit: 10,
|
|
});
|
|
|
|
expect((result as any).text).toContain('src/beta.vue:1:<template>targetValue</template>');
|
|
expect((result as any).text).not.toContain('alpha.ts');
|
|
});
|
|
|
|
it('greps file contents with context lines', async () => {
|
|
const root = createFixture();
|
|
fs.writeFileSync(
|
|
path.join(root, 'src', 'context.ts'),
|
|
['before line', 'const targetValue = 2;', 'after line'].join('\n'),
|
|
'utf8'
|
|
);
|
|
|
|
const result = await grepTool.execute('grep-context', {
|
|
path: root,
|
|
pattern: 'targetValue = 2',
|
|
glob: '**/*.ts',
|
|
literal: true,
|
|
context: 1,
|
|
limit: 10,
|
|
});
|
|
|
|
expect((result as any).text).toContain('src/context.ts-1-before line');
|
|
expect((result as any).text).toContain('src/context.ts:2:const targetValue = 2;');
|
|
expect((result as any).text).toContain('src/context.ts-3-after line');
|
|
});
|
|
|
|
it('greps from runtime session cwd when path is omitted', async () => {
|
|
const root = createFixture();
|
|
|
|
const result = await grepTool.execute('grep-cwd', {
|
|
pattern: 'targetValue',
|
|
literal: true,
|
|
limit: 10,
|
|
_netaRuntime: { sessionCwd: root },
|
|
} as any);
|
|
|
|
expect((result as any).text).toContain('src/alpha.ts:1:export const targetValue = 1;');
|
|
});
|
|
|
|
it('stops grep output at the match limit', async () => {
|
|
const root = createFixture();
|
|
fs.writeFileSync(
|
|
path.join(root, 'src', 'many.ts'),
|
|
Array.from({ length: 8 }, (_, index) => `targetValue ${index + 1}`).join('\n'),
|
|
'utf8'
|
|
);
|
|
|
|
const result = await grepTool.execute('grep-limit', {
|
|
path: path.join(root, 'src', 'many.ts'),
|
|
pattern: 'targetValue',
|
|
literal: true,
|
|
limit: 2,
|
|
});
|
|
const text = (result as any).text as string;
|
|
|
|
expect(text).toContain('many.ts:1:targetValue 1');
|
|
expect(text).toContain('many.ts:2:targetValue 2');
|
|
expect(text).not.toContain('many.ts:3:targetValue 3');
|
|
expect(text).toContain('2');
|
|
expect(text).toContain('limit');
|
|
});
|
|
|
|
it('includes hidden files in fallback traversal to match Pi hidden search behavior', async () => {
|
|
const root = createFixture();
|
|
|
|
const result = await grepTool.execute('grep-hidden', {
|
|
path: path.join(root, '.hidden.ts'),
|
|
pattern: 'hiddenValue',
|
|
literal: true,
|
|
limit: 10,
|
|
});
|
|
|
|
expect((result as any).text).toContain('.hidden.ts:1:const hiddenValue = true;');
|
|
});
|
|
|
|
it('counts grep limit by matches instead of rendered context lines in local fallback', async () => {
|
|
const root = createFixture();
|
|
const filePath = path.join(root, 'src', 'multi.ts');
|
|
fs.writeFileSync(
|
|
filePath,
|
|
['before one', 'targetValue first', 'after one', 'before two', 'targetValue second', 'after two'].join('\n'),
|
|
'utf8'
|
|
);
|
|
|
|
const result = await grepContentLocally({
|
|
files: [filePath],
|
|
root,
|
|
context: 1,
|
|
limit: 1,
|
|
pattern: line => line.includes('targetValue'),
|
|
});
|
|
|
|
expect(result.matchLimitReached).toBe(true);
|
|
expect(result.outputLines).toEqual([
|
|
'src/multi.ts-1-before one',
|
|
'src/multi.ts:2:targetValue first',
|
|
'src/multi.ts-3-after one',
|
|
]);
|
|
});
|
|
});
|