558 lines
37 KiB
JavaScript
558 lines
37 KiB
JavaScript
import { describe, it, expect } from 'vitest';
|
||
import { join } from 'node:path';
|
||
import { parsePlan, validatePlan, komandaProverokPunkta, razdvoenieProverok } from './plan-model.mjs';
|
||
// 🔴 Список мерных приборов ОДИН на всю затею и живёт в `claims-audit.mjs`. Здесь он
|
||
// только читается — чтобы сторож ниже краснел на РАСХОЖДЕНИИ ворот с общим списком,
|
||
// а не сторожил свою же копию.
|
||
import { MERNYE_PRIBORY } from './claims-audit.mjs';
|
||
import { REPO_ROOT } from './paths.mjs';
|
||
|
||
const HOROSHIY = `# План задачи
|
||
### Task 1: Первый
|
||
<!-- night: tests=tools/night/a.test.mjs; files=app/app/Models/A.php -->
|
||
текст первого пункта
|
||
### Task 2: Второй
|
||
<!-- night: tests=tools/night/b.test.mjs, tools/night/c.test.mjs; files=app/app/Models/B.php, app/resources/js/B.vue; long=yes -->
|
||
текст второго пункта
|
||
`;
|
||
|
||
describe('parsePlan — пункты плана и их пометы', () => {
|
||
it('находит все пункты по заголовкам, номера идут по порядку плана', () => {
|
||
expect(parsePlan(HOROSHIY).map(i => i.id)).toEqual(['1', '2']);
|
||
});
|
||
|
||
it('читает название пункта — владелец увидит его в отказе и в сводке', () => {
|
||
expect(parsePlan(HOROSHIY)[0].title).toBe('Первый');
|
||
});
|
||
|
||
it('читает список проверок пункта — им и меряется закрытие (проверка 39а)', () => {
|
||
expect(parsePlan(HOROSHIY)[1].tests).toEqual(['tools/night/b.test.mjs', 'tools/night/c.test.mjs']);
|
||
});
|
||
|
||
it('читает список ФАЙЛОВ пункта — на нём стоят три замка куска 3', () => {
|
||
// Не разберём files — и ворота пересечения, и `mayTouch`, и список чужих файлов
|
||
// в поручении работнику получат пустоту, то есть пропустят что угодно.
|
||
expect(parsePlan(HOROSHIY)[1].files).toEqual(['app/app/Models/B.php', 'app/resources/js/B.vue']);
|
||
});
|
||
|
||
it('files не написано — пустой список, а НЕ «все файлы мира»', () => {
|
||
expect(parsePlan('### Task 9: Без файлов\n<!-- night: tests=a.test.mjs -->\n')[0].files).toEqual([]);
|
||
});
|
||
|
||
it('tests не написано — пустой список, и пункт из плана НЕ пропадает', () => {
|
||
// 🔴 Пропади он — план прошёл бы проверку 39: пункта без проверок в списке нет,
|
||
// значит и жаловаться не на что. Пункт обязан остаться и обязан быть назван виновным.
|
||
const items = parsePlan('### Task 9: Голый\nтекст\n');
|
||
expect(items).toHaveLength(1);
|
||
expect(items[0].tests).toEqual([]);
|
||
});
|
||
|
||
it('читает пометку «длинный»', () => {
|
||
const items = parsePlan(HOROSHIY);
|
||
expect(items[0].long).toBe(false);
|
||
expect(items[1].long).toBe(true);
|
||
});
|
||
|
||
it('помета на ТРЕТЬЕЙ строке под заголовком — ещё найдена', () => {
|
||
const p = '### Task 3: Третий\nпервая строка\nвторая строка\n<!-- night: tests=a.test.mjs -->\n';
|
||
expect(parsePlan(p)[0].tests).toEqual(['a.test.mjs']);
|
||
});
|
||
|
||
it('помета на ЧЕТВЁРТОЙ строке — уже НЕ найдена, окно ровно три строки', () => {
|
||
// 🪤 Без границы помета нашлась бы где угодно ниже по тексту — например, помета
|
||
// СЛЕДУЮЩЕГО пункта досталась бы предыдущему, и проверки чужого пункта считались бы
|
||
// своими. Граница названа в договоре о стыках дословно: ближайшие три строки.
|
||
const p = '### Task 3: Третий\nстрока\nстрока\nстрока\n<!-- night: tests=a.test.mjs -->\n';
|
||
expect(parsePlan(p)[0].tests).toEqual([]);
|
||
});
|
||
|
||
it('🔴 окно обрывается на заголовке СЛЕДУЮЩЕГО пункта — чужая помета своей не станет', () => {
|
||
// 🔴 Одного числа «три строки» мало: пункты идут вплотную, и пункт БЕЗ пометы
|
||
// дотягивается до пометы соседа через его заголовок. Тогда пункт без единой
|
||
// проверки выглядит как пункт с проверками — проверка 39 обходится молча,
|
||
// а закрытие меряется зеленью ЧУЖОГО пункта (проверка 38 туда же).
|
||
const p = '### Task 1: Без пометы\nтекст\n### Task 2: С пометой\n<!-- night: tests=a.test.mjs -->\n';
|
||
const items = parsePlan(p);
|
||
expect(items[0].tests).toEqual([]);
|
||
expect(items[1].tests).toEqual(['a.test.mjs']);
|
||
});
|
||
|
||
it('🔴 «long=yes» в ТЕКСТЕ пункта пометкой не считается — её ставит пишущий план (проверка 41)', () => {
|
||
// 🔴 Проверка 41 требует дословно: «пометку ставит тот, кто пишет план, а не работник
|
||
// на ходу». Работник, дописавший в текст «long=yes», не должен уметь пометить свой
|
||
// пункт длинным — иначе датчик топтания выключается изнутри, теми же руками,
|
||
// за которыми он и следит.
|
||
const p = [
|
||
'### Task 7: Работник дописал сам',
|
||
'Работник посчитал пункт длинным и написал прямо в тексте long=yes — это не помета.',
|
||
'<!-- night: tests=a.test.mjs -->',
|
||
'',
|
||
].join('\n');
|
||
expect(parsePlan(p)[0].long).toBe(false);
|
||
});
|
||
|
||
it('«#### Task 5» — четыре решётки, это не пункт', () => {
|
||
expect(parsePlan('#### Task 5: Подзаголовок\n<!-- night: tests=a.test.mjs -->\n')).toEqual([]);
|
||
});
|
||
|
||
it('составной номер пункта читается целиком: «4.11», а не «4»', () => {
|
||
expect(parsePlan('### Task 4.11: Составной\n<!-- night: tests=a.test.mjs -->\n')[0].id).toBe('4.11');
|
||
});
|
||
|
||
it('лишние пробелы и пустые куски списка срезаются', () => {
|
||
const p = '### Task 1: A\n<!-- night: tests= a.test.mjs ,, b.test.mjs ; files= ; long= YES -->\n';
|
||
const item = parsePlan(p)[0];
|
||
expect(item.tests).toEqual(['a.test.mjs', 'b.test.mjs']);
|
||
expect(item.files).toEqual([]);
|
||
expect(item.long).toBe(true);
|
||
});
|
||
|
||
it('🔴 пункт ВНУТРИ рамки-образца — не пункт, а картинка пункта', () => {
|
||
// 🔴 Взято с живых планов: `docs/superpowers/plans/2026-08-01-kusok4-...md` и
|
||
// `...kusok3-...md` целиком состоят из таких образцов — там 17 и 4 заголовка,
|
||
// и НИ ОДНОГО настоящего пункта. Разбор брал их за работу: помощник соседней задачи
|
||
// получил куски примеров как пункты и чуть не записал ложный вывод о планах.
|
||
// Примета — РАЗМЕТКА: рамка ``` … ```, а не какое-то слово внутри неё.
|
||
const p = [
|
||
'# План',
|
||
'```javascript',
|
||
'const OBRAZEC = `',
|
||
'### Task 1: Первый из примера',
|
||
'<!-- night: tests=primer.test.mjs -->',
|
||
'`;',
|
||
'```',
|
||
'### Task 7: Настоящий',
|
||
'<!-- night: tests=nastoyashchiy.test.mjs -->',
|
||
'',
|
||
].join('\n');
|
||
const items = parsePlan(p);
|
||
expect(items.map(i => i.id)).toEqual(['7']);
|
||
expect(items[0].tests).toEqual(['nastoyashchiy.test.mjs']);
|
||
});
|
||
|
||
it('рамка на тильдах — тоже рамка', () => {
|
||
const p = '~~~md\n### Task 1: Из примера\n~~~\n### Task 2: Настоящий\n<!-- night: tests=a.test.mjs -->\n';
|
||
expect(parsePlan(p).map(i => i.id)).toEqual(['2']);
|
||
});
|
||
|
||
it('🪤 помета ВНУТРИ рамки настоящему пункту не достаётся', () => {
|
||
// 🪤 Мало не считать заголовок из образца пунктом: если помету из образца отдать
|
||
// настоящему пункту, тот получит ЧУЖИЕ проверки — проверка 38 меряет закрытие
|
||
// зеленью, которой этот пункт не касался.
|
||
const p = [
|
||
'### Task 1: Настоящий без пометы',
|
||
'```',
|
||
'<!-- night: tests=iz-obrazca.test.mjs -->',
|
||
'```',
|
||
'',
|
||
].join('\n');
|
||
expect(parsePlan(p)[0].tests).toEqual([]);
|
||
});
|
||
|
||
it('🔴 «````» с кавычками в подписи — НЕ рамка: пункт ниже не пропадает', () => {
|
||
// 🔴 Вырезание второго рода, и обман не выдуманный — он лежит в живом плане
|
||
// `docs/superpowers/plans/2026-06-12-router-mentor-activation-wrapper.md` строка 238.
|
||
// Простая примета «строка начинается с трёх кавычек» считает эту строку началом
|
||
// рамки, рамка не закрывается никогда — и ТРИ настоящих пункта ниже пропадают МОЛЧА.
|
||
// По разметке это не рамка, а вставка кода в строке: у рамки на кавычках в подписи
|
||
// кавычек быть не может.
|
||
const p = [
|
||
'Решение владельца: план несёт блок',
|
||
'```` ```verified-context-json ```` с массивом записей.',
|
||
'### Task 4: Настоящий после ловушки',
|
||
'<!-- night: tests=a.test.mjs -->',
|
||
'',
|
||
].join('\n');
|
||
expect(parsePlan(p).map(i => i.id)).toEqual(['4']);
|
||
});
|
||
|
||
it('рамка открыта четырьмя кавычками — тройка внутри её не закрывает', () => {
|
||
const p = [
|
||
'````md',
|
||
'```js',
|
||
'### Task 1: Всё ещё пример',
|
||
'```',
|
||
'````',
|
||
'### Task 2: Настоящий',
|
||
'<!-- night: tests=a.test.mjs -->',
|
||
'',
|
||
].join('\n');
|
||
expect(parsePlan(p).map(i => i.id)).toEqual(['2']);
|
||
});
|
||
|
||
it('🔴 рамку забыли закрыть, и она съела пункт — падаем ГРОМКО с номером строки', () => {
|
||
// 🔴 Самый дорогой случай: незакрытая рамка глотает ЧАСТЬ плана. Всё съесть —
|
||
// не беда, ворота скажут «в плане нет пунктов». А съесть половину — ночь отработает
|
||
// урезанный план, и владелец об этом не узнает НИКОГДА.
|
||
const p = [
|
||
'### Task 1: Настоящий',
|
||
'<!-- night: tests=a.test.mjs -->',
|
||
'```',
|
||
'кто-то забыл закрыть рамку',
|
||
'### Task 2: Съеденный',
|
||
'<!-- night: tests=b.test.mjs -->',
|
||
'',
|
||
].join('\n');
|
||
expect(() => parsePlan(p)).toThrow(/рамк/i);
|
||
expect(() => parsePlan(p)).toThrow(/3/); // номер строки, где рамка открыта
|
||
});
|
||
|
||
it('рамку забыли закрыть, но она ничего не съела — план не рушим', () => {
|
||
// 🪤 Так кончаются СЕМЬ живых планов в docs/superpowers/plans: лишняя тройка кавычек
|
||
// в самом конце. Ронять на этом целый план — ложная тревога: не потеряно ничего.
|
||
const p = [
|
||
'### Task 1: Настоящий',
|
||
'<!-- night: tests=a.test.mjs -->',
|
||
'```',
|
||
'хвост без закрытия',
|
||
'',
|
||
].join('\n');
|
||
expect(parsePlan(p).map(i => i.id)).toEqual(['1']);
|
||
});
|
||
|
||
it('🔴 подан не текст — падает ГРОМКО, а не «в плане нет пунктов»', () => {
|
||
// 🪤 Пустышка, тихо отвечающая «всё хорошо», за три куска ловилась пять раз.
|
||
// Отдай мы тут пустой список — ворота напечатали бы владельцу «в вашем плане
|
||
// нет ни одного пункта», хотя план цел, а сломалось чтение файла.
|
||
expect(() => parsePlan(null)).toThrow(/текст плана/i);
|
||
expect(() => parsePlan(undefined)).toThrow(/текст плана/i);
|
||
});
|
||
});
|
||
|
||
describe('validatePlan — проверка 39 (пункт без проверок) и проверка 41 (перебор длинных)', () => {
|
||
it('план без пунктов вовсе — не проходит', () => {
|
||
const v = validatePlan(parsePlan('# просто текст, заголовков пунктов нет'));
|
||
expect(v.ok).toBe(false);
|
||
expect(v.reason).toMatch(/ни одного пункта/i);
|
||
});
|
||
|
||
it('🔴 два пункта с ОДНИМ номером — план не проходит, номер назван', () => {
|
||
// 🔴 Номер пункта — это КЛЮЧ: по нему ведут очередь, по нему запирают файлы, по нему
|
||
// работник заявляет «закрыл». Два пункта с одним номером — двое работников закроют
|
||
// друг друга, и оба отчитаются правдой. Замолчать это нельзя.
|
||
// Живьём такое есть: docs/superpowers/plans/2026-05-20-observer-instrument-expansion.md
|
||
// несёт два настоящих «### Task 13» подряд.
|
||
const p = [
|
||
'### Task 13: Первый', '<!-- night: tests=a.test.mjs -->',
|
||
'### Task 13: Он же, переписанный', '<!-- night: tests=b.test.mjs -->', '',
|
||
].join('\n');
|
||
const v = validatePlan(parsePlan(p));
|
||
expect(v.ok).toBe(false);
|
||
expect(v.reason).toContain('13');
|
||
expect(v.reason).toMatch(/повтор|один и тот же номер/i);
|
||
});
|
||
|
||
it('🪤 повторяются несколько номеров — названы ВСЕ', () => {
|
||
const p = [
|
||
'### Task 1: A', '<!-- night: tests=a.test.mjs -->',
|
||
'### Task 1: B', '<!-- night: tests=b.test.mjs -->',
|
||
'### Task 2: C', '<!-- night: tests=c.test.mjs -->',
|
||
'### Task 2: D', '<!-- night: tests=d.test.mjs -->', '',
|
||
].join('\n');
|
||
const v = validatePlan(parsePlan(p));
|
||
expect(v.reason).toContain('1');
|
||
expect(v.reason).toContain('2');
|
||
});
|
||
|
||
it('🔴 повтор ловится РАНЬШЕ, чем «пункт без проверок»', () => {
|
||
// 🔴 Иначе владельцу назовут виновным «Task 1» — а таких в плане два, и он не поймёт,
|
||
// который чинить. Пока ключ не уникален, любое имя в отказе двусмысленно.
|
||
const p = [
|
||
'### Task 1: С проверкой', '<!-- night: tests=a.test.mjs -->',
|
||
'### Task 1: Без проверок', 'текст', '',
|
||
].join('\n');
|
||
const v = validatePlan(parsePlan(p));
|
||
expect(v.ok).toBe(false);
|
||
expect(v.reason).toMatch(/повтор|один и тот же номер/i);
|
||
expect(v.reason).not.toMatch(/ни одной проверки/i);
|
||
});
|
||
|
||
it('номера разные — ложной тревоги нет', () => {
|
||
const p = [
|
||
'### Task 1: A', '<!-- night: tests=a.test.mjs -->',
|
||
'### Task 1.1: B', '<!-- night: tests=b.test.mjs -->', '',
|
||
].join('\n');
|
||
expect(validatePlan(parsePlan(p)).ok).toBe(true);
|
||
});
|
||
|
||
it('пункт без единой проверки — план не проходит, и виновный НАЗВАН (проверка 39б)', () => {
|
||
const bad = '### Task 1: Без проверок\nтекст\n### Task 2: С проверкой\n<!-- night: tests=a.test.mjs -->\n';
|
||
const v = validatePlan(parsePlan(bad));
|
||
expect(v.ok).toBe(false);
|
||
expect(v.reason).toContain('Task 1');
|
||
expect(v.reason).toMatch(/ни одной проверки/i);
|
||
});
|
||
|
||
it('🪤 виновных несколько — названы ВСЕ, а не первый попавшийся', () => {
|
||
// 🪤 Владелец чинит план по этому списку. Назови мы один пункт — он поправит его,
|
||
// запустит снова, получит отказ про второй, и так каждую ночь по одному пункту.
|
||
const bad = '### Task 1: A\nтекст\n### Task 2: B\nтекст\n### Task 3: C\n<!-- night: tests=a.test.mjs -->\n';
|
||
const v = validatePlan(parsePlan(bad));
|
||
expect(v.reason).toContain('Task 1');
|
||
expect(v.reason).toContain('Task 2');
|
||
});
|
||
|
||
it('длинных ровно половина — проходит', () => {
|
||
const p = '### Task 1: A\n<!-- night: tests=a.test.mjs; long=yes -->\n### Task 2: B\n<!-- night: tests=b.test.mjs -->\n';
|
||
expect(validatePlan(parsePlan(p)).ok).toBe(true);
|
||
});
|
||
|
||
it('длинных больше половины — плана к запуску нет, и оба числа названы (проверка 41)', () => {
|
||
const p = [
|
||
'### Task 1: A', '<!-- night: tests=a.test.mjs; long=yes -->',
|
||
'### Task 2: B', '<!-- night: tests=b.test.mjs; long=yes -->',
|
||
'### Task 3: C', '<!-- night: tests=c.test.mjs -->', '',
|
||
].join('\n');
|
||
const v = validatePlan(parsePlan(p));
|
||
expect(v.ok).toBe(false);
|
||
expect(v.reason).toMatch(/больше половины/i);
|
||
expect(v.reason).toContain('2');
|
||
expect(v.reason).toContain('3');
|
||
});
|
||
|
||
it('один пункт, и он длинный — тоже перебор (граница «больше половины»)', () => {
|
||
const p = '### Task 1: A\n<!-- night: tests=a.test.mjs; long=yes -->\n';
|
||
expect(validatePlan(parsePlan(p)).ok).toBe(false);
|
||
});
|
||
|
||
it('план прошёл — числа отданы, их печатает владельцу задача 6', () => {
|
||
const v = validatePlan(parsePlan(HOROSHIY));
|
||
expect(v.ok).toBe(true);
|
||
expect(v.reason).toBeNull();
|
||
expect(v.items_count).toBe(2);
|
||
expect(v.long_count).toBe(1);
|
||
});
|
||
|
||
it('🔴 подан не список пунктов — падает ГРОМКО, а не «план хорош»', () => {
|
||
// 🔴 Это самый дорогой из тихих сбоев: зовущий (задача 6) промахнулся мимо разбора,
|
||
// а ворота прочли «ok» и пустили в работу план, которого никто не смотрел.
|
||
expect(() => validatePlan(null)).toThrow(/список пунктов/i);
|
||
expect(() => validatePlan({ items: [] })).toThrow(/список пунктов/i);
|
||
});
|
||
});
|
||
|
||
describe('komandaProverokPunkta — чем гонять проверки ИМЕННО ЭТОГО пункта (проверка 38)', () => {
|
||
it('🔴 команда строится ИЗ `tests` этого пункта, а не из общего набора', () => {
|
||
// 🔴🔴 Сердце проверки 38. Возьми правило общий набор — «зелень по этому пункту»
|
||
// стала бы «зеленью вообще», и пункт засчитывался бы за чужую работу при полностью
|
||
// зелёных проверках. Ломается ТИХО, поэтому сторожится отсюда и до самого замка.
|
||
expect(komandaProverokPunkta({ id: '1', tests: ['app/tests/Feature/PervyyTest.php'] }))
|
||
.toBe('php artisan test --filter=PervyyTest');
|
||
expect(komandaProverokPunkta({ id: '2', tests: ['tools/night/vtoroy.test.mjs'] }))
|
||
.toBe('npm run test:tools -- tools/night/vtoroy.test.mjs');
|
||
});
|
||
|
||
it('у пункта проверок нет — команды НЕТ, а не «весь набор на всякий случай»', () => {
|
||
// 🪤 Отдай тут общий набор — пункт без проверок закрылся бы чужой зеленью, то есть
|
||
// проверка 39 обходилась бы через замок.
|
||
expect(komandaProverokPunkta({ id: '3', tests: [] })).toBeNull();
|
||
expect(komandaProverokPunkta(null)).toBeNull();
|
||
});
|
||
|
||
// 🔴🔴 ЗДЕСЬ СТОЯЛ СТОРОЖ, ОХРАНЯВШИЙ БЕДУ. Он требовал дословно:
|
||
// komandaProverokPunkta({ tests: ['app/tests/Feature/A.php', 'tools/night/b.test.mjs'] })
|
||
// === 'npm run test:tools -- app/tests/Feature/A.php tools/night/b.test.mjs'
|
||
// и был зелёным. Довод при нём стоял такой: «`php artisan test` наш `.mjs` не запустит
|
||
// вовсе, поэтому смешанный набор идёт нашим прогоном». Довод верен наполовину — и
|
||
// ровно этой половиной и опасен: наш прогон php-шную проверку не запустит ТОЖЕ.
|
||
//
|
||
// 🔴 ЗАМЕРЕНО ЖИВЬЁМ 02.08.2026, не выведено рассуждением. Эта самая команда выполнена
|
||
// руками из корня хозяйства:
|
||
// npm run test:tools -- app/tests/Feature/ProjectSyncStatusFromPivotTest.php \
|
||
// tools/night/plan-fingerprint.test.mjs
|
||
// Ответ: «Test Files 1 passed (1) · Tests 15 passed (15)», код возврата 0. То есть
|
||
// из ДВУХ названных проверок отработала ОДНА — наша; php-шная не запускалась вовсе,
|
||
// и ни слова о ней сказано не было. Имя php-шного файла vitest берёт за строку-отбор,
|
||
// ни одного файла по ней не находит и молча идёт дальше — потому что второе имя
|
||
// что-то нашло.
|
||
//
|
||
// 🔴 ЧЕМ ЭТО КОНЧАЕТСЯ У ЗАМКА. Прогон отработал с нулём и что-то напечатал — значит
|
||
// и заслон «программа ничего не сказала» (item-audit.mjs) его не поймает. Замок
|
||
// объявляет проверки зелёными и засчитывает пункт, у которого половина проверок
|
||
// не запускалась. Владелец наутро читает «пункт закрыт, проверки прогнаны надзирателем
|
||
// и зелены» — про бэкенд, которого никто не мерил.
|
||
//
|
||
// 🪤 Отдельно замерено, что тихо это ТОЛЬКО у смешанного: набор из одних php-шных имён,
|
||
// поданный тому же прогону, отвечает кодом 1 («No test files found»). Беда рождается
|
||
// именно смешением, а не самим php-шным именем.
|
||
it('🔴 проверки пункта РАЗНОГО рода — команды НЕТ: половину молча не меряем', () => {
|
||
// Одной командой два рода не гоняются. Врать половиной хуже, чем не мерить вовсе:
|
||
// «замерить нечем» замок читает как «пункт не засчитан», а половинную зелень — как
|
||
// «пункт закрыт».
|
||
expect(komandaProverokPunkta({ id: '4', tests: ['app/tests/Feature/A.php', 'tools/night/b.test.mjs'] }))
|
||
.toBeNull();
|
||
// Порядок не важен: php-шная может стоять и второй.
|
||
expect(komandaProverokPunkta({ id: '5', tests: ['tools/night/b.test.mjs', 'app/tests/Feature/A.php'] }))
|
||
.toBeNull();
|
||
// 🪤 Вырезание второго рода: род считается по концу имени БЕЗ оглядки на регистр.
|
||
// Считай мы его строго — «A.PHP» рядом с нашей проверкой снова уехало бы в наш прогон,
|
||
// и беда вернулась бы целиком, с зелёным сторожем над ней.
|
||
expect(komandaProverokPunkta({ id: '6', tests: ['app/tests/Feature/A.PHP', 'tools/night/b.test.mjs'] }))
|
||
.toBeNull();
|
||
});
|
||
|
||
// 🔴🔴 «А ЧТО ПРИДЁТ СЮДА, ЕСЛИ ПРИШЛЮТ НЕ ДА И НЕ НЕТ, А ТРЕТЬЕ?»
|
||
//
|
||
// Пункт приходит к замку не только из разбора плана, но и с диска — из карточки работника
|
||
// (`next-item.json`). Порченый или правленный рукой JSON легко даёт `tests` СТРОКОЙ,
|
||
// а не списком. Замерено 02.08.2026: первый вид этой защиты падал на такой строке
|
||
// с `TypeError`, причём падал ВЫШЕ того места, где замок ловит сбои прогона, — то есть
|
||
// не «пункт не засчитан», а надзиратель насмерть посреди ночи.
|
||
//
|
||
// 🔴 Ответ выбран «не мой вопрос»: разного рода тут не видно, судить нечего, и правило
|
||
// молчит. Негодный `tests` ловят двое других — ворота плана (там `Array.isArray`
|
||
// прописан прямо) и сам замок, у которого этот сбой попадает в разбор «замерить нечем».
|
||
it('🪤 `tests` пришли не списком — правило не падает и никого не обвиняет', () => {
|
||
expect(() => razdvoenieProverok({ id: '1', tests: 'a.php,b.test.mjs' })).not.toThrow();
|
||
expect(razdvoenieProverok({ id: '1', tests: 'a.php,b.test.mjs' })).toBeNull();
|
||
expect(razdvoenieProverok({ id: '2', tests: 42 })).toBeNull();
|
||
expect(razdvoenieProverok(null)).toBeNull();
|
||
// А ворота плана такой пункт всё равно не пускают — своим отказом, не этим.
|
||
expect(validatePlan([{ id: '1', tests: 'a.php,b.test.mjs', files: [] }]).ok).toBe(false);
|
||
});
|
||
|
||
it('🪤 однородный набор по-прежнему считается — лишнего отказа тут нет', () => {
|
||
// Лишний отказ — тоже беда: пункт, который вправду можно померить, ночь бы не взяла.
|
||
expect(komandaProverokPunkta({ id: '7', tests: ['app/tests/Feature/A.php', 'app/tests/Unit/B.php'] }))
|
||
.toBe('php artisan test --filter=A|B');
|
||
expect(komandaProverokPunkta({ id: '8', tests: ['tools/night/a.test.mjs', 'tools/night/b.test.mjs'] }))
|
||
.toBe('npm run test:tools -- tools/night/a.test.mjs tools/night/b.test.mjs');
|
||
});
|
||
|
||
it('🪤 «php» ВНУТРИ имени нашей проверки — это наша проверка, а не php-шная', () => {
|
||
// 🪤 Вырезание второго рода: считай род по «встречается ли .php в имени» вместо
|
||
// «кончается ли имя на .php» — и наша же проверка про php-шную часть затеи
|
||
// (`php-shnye.test.mjs`, `A.php.test.mjs`) объявлялась бы чужого рода. Пункт с двумя
|
||
// нашими проверками получил бы отказ на ровном месте, и владелец правил бы план,
|
||
// в котором править нечего. Лишний отказ — тоже беда.
|
||
expect(komandaProverokPunkta({ id: '9', tests: ['tools/night/php-shnye.test.mjs', 'tools/night/b.test.mjs'] }))
|
||
.toBe('npm run test:tools -- tools/night/php-shnye.test.mjs tools/night/b.test.mjs');
|
||
expect(razdvoenieProverok({ id: '9', tests: ['tools/night/A.php.test.mjs', 'tools/night/b.test.mjs'] }))
|
||
.toBeNull();
|
||
});
|
||
});
|
||
|
||
// 🔴🔴 ВОРОТА ПЛАНА НЕ ПУСКАЮТ ПУНКТ СО СМЕШАННЫМИ ПРОВЕРКАМИ.
|
||
//
|
||
// 🔴 ЗАЧЕМ ЗДЕСЬ, А НЕ ТОЛЬКО У ЗАМКА — довод тот же, что и у мерного прибора в `files=`:
|
||
// это самое дешёвое из мест. Владелец правит план утром за минуту, и правка простая —
|
||
// разбить пункт надвое. Оставь мы одну защиту у замка — план бы прошёл, работник взял бы
|
||
// пункт, закрыл бы его, а замок каждый раз отвечал бы «замерить нечем»: пункт не
|
||
// засчитывается, работник берёт его снова, и так до утра. Это ровно та ночь, которая
|
||
// сгорает молча и выглядит как работа.
|
||
describe('🔴 ворота плана — проверки пункта разного рода', () => {
|
||
it('план со смешанным пунктом не годен, и названы пункт, обе проверки и что делать', () => {
|
||
const p = '### Task 1: И бэкенд, и затея\n<!-- night: tests=app/tests/Feature/A.php, tools/night/b.test.mjs -->\n';
|
||
const v = validatePlan(parsePlan(p));
|
||
expect(v.ok).toBe(false);
|
||
expect(v.reason).toContain('Task 1');
|
||
// Владелец не программист: ему нужны имена обеих проверок, а не слово «смешанный».
|
||
expect(v.reason).toContain('app/tests/Feature/A.php');
|
||
expect(v.reason).toContain('tools/night/b.test.mjs');
|
||
// И прямое указание, что делать: разбить пункт на два.
|
||
expect(v.reason).toMatch(/раздел|разбей|разбить|два пункта/i);
|
||
});
|
||
|
||
it('🪤 виновных несколько — названы ВСЕ разом, а не первый попавшийся', () => {
|
||
const p = [
|
||
'### Task 1: A', '<!-- night: tests=a.php, b.test.mjs -->',
|
||
'### Task 2: B', '<!-- night: tests=b.test.mjs, c.test.mjs -->',
|
||
'### Task 3: C', '<!-- night: tests=x.test.mjs, y.php -->', '',
|
||
].join('\n');
|
||
const v = validatePlan(parsePlan(p));
|
||
expect(v.ok).toBe(false);
|
||
expect(v.reason).toContain('Task 1');
|
||
expect(v.reason).toContain('Task 3');
|
||
expect(v.reason).not.toContain('Task 2');
|
||
});
|
||
|
||
it('🪤 однородные пункты план не роняют — ни php-шные, ни наши', () => {
|
||
const p = [
|
||
'### Task 1: Бэкенд', '<!-- night: tests=app/tests/Feature/A.php, app/tests/Unit/B.php -->',
|
||
'### Task 2: Затея', '<!-- night: tests=tools/night/a.test.mjs -->', '',
|
||
].join('\n');
|
||
expect(validatePlan(parsePlan(p)).ok).toBe(true);
|
||
});
|
||
});
|
||
|
||
// 🔴🔴 ВОРОТА ПЛАНА НЕ ПУСКАЮТ ПЛАН С МЕРНЫМ ПРИБОРОМ В `files=`.
|
||
//
|
||
// 🔴 ЗАЧЕМ ИМЕННО ЗДЕСЬ, А НЕ ТОЛЬКО В ЗАМКЕ. Замерено 02.08.2026: пункт с `package.json`
|
||
// в `files=` проходил ворота, работник честно занимал файл, правил его и коммитил, а сверка
|
||
// тронутых файлов (`claims-audit.mjs`) видела прибор, ОТКАТЫВАЛА правку и ОТБИРАЛА пункт —
|
||
// и так каждые полминуты до утра. Владелец наутро читал «работал восемь часов» и видел ноль
|
||
// сделанного. Это ХУЖЕ отказа: отказ виден сразу, а такая ночь сгорает молча и выглядит
|
||
// как работа. Ворота ловят ту же беду за минуту до её начала и словами, по которым владелец
|
||
// правит план утром.
|
||
describe('🔴 ворота плана — мерный прибор в `files=` пункта', () => {
|
||
it('план с `files=package.json` не годен, и названы пункт, файл и довод', () => {
|
||
const p = '### Task 1: Правка наряда\n<!-- night: tests=a.test.mjs; files=package.json -->\n';
|
||
const v = validatePlan(parsePlan(p));
|
||
expect(v.ok).toBe(false);
|
||
expect(v.reason).toContain('Task 1');
|
||
expect(v.reason).toContain('package.json');
|
||
// Довод берётся из самого списка — владельцу нужен не запрет, а причина запрета.
|
||
expect(v.reason).toContain('наряд');
|
||
});
|
||
|
||
it('🔴 запирается КАЖДАЯ позиция общего списка — второго списка тут не заведено', () => {
|
||
// 🪤 Заведись здесь свой список — он разошёлся бы с `claims-audit.mjs` молча: ворота
|
||
// пускали бы то, что сверка потом откатывает, и ночь сгорала бы ровно так же.
|
||
// Этот сторож покраснеет и на своём списке, и на позиции, забытой при пополнении общего.
|
||
for (const pribor of MERNYE_PRIBORY) {
|
||
const put = pribor.vetka ? `${pribor.put}/vglub.mjs` : pribor.put;
|
||
const p = `### Task 1: A\n<!-- night: tests=a.test.mjs; files=${put} -->\n`;
|
||
expect(validatePlan(parsePlan(p)).ok, `«${put}» прошёл мимо ворот`).toBe(false);
|
||
}
|
||
});
|
||
|
||
it('обходы написанием: точка, обратная косая, регистр, шаг назад через `..`', () => {
|
||
// 🪤 Вырезание второго рода: автор плана не пишет прибор в лоб, а называет его иначе.
|
||
for (const put of ['./package.json', '.\\package.json', 'PACKAGE.JSON',
|
||
'app/../package.json', 'tools/night/../../package.json', 'tools\\night\\supervisor.mjs']) {
|
||
const p = `### Task 1: A\n<!-- night: tests=a.test.mjs; files=${put} -->\n`;
|
||
expect(validatePlan(parsePlan(p)).ok, `обход «${put}» ПРОШЁЛ мимо ворот`).toBe(false);
|
||
}
|
||
});
|
||
|
||
it('полный путь от корня хозяйства — тот же прибор, а не другой файл', () => {
|
||
// 🪤 Владелец набирает пути из окна Windows целиком; несведённый полный путь ворота
|
||
// считали бы посторонним файлом и пустили бы план.
|
||
const put = join(REPO_ROOT, 'package.json');
|
||
const p = `### Task 1: A\n<!-- night: tests=a.test.mjs; files=${put} -->\n`;
|
||
expect(validatePlan(parsePlan(p)).ok).toBe(false);
|
||
});
|
||
|
||
it('🪤 похожее имя рядом — план ГОДЕН: ворота не смеют запирать лишнее', () => {
|
||
// Лишний запрет — тоже беда: владелец правит план, которого нечего править.
|
||
for (const put of ['tools/nightly/x.mjs', 'tools/night-plan.mjs', 'package.json.bak',
|
||
'app/package.json', 'docs/зам.md']) {
|
||
const p = `### Task 1: A\n<!-- night: tests=a.test.mjs; files=${put} -->\n`;
|
||
expect(validatePlan(parsePlan(p)).ok, `«${put}» заперт зря`).toBe(true);
|
||
}
|
||
});
|
||
|
||
it('🪤 виновных несколько — названы ВСЕ разом, а не первый попавшийся', () => {
|
||
// 🔴 Тот же довод, что и у пунктов без проверок: получать отказ по одному пункту
|
||
// за ночь владельцу незачем.
|
||
const p = [
|
||
'### Task 1: A', '<!-- night: tests=a.test.mjs; files=package.json -->',
|
||
'### Task 2: B', '<!-- night: tests=b.test.mjs; files=app/A.php -->',
|
||
'### Task 3: C', '<!-- night: tests=c.test.mjs; files=tools/night/supervisor.mjs -->', '',
|
||
].join('\n');
|
||
const v = validatePlan(parsePlan(p));
|
||
expect(v.ok).toBe(false);
|
||
expect(v.reason).toContain('Task 1');
|
||
expect(v.reason).toContain('Task 3');
|
||
expect(v.reason).not.toContain('Task 2');
|
||
});
|
||
|
||
it('🔴 прибор в `tests=` — это НЕ запрет: проверки затеи гонять и нужно', () => {
|
||
// 🪤 Запрети мы прибор в `tests=` — ни один пункт нельзя было бы померить проверками
|
||
// самой затеи, а именно ими она и меряется. Запрет стоит на ПРАВКЕ файла, а `tests=`
|
||
// говорит, чем ГОНЯТЬ, и файла не трогает.
|
||
const p = '### Task 1: A\n<!-- night: tests=tools/night/supervisor.test.mjs; files=app/A.php -->\n';
|
||
expect(validatePlan(parsePlan(p)).ok).toBe(true);
|
||
});
|
||
});
|