// git-clone-gitattributes.test.mjs — Tests for E12 .gitattributes filter-driver advisory // Pure-function tests for scanGitAttributes(); the CLI path is exercised // indirectly via the existing git-clone-sandbox.test.mjs suite. import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { mkdtempSync, writeFileSync, rmSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; const { scanGitAttributes } = await import('../../scanners/lib/git-clone.mjs'); function makeRepo(contents) { const dir = mkdtempSync(join(tmpdir(), 'gitattr-test-')); if (contents !== null) { writeFileSync(join(dir, '.gitattributes'), contents); } return dir; } describe('scanGitAttributes', () => { it('flags filter driver directive (LFS-style)', () => { const dir = makeRepo('*.txt filter=lfs diff=lfs merge=lfs -text\n'); try { const warnings = scanGitAttributes(dir); const kinds = warnings.map(w => w.kind).sort(); assert.deepEqual(kinds, ['diff', 'filter', 'merge']); assert.equal(warnings[0].driver, 'lfs'); assert.equal(warnings[0].line, 1); assert.ok(warnings[0].raw.includes('filter=lfs')); } finally { rmSync(dir, { recursive: true, force: true }); } }); it('flags custom filter driver', () => { const dir = makeRepo('secrets.* filter=encrypt diff=encrypt\n'); try { const warnings = scanGitAttributes(dir); assert.equal(warnings.length, 2); assert.ok(warnings.find(w => w.kind === 'filter' && w.driver === 'encrypt')); assert.ok(warnings.find(w => w.kind === 'diff' && w.driver === 'encrypt')); } finally { rmSync(dir, { recursive: true, force: true }); } }); it('returns empty array when .gitattributes is absent', () => { const dir = makeRepo(null); try { const warnings = scanGitAttributes(dir); assert.deepEqual(warnings, []); } finally { rmSync(dir, { recursive: true, force: true }); } }); it('returns empty array on empty .gitattributes', () => { const dir = makeRepo(''); try { assert.deepEqual(scanGitAttributes(dir), []); } finally { rmSync(dir, { recursive: true, force: true }); } }); it('returns empty array when only blank lines and comments', () => { const dir = makeRepo('# comment line\n\n# filter=trap inside comment\n \n'); try { assert.deepEqual(scanGitAttributes(dir), []); } finally { rmSync(dir, { recursive: true, force: true }); } }); it('ignores trailing inline comments after stripping', () => { const dir = makeRepo('*.bin -text # filter=trap (this is a comment)\n'); try { assert.deepEqual(scanGitAttributes(dir), []); } finally { rmSync(dir, { recursive: true, force: true }); } }); it('reports correct line numbers across multi-line files', () => { const dir = makeRepo([ '# hardening', '* -text', '', '*.lfs filter=lfs', 'docs/* diff=astextplain', ].join('\n') + '\n'); try { const warnings = scanGitAttributes(dir); const filter = warnings.find(w => w.kind === 'filter'); const diff = warnings.find(w => w.kind === 'diff'); assert.equal(filter.line, 4); assert.equal(filter.driver, 'lfs'); assert.equal(diff.line, 5); assert.equal(diff.driver, 'astextplain'); } finally { rmSync(dir, { recursive: true, force: true }); } }); it('handles unreadable .gitattributes by returning empty array', () => { // Pass a path that exists as file (not directory) so existsSync says yes // but join(path, '.gitattributes') is invalid — emulates a read error // gracefully by passing a non-directory location. const result = scanGitAttributes('/does/not/exist/at/all'); assert.deepEqual(result, []); }); });