418bd1fe70
Pre-fix all three regexes in extractTestMetrics fell through when Vitest output contained " | N skipped" between "passed" and "(TOTAL)" — so any test suite with .skip()'ed tests produced sentinel result=fail (false negative), blocking subsequent git commit. Two new patterns: - "Tests N passed | M skipped (TOTAL)" - "Tests X failed | N passed | M skipped (TOTAL)" Companion tests in tools/enforce-verify-record.test.mjs (new file matches TDD-gate basename heuristic) and tools/enforce-verify-before-push.test.mjs. Verified RED to GREEN: 38/38 tests pass after fix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
127 lines
4.8 KiB
JavaScript
127 lines
4.8 KiB
JavaScript
import { describe, it, expect } from 'vitest';
|
|
import { decide } from './enforce-verify-before-push.mjs';
|
|
import { decideRecord, extractTestMetrics } from './enforce-verify-record.mjs';
|
|
|
|
describe('enforce-verify-record / decideRecord', () => {
|
|
it('returns null for non-Bash', () => {
|
|
expect(decideRecord({ toolName: 'Edit', command: 'foo' })).toBeNull();
|
|
});
|
|
it('returns null for non-test command', () => {
|
|
expect(decideRecord({ toolName: 'Bash', command: 'git status', exitCode: 0, stdout: '' })).toBeNull();
|
|
});
|
|
it('returns null for narrow vitest (specific test file)', () => {
|
|
expect(decideRecord({ toolName: 'Bash', command: 'npx vitest run tools/foo.test.mjs', exitCode: 0, stdout: '' })).toBeNull();
|
|
});
|
|
it('records PASS on full vitest run with all-passed summary', () => {
|
|
const rec = decideRecord({
|
|
toolName: 'Bash', command: 'npx vitest run', exitCode: 0,
|
|
stdout: 'Tests 3708 passed (3708)',
|
|
});
|
|
expect(rec.result).toBe('pass');
|
|
expect(rec.tests_total).toBe(3708);
|
|
expect(rec.tests_passed).toBe(3708);
|
|
});
|
|
it('records FAIL on full vitest run with failed summary', () => {
|
|
const rec = decideRecord({
|
|
toolName: 'Bash', command: 'npx vitest run', exitCode: 1,
|
|
stdout: 'Tests 3 failed | 600 passed (603)',
|
|
});
|
|
expect(rec.result).toBe('fail');
|
|
expect(rec.tests_failed).toBe(3);
|
|
});
|
|
|
|
it('records PASS when exit=1 but tests_failed=0 (infra file-load failures)', () => {
|
|
// E.g. worktree CRLF copies of test files crash to load → exit code 1
|
|
// but all actual tests passed.
|
|
const rec = decideRecord({
|
|
toolName: 'Bash', command: 'npx vitest run', exitCode: 1,
|
|
stdout: 'Test Files 95 failed | 411 passed (506)\n Tests 8091 passed (8091)',
|
|
});
|
|
expect(rec.result).toBe('pass');
|
|
});
|
|
it('records pest', () => {
|
|
const rec = decideRecord({
|
|
toolName: 'Bash', command: 'composer test', exitCode: 0,
|
|
stdout: 'Tests: 742 passed (1908 assertions)',
|
|
});
|
|
expect(rec.result).toBe('pass');
|
|
});
|
|
});
|
|
|
|
describe('enforce-verify-record / extractTestMetrics', () => {
|
|
it('parses vitest all-passed', () => {
|
|
expect(extractTestMetrics('Tests 3708 passed (3708)')).toMatchObject({
|
|
tests_passed: 3708, tests_total: 3708, tests_failed: 0,
|
|
});
|
|
});
|
|
it('parses vitest mixed failure', () => {
|
|
expect(extractTestMetrics('Tests 1 failed | 631 passed (632)')).toMatchObject({
|
|
tests_failed: 1, tests_passed: 631, tests_total: 632,
|
|
});
|
|
});
|
|
it('parses vitest passed with skipped', () => {
|
|
// Vitest 4.x summary when some tests are .skip()'ed:
|
|
// "Tests 924 passed | 3 skipped (927)"
|
|
// Previously fell through all regexes → result=fail (false negative).
|
|
expect(extractTestMetrics('Tests 924 passed | 3 skipped (927)')).toMatchObject({
|
|
tests_passed: 924, tests_failed: 0, tests_total: 927,
|
|
});
|
|
});
|
|
it('parses vitest failed+passed+skipped triplet', () => {
|
|
expect(extractTestMetrics('Tests 1 failed | 920 passed | 3 skipped (924)')).toMatchObject({
|
|
tests_failed: 1, tests_passed: 920, tests_total: 924,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('enforce-verify-before-push / decide', () => {
|
|
it('allows non-Bash', () => {
|
|
expect(decide({ toolName: 'Edit', command: '' }).block).toBe(false);
|
|
});
|
|
it('allows non-git Bash', () => {
|
|
expect(decide({ toolName: 'Bash', command: 'ls -la' }).block).toBe(false);
|
|
});
|
|
it('blocks git commit without sentinel', () => {
|
|
const r = decide({ toolName: 'Bash', command: 'git commit -m "x"' });
|
|
expect(r.block).toBe(true);
|
|
expect(r.message).toMatch(/No verification/);
|
|
});
|
|
it('blocks git push without sentinel', () => {
|
|
expect(decide({ toolName: 'Bash', command: 'git push origin main' }).block).toBe(true);
|
|
});
|
|
it('blocks when sentinel result=fail', () => {
|
|
const r = decide({
|
|
toolName: 'Bash', command: 'git commit -m "x"',
|
|
sentinel: { result: 'fail', exit_code: 1, tests_passed: 600, tests_total: 603, tests_failed: 3 },
|
|
sentinelAge: 60,
|
|
});
|
|
expect(r.block).toBe(true);
|
|
expect(r.message).toMatch(/FAILED/);
|
|
});
|
|
it('blocks when sentinel is stale', () => {
|
|
const r = decide({
|
|
toolName: 'Bash', command: 'git commit -m "x"',
|
|
sentinel: { result: 'pass' },
|
|
sentinelAge: 60 * 60, // 1 hour > 30 min
|
|
});
|
|
expect(r.block).toBe(true);
|
|
expect(r.message).toMatch(/stale/);
|
|
});
|
|
it('allows when sentinel is fresh + pass', () => {
|
|
const r = decide({
|
|
toolName: 'Bash', command: 'git commit -m "x"',
|
|
sentinel: { result: 'pass' },
|
|
sentinelAge: 120,
|
|
});
|
|
expect(r.block).toBe(false);
|
|
});
|
|
it('allows when override phrase present', () => {
|
|
const r = decide({
|
|
toolName: 'Bash', command: 'git push',
|
|
sentinel: null,
|
|
override: { phrase: 'срочно', suppresses: ['verify-before-push'] },
|
|
});
|
|
expect(r.block).toBe(false);
|
|
});
|
|
});
|