Files
portal/tools/enforce-verify-before-push.test.mjs
T
Дмитрий 418bd1fe70 fix(hooks): extractTestMetrics — recognise Vitest "passed | N skipped" formats
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>
2026-05-26 16:33:02 +03:00

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);
});
});