Files
portal/tools/night/plan-model.test.mjs
T

201 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, it, expect } from 'vitest';
import { parsePlan, validatePlan, komandaProverokPunkta } from './plan-model.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('🔴 подан не текст — падает ГРОМКО, а не «в плане нет пунктов»', () => {
// 🪤 Пустышка, тихо отвечающая «всё хорошо», за три куска ловилась пять раз.
// Отдай мы тут пустой список — ворота напечатали бы владельцу «в вашем плане
// нет ни одного пункта», хотя план цел, а сломалось чтение файла.
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('пункт без единой проверки — план не проходит, и виновный НАЗВАН (проверка 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();
});
it('проверки пункта и php-шные, и наши — берётся набор, который умеет и то и другое', () => {
// Смешанный пункт бывает: правка бэкенда плюс правка затеи. `php artisan test`
// наш `.mjs` не запустит вовсе, поэтому смешанный набор идёт нашим прогоном.
expect(komandaProverokPunkta({ id: '4', tests: ['app/tests/Feature/A.php', 'tools/night/b.test.mjs'] }))
.toBe('npm run test:tools -- app/tests/Feature/A.php tools/night/b.test.mjs');
});
});