Files
BunBun Labs f8c82902a5 feat: AI Code Governance CLI v1.0.0
- Config-driven gates via .codegov.yml/.codegov.json
- disclosure gate: requires AI-GENERATED/AI-ASSISTED markers on changed files
- rfc gate: requires issue/RFC reference in commit message or PR description
- checklist gate: requires all items checked in REQUIREMENTS.md
- quality gate: runs configurable lint/test/typecheck commands
- CLI with readable reports and non-zero exit on violations
- 41 tests covering all gates, config loading, and integration
- Zero skeletons, no fake features, deferred items honestly documented
2026-06-14 11:08:11 +00:00

137 lines
4.9 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { writeFileSync, mkdirSync, rmSync } from 'fs';
import { resolve } from 'path';
import { runDisclosureGate } from '../src/gates/disclosure';
import { runRfcGate } from '../src/gates/rfc';
import { runChecklistGate } from '../src/gates/checklist';
import { runQualityGate } from '../src/gates/quality';
import * as gitModule from '../src/git';
const TMP = resolve(__dirname, 'tmp-gates');
function clean() {
try { rmSync(TMP, { recursive: true, force: true }); } catch {}
}
describe('disclosure gate', () => {
beforeEach(() => { clean(); mkdirSync(TMP, { recursive: true }); });
afterEach(() => { clean(); });
it('passes when all files have marker', () => {
writeFileSync(resolve(TMP, 'a.ts'), '// AI-GENERATED: this file');
writeFileSync(resolve(TMP, 'b.ts'), '/* AI-ASSISTED */');
const r = runDisclosureGate([resolve(TMP, 'a.ts'), resolve(TMP, 'b.ts')], {});
expect(r.passed).toBe(true);
});
it('fails when a file lacks marker', () => {
writeFileSync(resolve(TMP, 'a.ts'), '// AI-GENERATED: this file');
writeFileSync(resolve(TMP, 'b.ts'), 'no marker here');
const r = runDisclosureGate([resolve(TMP, 'a.ts'), resolve(TMP, 'b.ts')], {});
expect(r.passed).toBe(false);
expect(r.details?.length).toBe(1);
expect(r.details?.[0]).toContain('b.ts');
});
it('passes when no files to check (all excluded)', () => {
const r = runDisclosureGate([], { excludePaths: ['node_modules/'] });
expect(r.passed).toBe(true);
});
it('excludes configured paths', () => {
mkdirSync(resolve(TMP, 'node_modules'), { recursive: true });
mkdirSync(resolve(TMP, 'src'), { recursive: true });
writeFileSync(resolve(TMP, 'node_modules/x.js'), 'no marker');
writeFileSync(resolve(TMP, 'src/app.ts'), '// AI-GENERATED');
const r = runDisclosureGate([resolve(TMP, 'node_modules/x.js'), resolve(TMP, 'src/app.ts')], { excludePaths: ['node_modules/'] });
expect(r.passed).toBe(true);
});
});
describe('rfc gate', () => {
beforeEach(() => { vi.restoreAllMocks(); });
it('passes when commit message has issue ref', () => {
vi.spyOn(gitModule, 'getCommitMessage').mockReturnValue('Fix bug #123');
const r = runRfcGate({ source: 'commit-message' });
expect(r.passed).toBe(true);
});
it('fails when commit message lacks issue ref', () => {
vi.spyOn(gitModule, 'getCommitMessage').mockReturnValue('Fix bug');
const r = runRfcGate({ source: 'commit-message' });
expect(r.passed).toBe(false);
});
it('passes when env var matches pattern', () => {
process.env.CODEGOV_RFC_REF = 'TICKET-42';
const r = runRfcGate({ source: 'env', envVar: 'CODEGOV_RFC_REF' });
expect(r.passed).toBe(true);
delete process.env.CODEGOV_RFC_REF;
});
it('fails when env var is missing', () => {
delete process.env.CODEGOV_RFC_REF;
const r = runRfcGate({ source: 'env', envVar: 'CODEGOV_RFC_REF' });
expect(r.passed).toBe(false);
});
it('passes with custom pattern', () => {
vi.spyOn(gitModule, 'getCommitMessage').mockReturnValue('Fix [JIRA-99]');
const r = runRfcGate({ source: 'commit-message', pattern: 'JIRA-[0-9]+' });
expect(r.passed).toBe(true);
});
});
describe('checklist gate', () => {
beforeEach(() => { clean(); mkdirSync(TMP, { recursive: true }); });
afterEach(() => { clean(); });
it('passes when all items checked', () => {
writeFileSync(resolve(TMP, 'REQ.md'), '- [x] Feature A\n- [x] Feature B\n');
const r = runChecklistGate({ filePath: resolve(TMP, 'REQ.md') });
expect(r.passed).toBe(true);
});
it('fails when an item is unchecked', () => {
writeFileSync(resolve(TMP, 'REQ.md'), '- [x] Feature A\n- [ ] Feature B\n');
const r = runChecklistGate({ filePath: resolve(TMP, 'REQ.md') });
expect(r.passed).toBe(false);
expect(r.details?.length).toBe(1);
});
it('fails when file is missing', () => {
const r = runChecklistGate({ filePath: resolve(TMP, 'MISSING.md') });
expect(r.passed).toBe(false);
});
it('fails when no checked items exist', () => {
writeFileSync(resolve(TMP, 'REQ.md'), '- [ ] Feature A\n');
const r = runChecklistGate({ filePath: resolve(TMP, 'REQ.md') });
expect(r.passed).toBe(false);
});
});
describe('quality gate', () => {
it('passes when no commands configured', () => {
const r = runQualityGate({ commands: [] });
expect(r.passed).toBe(true);
});
it('passes when all commands succeed', () => {
const r = runQualityGate({ commands: ['echo ok'] });
expect(r.passed).toBe(true);
});
it('fails when a command fails', () => {
const r = runQualityGate({ commands: ['exit 1'] });
expect(r.passed).toBe(false);
});
it('runs multiple commands and reports failures', () => {
const r = runQualityGate({ commands: ['echo ok', 'exit 1', 'exit 0'] });
expect(r.passed).toBe(false);
expect(r.details?.length).toBe(1);
});
});