// file-discovery.test.mjs — Tests for scanners/lib/file-discovery.mjs // Zero external dependencies: node:test + node:assert only. // // Regression focus (v7.8.3, #23): discovery keyed on extname(name), but // extname('.env.local') === '.local' and extname('.env.example') === '.example', // so the multi-part entries in TEXT_EXTENSIONS were dead and these common // secret files were silently skipped. import { describe, it, after } from 'node:test'; import assert from 'node:assert/strict'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { mkdtemp, writeFile, rm } from 'node:fs/promises'; import { discoverFiles } from '../../scanners/lib/file-discovery.mjs'; const created = []; async function makeRepo(files) { const root = await mkdtemp(join(tmpdir(), 'llmsec-disc-')); created.push(root); for (const [name, content] of Object.entries(files)) { await writeFile(join(root, name), content, 'utf8'); } return root; } after(async () => { for (const dir of created) await rm(dir, { recursive: true, force: true }); }); describe('file-discovery — multi-part extensions (v7.8.3, #23)', () => { it('discovers .env.local and .env.example', async () => { const root = await makeRepo({ '.env.local': 'API_KEY=sk-local-secret\n', '.env.example': 'API_KEY=changeme\n', '.env': 'API_KEY=sk-real-secret\n', }); const { files } = await discoverFiles(root); const relPaths = files.map(f => f.relPath); assert.ok(relPaths.includes('.env'), '.env must be discovered (control)'); assert.ok(relPaths.includes('.env.local'), '.env.local must be discovered'); assert.ok(relPaths.includes('.env.example'), '.env.example must be discovered'); }); it('discovers a prefixed multi-part name like backend.env.local', async () => { const root = await makeRepo({ 'backend.env.local': 'DB_PASSWORD=hunter2\n', }); const { files } = await discoverFiles(root); assert.ok( files.some(f => f.relPath === 'backend.env.local'), 'backend.env.local must be discovered' ); }); it('still skips unknown binary-ish extensions', async () => { const root = await makeRepo({ 'photo.png': 'not really a png but the extension rules it out\n', 'archive.tar.gz': 'binary-ish\n', }); const { files, skipped } = await discoverFiles(root); assert.equal(files.length, 0, 'unknown extensions must be skipped'); assert.ok(skipped >= 2); }); it('still discovers extensionless files and plain known extensions', async () => { const root = await makeRepo({ 'Makefile': 'all:\n\ttrue\n', 'index.mjs': 'export default 1;\n', }); const { files } = await discoverFiles(root); const relPaths = files.map(f => f.relPath); assert.ok(relPaths.includes('Makefile')); assert.ok(relPaths.includes('index.mjs')); }); });