#!/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 ?? '<>')); 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); });