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'), '\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:'); 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', ]); }); });