import assert from 'node:assert/strict'; import test from 'node:test'; import { assertHookMode, cleanedCommitMessage, resolveValidationRange, terminalTrailers, validateHistoricalCommit, validateCommitMessage, } from '../scripts/validate-commit-provenance.mjs'; test('provenance accepts positive issues and one visibility trailer at the end', () => { const message = `Fix relay\n\nIssue: #94\nIssue: #98\nUser-Visible: yes\n`; assert.deepEqual(validateCommitMessage(message, [ 'docs/CHANGELOG.md', 'docs/CHANGELOG.ru.md', ]), []); assert.deepEqual(terminalTrailers(message).get('Issue'), ['#94', '#98']); }); test('editor comments and scissors suffix do not hide terminal trailers', () => { const message = `Fix relay\n\nIssue: #94\nUser-Visible: no\n# Please enter the commit message\n# On branch dev\n`; assert.deepEqual(validateCommitMessage(message), []); assert.equal(cleanedCommitMessage(`${message}# ------------------------ >8 ------------------------\nignored`) .includes('ignored'), false); }); test('user-visible provenance requires both localized changelogs', () => { const message = 'Fix UI\n\nIssue: #94\nUser-Visible: yes'; assert.match(validateCommitMessage(message, [])[0], /CHANGELOG\.md/); assert.deepEqual(validateCommitMessage(message, [ 'docs/CHANGELOG.md', 'docs/CHANGELOG.ru.md', ]), []); }); test('hook mode requires the executable index bit', () => { assert.doesNotThrow(() => assertHookMode('100755 deadbeef 0\t.githooks/commit-msg')); assert.throws( () => assertHookMode('100644 deadbeef 0\t.githooks/commit-msg'), /must be tracked as executable/, ); }); test('validation range uses PR ancestry, push before and dev for a new issue branch', () => { const calls = []; const runner = (args) => { calls.push(args); return args[0] === 'merge-base' ? 'common-base' : 'exists'; }; assert.equal(resolveValidationRange({ eventName: 'pull_request', baseSha: 'base', headSha: 'head', }, runner), 'common-base..head'); assert.deepEqual(calls.at(-1), ['merge-base', 'base', 'head']); assert.equal(resolveValidationRange({ // GitHub's default branch is main, but House Plan issue branches start at // dev. The all-zero first-push SHA must therefore compare with origin/dev. eventName: 'push', beforeSha: '000000', headSha: 'head', }, runner), 'common-base..head'); assert.deepEqual(calls.at(-1), ['merge-base', 'refs/remotes/origin/dev', 'head']); assert.equal(resolveValidationRange({ eventName: 'push', beforeSha: 'before', headSha: 'head', developmentBranch: 'dev', }, runner), 'common-base..head'); assert.deepEqual(calls.at(-1), ['merge-base', 'before', 'head']); }); test('provenance ignores trailer-like prose and rejects zero or duplicate visibility', () => { assert.notDeepEqual(validateCommitMessage('Issue: #12\n\nExplanation after it'), []); assert.notDeepEqual(validateCommitMessage('Fix\n\nIssue: #0\nUser-Visible: no'), []); assert.notDeepEqual(validateCommitMessage( 'Fix\n\nIssue: #12\nUser-Visible: no\nUser-Visible: yes', ), []); }); test('golden files require exact release-review provenance', () => { const changed = ['demo/golden/baselines/example.png']; const base = 'Update baseline\n\nIssue: #75\nUser-Visible: no'; assert.equal(validateCommitMessage(base, changed).length, 2); assert.deepEqual(validateCommitMessage( `${base}\nRelease: v1.2.3-beta.1\nBaseline-Reviewed: https://example.test/run`, changed, ), []); }); test('#641: a local WSL review trailer is exclusive and bound to the accepted index', () => { const changed = ['demo/golden/baselines/example.png']; const digest = 'a'.repeat(64); const base = 'Update baseline\n\nIssue: #641\nUser-Visible: no\nRelease: v1.2.3-beta.1'; const local = `${base}\nBaseline-Reviewed-Local: sha256:${digest}`; const index = { localAttestation: { sha256: digest } }; assert.deepEqual(validateCommitMessage(local, changed, { baselineIndex: index }), []); assert.match(validateCommitMessage(local, changed, { baselineIndex: { localAttestation: { sha256: 'b'.repeat(64) } }, }).join('\n'), /does not match/); assert.match(validateCommitMessage( `${local}\nBaseline-Reviewed: https:\/\/example.test\/run`, changed, { baselineIndex: index }, ).join('\n'), /requires one Baseline-Reviewed or Baseline-Reviewed-Local/); assert.match(validateCommitMessage( `${base}\nBaseline-Reviewed-Local: sha256:ABC`, changed, { baselineIndex: index }, ).join('\n'), /64 lowercase hex/); }); test('the audited beta.2 baseline exception is exact and golden-only', () => { const changed = ['demo/golden/baselines/example.png']; const message = 'Update baseline\n\nIssue: #426\nUser-Visible: no'; const audited = 'd4dd027b0a27c3c290195cb0e504b1a44c4c2611'; assert.deepEqual(validateHistoricalCommit(audited, message, changed), []); assert.equal(validateHistoricalCommit(`${audited.slice(0, -1)}2`, message, changed).length, 2); assert.match(validateHistoricalCommit(audited, 'Update baseline', changed)[0], /Issue/); }); // #701 (PROCESS.md §3 п.10): трейлеры — только на коммитах с продуктовыми и // инфраструктурными файлами; документационный коммит их не требует. test('#701: документационный коммит (только класс C) трейлеров не требует', () => { assert.deepEqual(validateCommitMessage('docs: fix a typo in the guide', ['docs/USER-GUIDE.md', 'README.md']), []); assert.deepEqual(validateCommitMessage('docs: changelog wording', ['docs/CHANGELOG.md']), []); // Хоть один файл вне класса C — прежнее правило. assert.equal(validateCommitMessage('fix: x', ['docs/USER-GUIDE.md', 'src/card.ts']).length, 2); assert.equal(validateCommitMessage('test: x', ['test/a.test.mjs']).length, 2); assert.equal(validateCommitMessage('build: x', ['dist/houseplan-card.js']).length >= 2, true); // Судить нечем — не документационный коммит. assert.equal(validateCommitMessage('docs: typo', []).length, 2); // Трейлер, если он есть, судится всегда: кривой номер — ошибка и в docs-коммите. assert.equal(validateCommitMessage('docs: typo\n\nIssue: #x', ['docs/a.md']).length, 2); assert.deepEqual(validateCommitMessage('docs: typo\n\nIssue: #9\nUser-Visible: no', ['docs/a.md']), []); });