201 lines
13 KiB
JavaScript
201 lines
13 KiB
JavaScript
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');
|
||
});
|
||
});
|