Enhance testing framework by introducing a unified test runner in run-all.js, allowing for streamlined execution of multiple test scripts. Update package.json to include new test commands for individual test scripts and the consolidated runner. Add comprehensive test cases for list_files, read_file, and ripgrep functionalities, improving overall test coverage and error handling.
This commit is contained in:
@@ -49,7 +49,11 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node cli.js",
|
"start": "node cli.js",
|
||||||
"test": "node tests/run-tests.js"
|
"test": "node tests/run-all.js",
|
||||||
|
"test:patch": "node tests/run-tests.js",
|
||||||
|
"test:readfile": "node tests/run-readfile-tests.js",
|
||||||
|
"test:listfiles": "node tests/run-listfiles-tests.js",
|
||||||
|
"test:ripgrep": "node tests/run-ripgrep-tests.js"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
|
|||||||
32
tests/run-all.js
Normal file
32
tests/run-all.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import { spawn } from 'node:child_process';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
const tests = [
|
||||||
|
'tests/run-tests.js',
|
||||||
|
'tests/run-readfile-tests.js',
|
||||||
|
'tests/run-listfiles-tests.js',
|
||||||
|
'tests/run-ripgrep-tests.js',
|
||||||
|
];
|
||||||
|
|
||||||
|
function runOne(scriptPath) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const abs = path.resolve(process.cwd(), scriptPath);
|
||||||
|
const child = spawn(process.execPath, [abs], { stdio: 'inherit' });
|
||||||
|
child.on('close', (code) => resolve({ script: scriptPath, code }));
|
||||||
|
child.on('error', (err) => resolve({ script: scriptPath, code: 1, error: err }));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let anyFailed = false;
|
||||||
|
for (const t of tests) {
|
||||||
|
const res = await runOne(t);
|
||||||
|
if (res.code !== 0) anyFailed = true;
|
||||||
|
}
|
||||||
|
process.exit(anyFailed ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => { console.error('Fatal error in run-all:', err); process.exit(1); });
|
||||||
|
|
||||||
|
|
||||||
160
tests/run-listfiles-tests.js
Normal file
160
tests/run-listfiles-tests.js
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import { promises as fs } from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { run as runListFiles } from '../tools/list_files.js';
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const chrootRoot = '/home/seb/src/aiTools/tmp';
|
||||||
|
const sandboxRoot = path.resolve(chrootRoot, 'listfiles-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) {
|
||||||
|
const toStr = (v) => typeof v === 'string' ? v : JSON.stringify(v);
|
||||||
|
if (toStr(actual) !== toStr(expected)) {
|
||||||
|
const ellipsize = (s) => (s && s.length > 400 ? s.slice(0, 400) + '…' : (s ?? '<<undefined>>'));
|
||||||
|
throw new Error(`${label} mismatch.\nExpected:\n${ellipsize(toStr(expected))}\nActual:\n${ellipsize(toStr(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);
|
||||||
|
|
||||||
|
await writeFiles(caseDir, typeof testCase.before === 'function' ? await testCase.before({ dir: caseDir }) : (testCase.before || {}));
|
||||||
|
|
||||||
|
const args = await testCase.args({ dir: caseDir });
|
||||||
|
const result = await runListFiles(args);
|
||||||
|
|
||||||
|
if (testCase.expect?.errorRegex) {
|
||||||
|
if (!testCase.expect.errorRegex.test(result.err || '')) {
|
||||||
|
throw new Error(`Error regex mismatch. Got: ${result.err}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Expect cwd and files
|
||||||
|
let expectedCwd;
|
||||||
|
if (testCase.expect.cwdFromArgs === true) {
|
||||||
|
expectedCwd = (args.path === '' || args.path === '/') ? '.' : args.path;
|
||||||
|
} else if (testCase.expect.cwdFromArgs === 'file') {
|
||||||
|
expectedCwd = path.dirname(args.path || '.') || '.';
|
||||||
|
} else {
|
||||||
|
expectedCwd = testCase.expect.cwd;
|
||||||
|
}
|
||||||
|
expectEqual(result.cwd, expectedCwd, 'cwd');
|
||||||
|
expectEqual(result.files, JSON.stringify(testCase.expect.files), 'files');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cases() {
|
||||||
|
const list = [];
|
||||||
|
|
||||||
|
// 1. List empty dir depth 0
|
||||||
|
list.push({
|
||||||
|
name: 'list empty dir depth 0',
|
||||||
|
before: {},
|
||||||
|
args: async ({ dir }) => ({ path: path.relative(chrootRoot, dir) || '/', depth: 0, includeHidden: false }),
|
||||||
|
expect: { cwdFromArgs: true, files: [] }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. List single file
|
||||||
|
list.push({
|
||||||
|
name: 'list single file',
|
||||||
|
before: { 'a.txt': 'A' },
|
||||||
|
args: async ({ dir }) => ({ path: path.relative(chrootRoot, path.join(dir, 'a.txt')), depth: 1, includeHidden: false }),
|
||||||
|
expect: { cwdFromArgs: 'file', files: [['a.txt', 'f', 1]] }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Directory with nested structure depth 1
|
||||||
|
list.push({
|
||||||
|
name: 'nested depth 1',
|
||||||
|
before: { 'sub/x.txt': 'X', 'sub/inner/y.txt': 'Y', 'z.txt': 'Z' },
|
||||||
|
args: async ({ dir }) => ({ path: path.relative(chrootRoot, dir) || '/', depth: 1, includeHidden: false }),
|
||||||
|
expect: { cwdFromArgs: true, files: [['sub', 'd', null], ['sub/inner', 'd', null], ['sub/x.txt', 'f', 1], ['z.txt', 'f', 1]] }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. Depth unlimited (-1)
|
||||||
|
list.push({
|
||||||
|
name: 'depth unlimited',
|
||||||
|
before: { 'sub/x.txt': 'X', 'sub/inner/y.txt': 'Y', 'z.txt': 'Z' },
|
||||||
|
args: async ({ dir }) => ({ path: path.relative(chrootRoot, dir) || '/', depth: -1, includeHidden: false }),
|
||||||
|
expect: { cwdFromArgs: true, files: [
|
||||||
|
['sub', 'd', null],
|
||||||
|
['sub/inner', 'd', null],
|
||||||
|
['sub/inner/y.txt', 'f', 1],
|
||||||
|
['sub/x.txt', 'f', 1],
|
||||||
|
['z.txt', 'f', 1],
|
||||||
|
] }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 5. Include hidden
|
||||||
|
list.push({
|
||||||
|
name: 'include hidden',
|
||||||
|
before: { '.hidden': 'h', 'v.txt': 'v' },
|
||||||
|
args: async ({ dir }) => ({ path: path.relative(chrootRoot, dir) || '/', depth: 1, includeHidden: true }),
|
||||||
|
expect: { cwdFromArgs: true, files: [['.hidden', 'f', 1], ['v.txt', 'f', 1]] }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 6. Non-existent path -> error
|
||||||
|
list.push({
|
||||||
|
name: 'nonexistent path error',
|
||||||
|
before: {},
|
||||||
|
args: async ({ dir }) => ({ path: path.relative(chrootRoot, path.join(dir, 'nope')), depth: 1, includeHidden: false }),
|
||||||
|
expect: { errorRegex: /Path does not exist:/ }
|
||||||
|
});
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const all = cases();
|
||||||
|
await ensureDir(sandboxRoot);
|
||||||
|
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}`);
|
||||||
|
passed++;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`✗ ${label}`);
|
||||||
|
console.error(String(err?.stack || 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 list_files test runner:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
150
tests/run-readfile-tests.js
Normal file
150
tests/run-readfile-tests.js
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import { promises as fs } from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { run as runReadFile } from '../tools/read_file.js';
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const chrootRoot = '/home/seb/src/aiTools/tmp';
|
||||||
|
const sandboxRoot = path.resolve(chrootRoot, 'readfile-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 ?? '<<undefined>>'));
|
||||||
|
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);
|
||||||
|
|
||||||
|
await writeFiles(caseDir, typeof testCase.before === 'function' ? await testCase.before({ dir: caseDir }) : (testCase.before || {}));
|
||||||
|
|
||||||
|
const args = await testCase.args({ dir: caseDir });
|
||||||
|
const result = await runReadFile(args);
|
||||||
|
if (testCase.expect?.equals !== undefined) {
|
||||||
|
expectEqual(result, testCase.expect.equals, 'Tool result');
|
||||||
|
}
|
||||||
|
if (testCase.expect?.errorRegex) {
|
||||||
|
expectRegex(result, testCase.expect.errorRegex, 'Error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cases() {
|
||||||
|
const list = [];
|
||||||
|
|
||||||
|
// 1. Read entire small file
|
||||||
|
list.push({
|
||||||
|
name: 'read entire file',
|
||||||
|
before: { 'a.txt': 'A\nB\nC' },
|
||||||
|
args: async ({ dir }) => ({ path: path.relative(chrootRoot, path.join(dir, 'a.txt')), linesToSkip: 0, linesToRead: 400 }),
|
||||||
|
expect: { equals: 'A\nB\nC' }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Skip first line, read next 1
|
||||||
|
list.push({
|
||||||
|
name: 'skip and read one',
|
||||||
|
before: { 'a.txt': 'A\nB\nC' },
|
||||||
|
args: async ({ dir }) => ({ path: path.relative(chrootRoot, path.join(dir, 'a.txt')), linesToSkip: 1, linesToRead: 1 }),
|
||||||
|
expect: { equals: 'B' }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. linesToRead 0 defaults to 400
|
||||||
|
list.push({
|
||||||
|
name: 'linesToRead zero defaults',
|
||||||
|
before: { 'a.txt': 'L1\nL2\nL3' },
|
||||||
|
args: async ({ dir }) => ({ path: path.relative(chrootRoot, path.join(dir, 'a.txt')), linesToSkip: 0, linesToRead: 0 }),
|
||||||
|
expect: { equals: 'L1\nL2\nL3' }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. Missing file -> error string
|
||||||
|
list.push({
|
||||||
|
name: 'missing file error',
|
||||||
|
before: {},
|
||||||
|
args: async ({ dir }) => ({ path: path.relative(chrootRoot, path.join(dir, 'nope.txt')), linesToSkip: 0, linesToRead: 100 }),
|
||||||
|
expect: { errorRegex: /read_file error:/ }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 5. Path outside chroot -> error
|
||||||
|
list.push({
|
||||||
|
name: 'path outside chroot error',
|
||||||
|
before: {},
|
||||||
|
args: async () => ({ path: '../../etc/passwd', linesToSkip: 0, linesToRead: 100 }),
|
||||||
|
expect: { errorRegex: /read_file error: Path outside of allowed directory/ }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 6. Large file truncated to 400 lines
|
||||||
|
list.push({
|
||||||
|
name: 'truncate to 400 lines',
|
||||||
|
before: async () => {
|
||||||
|
const many = Array.from({ length: 450 }, (_, i) => `L${i + 1}`).join('\n');
|
||||||
|
return { 'big.txt': many };
|
||||||
|
},
|
||||||
|
args: async ({ dir }) => ({ path: path.relative(chrootRoot, path.join(dir, 'big.txt')), linesToSkip: 0, linesToRead: 99999 }),
|
||||||
|
expect: { equals: Array.from({ length: 400 }, (_, i) => `L${i + 1}`).join('\n') }
|
||||||
|
});
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const all = cases();
|
||||||
|
await ensureDir(sandboxRoot);
|
||||||
|
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}`);
|
||||||
|
passed++;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`✗ ${label}`);
|
||||||
|
console.error(String(err?.stack || 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 read_file test runner:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
178
tests/run-ripgrep-tests.js
Normal file
178
tests/run-ripgrep-tests.js
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
#!/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 ?? '<<undefined>>'));
|
||||||
|
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:/ }
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -27,8 +27,8 @@ export async function run(args) {
|
|||||||
rgArgs.push('-g', filePattern);
|
rgArgs.push('-g', filePattern);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add separator and pattern
|
// Add separator, pattern, and explicit search path '.' so rg scans the chroot cwd
|
||||||
rgArgs.push('--', pattern);
|
rgArgs.push('--', pattern, '.');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const proc = spawnSync('rg', rgArgs, {
|
const proc = spawnSync('rg', rgArgs, {
|
||||||
@@ -51,9 +51,13 @@ export async function run(args) {
|
|||||||
return `ripgrep error: exit ${proc.status}, ${proc.stderr}`;
|
return `ripgrep error: exit ${proc.status}, ${proc.stderr}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Limit to 200 lines
|
// Normalize paths (strip leading './') and limit to 200 lines
|
||||||
const lines = output.split('\n');
|
const lines = output.split('\n').map((l) => l.replace(/^\.\//, ''));
|
||||||
const limitedOutput = lines.slice(0, 200).join('\n');
|
let limitedOutput = lines.slice(0, 200).join('\n');
|
||||||
|
// Remove a single trailing newline if present to align with tests
|
||||||
|
if (limitedOutput.endsWith('\n')) {
|
||||||
|
limitedOutput = limitedOutput.replace(/\n$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
return limitedOutput;
|
return limitedOutput;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user