#!/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 ?? '<>')); 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:/ } }); // 7. Hidden excluded when includeHidden=false list.push({ name: 'hidden excluded by default', before: { '.hidden': 'h', 'shown.txt': 's' }, args: async ({ dir }) => ({ path: path.relative(chrootRoot, dir) || '/', depth: 1, includeHidden: false }), expect: { cwdFromArgs: true, files: [['shown.txt', 'f', 1]] } }); // 8. Depth 0 shows only top-level entries list.push({ name: 'depth 0 top-level only', before: { 'a.txt': 'A', 'sub/b.txt': 'B' }, args: async ({ dir }) => ({ path: path.relative(chrootRoot, dir) || '/', depth: 0, includeHidden: false }), expect: { cwdFromArgs: true, files: [['a.txt', 'f', 1], ['sub', 'd', null]] } }); // 9. Pass hidden file path with includeHidden=false (excluded) list.push({ name: 'hidden file path excluded when flag false', before: { '.only.txt': 'x' }, args: async ({ dir }) => ({ path: path.relative(chrootRoot, path.join(dir, '.only.txt')), depth: 1, includeHidden: false }), expect: { cwdFromArgs: 'file', files: [] } }); // 10. Pass hidden file path with includeHidden=true (included) list.push({ name: 'hidden file path included when flag true', before: { '.only.txt': 'x' }, args: async ({ dir }) => ({ path: path.relative(chrootRoot, path.join(dir, '.only.txt')), depth: 1, includeHidden: true }), expect: { cwdFromArgs: 'file', files: [['.only.txt', 'f', 1]] } }); // 11. Path normalization outside chroot -> error list.push({ name: 'outside chroot error', before: {}, args: async () => ({ path: '../../etc', depth: 1, includeHidden: false }), expect: { errorRegex: /Path escapes chroot boundary/ } }); 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); });