#!/usr/bin/env node import { promises as fs } from 'node:fs'; import fsSync from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { run as runRipgrep } from '../tools/ripgrep.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const repoRoot = path.resolve(__dirname, '..'); const chrootRoot = '/home/seb/src/aiTools/tmp'; const sandboxRoot = path.resolve(chrootRoot, 'rg-tests'); async function rimraf(dir) { await fs.rm(dir, { recursive: true, force: true }); } async function ensureDir(dir) { await fs.mkdir(dir, { recursive: true }); } async function writeFiles(baseDir, filesMap) { for (const [rel, content] of Object.entries(filesMap || {})) { const filePath = path.resolve(baseDir, rel); await ensureDir(path.dirname(filePath)); await fs.writeFile(filePath, content, 'utf8'); } } function slugify(name) { return name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, ''); } function expectEqual(actual, expected, label) { if (actual !== expected) { const ellipsize = (s) => (s && s.length > 400 ? s.slice(0, 400) + '…' : (s ?? '<>')); throw new Error(`${label} mismatch.\nExpected:\n${ellipsize(expected)}\nActual:\n${ellipsize(actual)}`); } } function expectRegex(actual, re, label) { if (!re.test(actual)) { throw new Error(`${label} mismatch. Expected to match ${re}, Actual: ${actual}`); } } async function runCase(index, testCase) { const idx = String(index + 1).padStart(2, '0'); const caseDir = path.resolve(sandboxRoot, `${idx}-${slugify(testCase.name)}`); await ensureDir(chrootRoot); await rimraf(caseDir); await ensureDir(caseDir); // Setup initial files await writeFiles(caseDir, typeof testCase.before === 'function' ? await testCase.before({ dir: caseDir }) : (testCase.before || {})); const args = await testCase.args({ dir: caseDir }); let threw = false; let output = ''; try { output = await runRipgrep(args); } catch (err) { threw = true; output = err?.message || String(err); } if (testCase.expect?.error) { if (!threw && typeof output === 'string') { // We expect error formatting to be returned as string starting with 'ripgrep error:' expectRegex(output, testCase.expect.error, 'Error string'); } else if (threw) { expectRegex(output, testCase.expect.error, 'Thrown error'); } } else { if (typeof testCase.expect?.equals === 'string') { expectEqual(output, testCase.expect.equals, 'Tool result'); } if (typeof testCase.expect?.lineCount === 'number') { const lines = output ? output.split('\n').filter(Boolean) : []; expectEqual(lines.length, testCase.expect.lineCount, 'Line count'); } } } function cases() { const list = []; // 1. Simple case-sensitive match list.push({ name: 'simple case-sensitive match', before: { 'a.txt': 'Hello\nWorld\nhello again' }, args: async ({ dir }) => ({ pattern: 'Hello', filePattern: path.relative(chrootRoot, path.join(dir, '**')), n_flag: true, i_flag: false }), expect: { equals: `${path.relative(chrootRoot, path.join(sandboxRoot, '01-simple-case-sensitive-match/a.txt'))}:1:Hello` } }); // 2. Case-insensitive matches list.push({ name: 'case-insensitive matches', before: { 'a.txt': 'Hello\nWorld\nhello again' }, args: async ({ dir }) => ({ pattern: 'hello', filePattern: path.relative(chrootRoot, path.join(dir, '**')), n_flag: true, i_flag: true }), expect: { equals: [ `${path.relative(chrootRoot, path.join(sandboxRoot, '02-case-insensitive-matches/a.txt'))}:1:Hello`, `${path.relative(chrootRoot, path.join(sandboxRoot, '02-case-insensitive-matches/a.txt'))}:3:hello again` ].join('\n') } }); // 3. filePattern filter to subdir and extension list.push({ name: 'filePattern filter', before: { 'sub/b.md': 'Alpha\nbeta\nGamma' }, args: async ({ dir }) => ({ pattern: 'Alpha', filePattern: path.relative(chrootRoot, path.join(dir, 'sub/*.md')), n_flag: true, i_flag: false }), expect: { equals: `${path.relative(chrootRoot, path.join(sandboxRoot, '03-filepattern-filter/sub/b.md'))}:1:Alpha` } }); // 4. No matches -> empty string list.push({ name: 'no matches returns empty', before: { 'a.txt': 'x\ny' }, args: async ({ dir }) => ({ pattern: 'nomatch', filePattern: path.relative(chrootRoot, path.join(dir, '**')), n_flag: true, i_flag: false }), expect: { equals: '' } }); // 5. Output limited to 200 lines list.push({ name: 'limit to 200 lines', before: async () => { const many = Array.from({ length: 250 }, (_, i) => `line${i + 1}`).join('\n'); return { 'long.txt': many }; }, args: async ({ dir }) => ({ pattern: 'line', filePattern: path.relative(chrootRoot, path.join(dir, 'long.txt')), n_flag: true, i_flag: false }), expect: { lineCount: 200 } }); // 6. Invalid regex pattern -> error line list.push({ name: 'invalid regex pattern', before: { 'a.txt': 'text' }, args: async ({ dir }) => ({ pattern: '[', filePattern: path.relative(chrootRoot, path.join(dir, '**')), n_flag: true, i_flag: false }), expect: { error: /ripgrep error:/ } }); // 7. No line numbers (n_flag false) list.push({ name: 'no line numbers', before: { 'a.txt': 'foo\nbar\nfoo' }, args: async ({ dir }) => ({ pattern: 'foo', filePattern: path.relative(chrootRoot, path.join(dir, '**')), n_flag: false, i_flag: false }), expect: { equals: [ `${path.relative(chrootRoot, path.join(sandboxRoot, '07-no-line-numbers/a.txt'))}:foo`, `${path.relative(chrootRoot, path.join(sandboxRoot, '07-no-line-numbers/a.txt'))}:foo` ].join('\n') } }); // 8. filePattern include-only to exclude .md (tool supports single -g, so include *.txt) list.push({ name: 'filePattern include-only excludes md', before: { 'a.txt': 'hit', 'b.md': 'hit' }, args: async ({ dir }) => ({ pattern: 'hit', filePattern: path.relative(chrootRoot, path.join(dir, '**/*.txt')), n_flag: true, i_flag: false }), expect: { equals: `${path.relative(chrootRoot, path.join(sandboxRoot, '08-filepattern-include-only-excludes-md/a.txt'))}:1:hit` } }); // 9. Empty filePattern searches all (we'll scope to the case dir by pattern and path shape) list.push({ name: 'empty filePattern searches all', before: { 'x.js': 'Hello', 'y.txt': 'Hello' }, args: async ({ dir }) => ({ pattern: 'Hello', filePattern: path.relative(chrootRoot, path.join(dir, '**')), n_flag: true, i_flag: false }), expect: { equals: [ `${path.relative(chrootRoot, path.join(sandboxRoot, '09-empty-filepattern-searches-all/x.js'))}:1:Hello`, `${path.relative(chrootRoot, path.join(sandboxRoot, '09-empty-filepattern-searches-all/y.txt'))}:1:Hello` ].join('\n') } }); // 10. Anchored regex list.push({ name: 'anchored regex', before: { 'a.txt': 'Hello\nHello world\nHello' }, args: async ({ dir }) => ({ pattern: '^Hello$', filePattern: path.relative(chrootRoot, path.join(dir, '**')), n_flag: true, i_flag: false }), expect: { equals: [ `${path.relative(chrootRoot, path.join(sandboxRoot, '10-anchored-regex/a.txt'))}:1:Hello`, `${path.relative(chrootRoot, path.join(sandboxRoot, '10-anchored-regex/a.txt'))}:3:Hello` ].join('\n') } }); // 11. Special regex characters list.push({ name: 'special regex characters', before: { 'a.txt': 'a+b?c\\d and a+b?c\\d' }, args: async ({ dir }) => ({ pattern: 'a\\+b\\?c\\\\d', filePattern: path.relative(chrootRoot, path.join(dir, '**')), n_flag: true, i_flag: false }), expect: { equals: `${path.relative(chrootRoot, path.join(sandboxRoot, '11-special-regex-characters/a.txt'))}:1:a+b?c\\d and a+b?c\\d` } }); // 12. Multiple files across dirs deterministic order list.push({ name: 'multi dirs deterministic', before: { 'b/b.txt': 'X', 'a/a.txt': 'X' }, args: async ({ dir }) => ({ pattern: '^X$', filePattern: path.relative(chrootRoot, path.join(dir, '**')), n_flag: true, i_flag: false }), expect: { equals: [ `${path.relative(chrootRoot, path.join(sandboxRoot, '12-multi-dirs-deterministic/a/a.txt'))}:1:X`, `${path.relative(chrootRoot, path.join(sandboxRoot, '12-multi-dirs-deterministic/b/b.txt'))}:1:X` ].join('\n') } }); return list; } async function main() { const all = cases(); await ensureDir(sandboxRoot); const results = []; let passed = 0; let failed = 0; for (let i = 0; i < all.length; i++) { const tc = all[i]; const label = `${String(i + 1).padStart(2, '0')} ${tc.name}`; try { await runCase(i, tc); console.log(`✓ ${label}`); results.push({ name: tc.name, ok: true }); passed++; } catch (err) { console.error(`✗ ${label}`); console.error(String(err?.stack || err)); results.push({ name: tc.name, ok: false, error: String(err?.message || err) }); failed++; } } console.log(''); console.log(`Passed: ${passed}, Failed: ${failed}, Total: ${all.length}`); if (failed > 0) process.exit(1); } main().catch((err) => { console.error('Fatal error in ripgrep test runner:', err); process.exit(1); });