// git-reflog-reset.test.mjs — Regression for #20 (LOW, v7.8.3). // // The force-push detector filtered reflog lines with // `l.includes('reset:') || l.includes('reset')` — the second term subsumes the // first, so any commit whose SUBJECT merely contains the word "reset" tripped // the detector (the reflog %gs for a commit is `commit: `). Only the // reflog ACTION `reset:` (from `git reset`) is force-push evidence. import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { spawnSync } from 'node:child_process'; import { mkdtempSync, writeFileSync, rmSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { resetCounter } from '../../scanners/lib/output.mjs'; import { scan } from '../../scanners/git-forensics.mjs'; const gitAvailable = spawnSync('git', ['--version'], { encoding: 'utf-8' }).status === 0; const RESET_TITLE = 'Force push signal: reflog contains reset entries'; /** Initialise a hermetic git repo (no global hooks/signing) at `dir`. */ function gitInit(dir) { const run = (...args) => spawnSync('git', args, { cwd: dir, encoding: 'utf-8', env: { ...process.env, GIT_CONFIG_NOSYSTEM: '1' } }); run('init', '-q'); run('config', 'user.email', 'test@example.com'); run('config', 'user.name', 'Test'); run('config', 'commit.gpgsign', 'false'); run('config', 'core.hooksPath', '/dev/null'); return run; } describe('git-forensics: reflog reset detection (#20)', () => { it('a commit subject containing the word "reset" does not trip the detector', { skip: !gitAvailable }, async () => { const repo = mkdtempSync(join(tmpdir(), 'git-reflog-reset-')); try { const run = gitInit(repo); writeFileSync(join(repo, 'README.md'), '# fixture\n'); run('add', '-A'); run('commit', '-q', '-m', 'reset onboarding flow'); resetCounter(); const result = await scan(repo, {}); assert.equal(result.status, 'ok', `expected ok, got '${result.status}'`); const resetFindings = result.findings.filter(f => f.title === RESET_TITLE); assert.equal( resetFindings.length, 0, `commit subject "reset onboarding flow" must not trip the force-push detector, ` + `got: ${resetFindings.map(f => f.evidence).join('; ')}`, ); } finally { rmSync(repo, { recursive: true, force: true }); } }); it('a real `reset:` reflog action still trips the detector', { skip: !gitAvailable }, async () => { const repo = mkdtempSync(join(tmpdir(), 'git-reflog-reset-')); try { const run = gitInit(repo); writeFileSync(join(repo, 'README.md'), '# fixture\n'); run('add', '-A'); run('commit', '-q', '-m', 'initial'); writeFileSync(join(repo, 'README.md'), '# fixture v2\n'); run('add', '-A'); run('commit', '-q', '-m', 'second'); // Rewrites history: reflog gains a `reset: moving to HEAD~1` action entry. run('reset', '-q', '--hard', 'HEAD~1'); resetCounter(); const result = await scan(repo, {}); assert.equal(result.status, 'ok', `expected ok, got '${result.status}'`); const resetFindings = result.findings.filter(f => f.title === RESET_TITLE); assert.ok( resetFindings.length >= 1, `expected the reset: reflog action to trip the detector, ` + `got findings: ${result.findings.map(f => f.title).join('; ')}`, ); } finally { rmSync(repo, { recursive: true, force: true }); } }); });