GPU_GUARD_MONOREPO/packages/backend/test/search_tools.test.ts
2026-05-20 21:39:12 +08:00

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