import { describe, it, expect } from 'vitest'; import { decide, extractStagedFiles, isSecurityRelevantPath, sessionRanSemgrep } from './enforce-semgrep-security.mjs'; import { findOverride } from './enforce-hook-helpers.mjs'; describe('isSecurityRelevantPath', () => { it('matches auth files', () => { expect(isSecurityRelevantPath('app/Http/Controllers/Auth/LoginController.php')).toBe(true); expect(isSecurityRelevantPath('app/Http/Middleware/Authenticate.php')).toBe(true); }); it('matches billing/ledger files', () => { expect(isSecurityRelevantPath('app/Services/BillingService.php')).toBe(true); expect(isSecurityRelevantPath('app/Services/LedgerService.php')).toBe(true); }); it('matches CSV import/export files', () => { expect(isSecurityRelevantPath('app/Imports/SupplierLeadsImport.php')).toBe(true); expect(isSecurityRelevantPath('app/Jobs/CsvReconcileJob.php')).toBe(true); expect(isSecurityRelevantPath('app/Http/Controllers/DealCsvController.php')).toBe(true); }); it('matches webhook files', () => { expect(isSecurityRelevantPath('app/Http/Controllers/SupplierWebhookController.php')).toBe(true); expect(isSecurityRelevantPath('app/Services/WebhookSignatureVerifier.php')).toBe(true); }); it('does NOT match docs/normal files', () => { expect(isSecurityRelevantPath('docs/superpowers/plans/2026-05-28-phase4.md')).toBe(false); expect(isSecurityRelevantPath('memory/feedback_communication.md')).toBe(false); expect(isSecurityRelevantPath('app/Models/Tenant.php')).toBe(false); expect(isSecurityRelevantPath('app/Http/Controllers/HomeController.php')).toBe(false); }); it('returns false for null/empty', () => { expect(isSecurityRelevantPath(null)).toBe(false); expect(isSecurityRelevantPath('')).toBe(false); }); }); describe('extractStagedFiles', () => { it('parses git diff --cached --name-only output', () => { const stdout = 'app/Services/BillingService.php\napp/Models/Deal.php\n'; expect(extractStagedFiles(stdout)).toEqual([ 'app/Services/BillingService.php', 'app/Models/Deal.php', ]); }); it('skips blank lines', () => { expect(extractStagedFiles('a.php\n\nb.php\n')).toEqual(['a.php', 'b.php']); }); it('returns [] for empty stdout', () => { expect(extractStagedFiles('')).toEqual([]); expect(extractStagedFiles(null)).toEqual([]); }); }); describe('sessionRanSemgrep', () => { it('returns true when a Bash tool_use ran semgrep CLI', () => { const sessionUses = [ { name: 'Bash', input: { command: 'pwd' } }, { name: 'Bash', input: { command: 'semgrep scan --config p/php' } }, ]; expect(sessionRanSemgrep(sessionUses)).toBe(true); }); it('returns true when "composer sast" ran', () => { expect(sessionRanSemgrep([{ name: 'Bash', input: { command: 'composer sast' } }])).toBe(true); expect(sessionRanSemgrep([{ name: 'Bash', input: { command: 'composer sast -- --diff' } }])).toBe(true); }); it('returns true when "npm run sast" ran', () => { expect(sessionRanSemgrep([{ name: 'Bash', input: { command: 'npm run sast' } }])).toBe(true); }); it('returns false when no semgrep-like command ran', () => { expect(sessionRanSemgrep([ { name: 'Bash', input: { command: 'git status' } }, { name: 'Bash', input: { command: 'npm test' } }, ])).toBe(false); }); it('returns false for empty list', () => { expect(sessionRanSemgrep([])).toBe(false); }); it('ignores tool_use that is not Bash', () => { expect(sessionRanSemgrep([{ name: 'Skill', input: { skill: 'semgrep' } }])).toBe(false); }); }); describe('decide() — enforce-semgrep-security', () => { it('passes when command is NOT a git commit', () => { expect(decide({ command: 'git status', stagedFiles: ['app/Services/BillingService.php'], semgrepRan: false, assistantText: '', override: null, })).toEqual({ block: false }); }); it('passes when no security-relevant files in staged', () => { expect(decide({ command: 'git commit -m "docs: update"', stagedFiles: ['docs/foo.md', 'memory/bar.md'], semgrepRan: false, assistantText: '', override: null, })).toEqual({ block: false }); }); it('passes when Semgrep ran this session', () => { expect(decide({ command: 'git commit -m "feat: billing"', stagedFiles: ['app/Services/BillingService.php'], semgrepRan: true, assistantText: '', override: null, })).toEqual({ block: false }); }); it('passes with global override', () => { expect(decide({ command: 'git commit -m "fix"', stagedFiles: ['app/Services/BillingService.php'], semgrepRan: false, assistantText: '', override: { phrase: 'срочно' }, })).toEqual({ block: false }); }); it('passes with inline semgrep-skip with non-empty reason', () => { expect(decide({ command: 'git commit -m "fix"', stagedFiles: ['app/Services/BillingService.php'], semgrepRan: false, assistantText: 'something\nsemgrep-skip: тривиальный docstring fix\nother', override: null, })).toEqual({ block: false }); }); it('does NOT pass with empty semgrep-skip reason', () => { const r = decide({ command: 'git commit -m "fix"', stagedFiles: ['app/Services/BillingService.php'], semgrepRan: false, assistantText: 'semgrep-skip: ', override: null, }); expect(r.block).toBe(true); }); it('blocks when commit has security file + no Semgrep + no override', () => { const r = decide({ command: 'git commit -m "feat: billing fix"', stagedFiles: ['app/Services/BillingService.php', 'app/Models/Deal.php'], semgrepRan: false, assistantText: '', override: null, }); expect(r.block).toBe(true); expect(r.message).toContain('Semgrep'); expect(r.message).toContain('BillingService'); }); }); describe('override vocab coverage', () => { it("global override \"без скилов\" suppresses semgrep-security", () => { const o = findOverride("без скилов", 'semgrep-security'); expect(o).toBeTruthy(); }); it("global override \"direct ok\" suppresses semgrep-security", () => { const o = findOverride("direct ok", 'semgrep-security'); expect(o).toBeTruthy(); }); it("global override \"срочно\" suppresses semgrep-security", () => { const o = findOverride("срочно", 'semgrep-security'); expect(o).toBeTruthy(); }); it("global override \"быстрый коммит\" suppresses semgrep-security", () => { const o = findOverride("быстрый коммит", 'semgrep-security'); expect(o).toBeTruthy(); }); it("global override \"recovery\" does NOT suppress semgrep-security (git-only scope)", () => { const o = findOverride("recovery", 'semgrep-security'); expect(o).toBeFalsy(); }); it("global override \"memory dump\" suppresses semgrep-security", () => { const o = findOverride("memory dump", 'semgrep-security'); expect(o).toBeTruthy(); }); it("global override \"ремонт инфраструктуры\" does NOT suppress semgrep-security (narrowed to verify-only)", () => { const o = findOverride("ремонт инфраструктуры\nремонт: test reason", 'semgrep-security'); expect(o).toBeFalsy(); }); });