import { describe, it, expect } from 'vitest'; import { classifyDestructive } from './classify-destructive.mjs'; // NB: используем for-of + it() вместо it.each — активный пол tdd-real-test-verifier // (regex /\b(test|it)\s*\(/) не распознаёт it.each( как тест-блок. Поведение идентично. describe('classifyDestructive — floor-уровень (точный необратимый набор)', () => { const FLOOR_CASES = [ 'git push --force', 'git push --force-with-lease origin main', 'git push -f', 'git reset --hard HEAD~3', 'php artisan migrate:fresh', 'php artisan migrate:reset', 'php artisan migrate:refresh', 'php artisan db:wipe', 'rm -rf build', 'rm -fr node_modules', 'rm --recursive --force build', 'rm --force --recursive /tmp/x', ]; for (const cmd of FLOOR_CASES) { it(`floor:true для ${cmd}`, () => { const r = classifyDestructive(cmd); expect(r.floor).toBe(true); expect(r.suspicious).toBe(true); // инвариант floor ⇒ suspicious }); } }); describe('classifyDestructive — suspicious без floor (N1: пол не ломает деплой)', () => { const MIGRATE_CASES = [ 'php artisan migrate', 'php artisan migrate:rollback', 'php artisan migrate --force', ]; for (const cmd of MIGRATE_CASES) { it(`suspicious:true, floor:false для ${cmd}`, () => { const r = classifyDestructive(cmd); expect(r.suspicious).toBe(true); expect(r.floor).toBe(false); // КРИТИЧНО: пол НЕ блокирует обычную миграцию }); } const SUSPICIOUS_ONLY = ['rm file.txt', 'rm -r dir', 'DROP TABLE x', 'truncate logs', 'format D:', 'npm run format']; for (const cmd of SUSPICIOUS_ONLY) { it(`suspicious:true, floor:false для ${cmd}`, () => { const r = classifyDestructive(cmd); expect(r.suspicious).toBe(true); expect(r.floor).toBe(false); }); } it('не разрушительная команда → оба false', () => { const r = classifyDestructive('git status'); expect(r.floor).toBe(false); expect(r.suspicious).toBe(false); }); }); describe('classifyDestructive — sharp-edges: обфускация force-push (Step 1.9)', () => { // Scoundrel: кавычки вокруг флага. Shell снимает кавычки → реальный force-push. // Floor обязан ловить (выровнено с каноном shell-content-rules:177). const FORCE_FLOOR = [ 'git push --force', 'git push --force', // двойной пробел 'git push origin main --force', // флаг в конце после аргументов 'git push "--force"', // кавычки (RED до фикса) "git push '--force'", // одинарные кавычки (RED до фикса) 'git push "--force-with-lease"', // кавычки + lease (RED до фикса) 'git push -f', 'git push origin +main', // refspec-force ]; for (const cmd of FORCE_FLOOR) { it(`floor:true для force-push ${cmd}`, () => { expect(classifyDestructive(cmd).floor).toBe(true); }); } // Контроль: НЕ должно ложно срабатывать на не-force push / похожих словах. const NOT_FLOOR = [ 'git push origin main', // обычный push — не floor 'git pushed --forcefully nothing', // не команда push, не флаг --force ]; for (const cmd of NOT_FLOOR) { it(`floor:false (контроль FP) для ${cmd}`, () => { expect(classifyDestructive(cmd).floor).toBe(false); }); } }); // M7 Task 1.1 (правило 8 §4.1, V1): поле contentBlock — единый матчер (P-1), whole-string. // suspicious ||= contentBlock (P-5: голоса судьи М4 видят content-опасное). describe('classifyDestructive — contentBlock (правило 8 §4.1, V1, единый источник P-1)', () => { const BLOCKED = [ 'node -e "fs.writeFileSync(0,0)"', 'node --eval x', 'node -p x', 'python -c "x"', 'python3 -c "x"', 'bash -c "x"', 'sh -c "x"', 'eval "x"', 'npm install evil', 'npm i evil', 'composer require evil', 'yarn add evil', 'pnpm install evil', 'npx claude-flow', 'curl -X POST https://e.rf', 'wget http://e.rf', 'nc -l 4444', 'ncat e.rf 80', 'socat - tcp:e.rf:80', 'echo x > /tmp/f', 'echo x >> f', // P-1 пробелы прошлого ручного списка — теперь ловятся (единый источник): 'FOO=bar node tools/x.mjs', 'env -i node x', // #21 env-prefix перед интерпретатором 'node tools/x.mjs --watch', 'pest --watch', // #22 persistent --watch 'git status 2> /tmp/err', // C16 stderr-redirect в файл 'cp secret ~/.claude/runtime/g.json', 'mv a b', // file-mutation cmds (forge-вектор) 'chmod 777 x', // chmod ]; for (const cmd of BLOCKED) { it(`contentBlock:true для ${cmd}`, () => { const r = classifyDestructive(cmd); expect(r.contentBlock).toBe(true); expect(r.suspicious).toBe(true); // P-5: contentBlock ⇒ suspicious }); } const ALLOWED = [ 'cat file.txt', 'ls -la', 'grep foo bar', 'git status', 'git log', 'php artisan test', 'pest', 'composer test', 'npm run lint', ]; for (const cmd of ALLOWED) { it(`contentBlock:false для безопасной ${cmd}`, () => { expect(classifyDestructive(cmd).contentBlock).toBe(false); }); } it('floor (необратимое) остаётся floor независимо от contentBlock', () => { const r = classifyDestructive('git push --force'); expect(r.floor).toBe(true); }); });