183 lines
5.8 KiB
JavaScript
183 lines
5.8 KiB
JavaScript
#!/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') }
|
|
});
|
|
|
|
// 7. Skip beyond file length -> empty
|
|
list.push({
|
|
name: 'skip beyond length returns empty',
|
|
before: { 's.txt': 'A\nB' },
|
|
args: async ({ dir }) => ({ path: path.relative(chrootRoot, path.join(dir, 's.txt')), linesToSkip: 10, linesToRead: 5 }),
|
|
expect: { equals: '' }
|
|
});
|
|
|
|
// 8. Skip to last line and read one
|
|
list.push({
|
|
name: 'skip to last line and read one',
|
|
before: { 't.txt': 'L1\nL2\nL3' },
|
|
args: async ({ dir }) => ({ path: path.relative(chrootRoot, path.join(dir, 't.txt')), linesToSkip: 2, linesToRead: 1 }),
|
|
expect: { equals: 'L3' }
|
|
});
|
|
|
|
// 9. Read exactly N lines from middle
|
|
list.push({
|
|
name: 'read middle two lines',
|
|
before: { 'u.txt': 'A\nB\nC\nD' },
|
|
args: async ({ dir }) => ({ path: path.relative(chrootRoot, path.join(dir, 'u.txt')), linesToSkip: 1, linesToRead: 2 }),
|
|
expect: { equals: 'B\nC' }
|
|
});
|
|
|
|
// 10. Empty file read -> empty string
|
|
list.push({
|
|
name: 'empty file read',
|
|
before: { 'empty.txt': '' },
|
|
args: async ({ dir }) => ({ path: path.relative(chrootRoot, path.join(dir, 'empty.txt')), linesToSkip: 0, linesToRead: 100 }),
|
|
expect: { equals: '' }
|
|
});
|
|
|
|
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);
|
|
});
|
|
|
|
|