201 lines
6.8 KiB
JavaScript
201 lines
6.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 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:/ }
|
|
});
|
|
|
|
// 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);
|
|
});
|
|
|
|
|