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
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user