555 lines
36 KiB
JavaScript
555 lines
36 KiB
JavaScript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||
import { mkdtempSync, rmSync, mkdirSync, writeFileSync, existsSync, readFileSync } from 'node:fs';
|
||
import { tmpdir } from 'node:os';
|
||
import { join } from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
import {
|
||
readCommand, consumeCommand, unknownCommands,
|
||
closeItem, afterItemDone, afterTaskDone,
|
||
deferredTaskGate, reshyonnyeSNevypolnimymi,
|
||
} from './items.mjs';
|
||
|
||
let dir;
|
||
beforeEach(() => {
|
||
dir = mkdtempSync(join(tmpdir(), 'night-cmd-'));
|
||
});
|
||
afterEach(() => { rmSync(dir, { recursive: true, force: true }); });
|
||
|
||
// Положить команду в каталог. Ровно так это делает панель, бот или рука владельца.
|
||
function polozhit(name, body = { at: 1 }) {
|
||
mkdirSync(dir, { recursive: true });
|
||
writeFileSync(join(dir, name), JSON.stringify(body), 'utf8');
|
||
}
|
||
|
||
describe('readCommand — что владелец велел этому работнику', () => {
|
||
it('каталога команд нет вовсе — не падаем, отвечаем «команды нет»', () => {
|
||
// 🪤 Каталог `.night/commands` заводит рука владельца, и в первую ночь его может
|
||
// не быть. Брошенная тут ошибка убила бы надзирателя молча — тот самый класс беды.
|
||
expect(readCommand(join(dir, 'нет-такого'), 'w-1')).toBeNull();
|
||
});
|
||
|
||
it('каталог есть, но пуст — работаем дальше', () => {
|
||
mkdirSync(dir, { recursive: true });
|
||
expect(readCommand(dir, 'w-1')).toBeNull();
|
||
});
|
||
|
||
it('положили личную «паузу» — работник её видит, и видно, что она личная', () => {
|
||
polozhit('w-1.pause.json');
|
||
const cmd = readCommand(dir, 'w-1');
|
||
expect(cmd.cmd).toBe('pause');
|
||
expect(cmd.common).toBe(false);
|
||
expect(cmd.file).toBe('w-1.pause.json');
|
||
});
|
||
|
||
it('чужую личную команду работник НЕ видит', () => {
|
||
polozhit('w-2.stop.json');
|
||
expect(readCommand(dir, 'w-1')).toBeNull();
|
||
});
|
||
|
||
it('«стоп всё» видит любой работник, не только адресат', () => {
|
||
polozhit('all.stopall.json');
|
||
const cmd = readCommand(dir, 'w-9');
|
||
expect(cmd.cmd).toBe('stopall');
|
||
expect(cmd.common).toBe(true);
|
||
});
|
||
|
||
it('«стоп всё» главнее личной паузы — отдаётся первой', () => {
|
||
// 🔴 Порядок не украшение: получи работник сперва паузу, он доделывал бы шаг
|
||
// до двадцати минут, пока владелец ждёт остановки всего прогона.
|
||
polozhit('all.stopall.json');
|
||
polozhit('w-1.pause.json');
|
||
expect(readCommand(dir, 'w-1').cmd).toBe('stopall');
|
||
});
|
||
|
||
it('слово не из закрытого списка не читается вовсе — мы не выдумываем, что оно значит', () => {
|
||
polozhit('w-1.udali-vsyo.json');
|
||
expect(readCommand(dir, 'w-1')).toBeNull();
|
||
});
|
||
|
||
it('файл команды порван — команда всё равно прочитана, тело просто пустое', () => {
|
||
// 🪤 Порванный файл — это отсутствие сведений, а не повод уронить надзирателя.
|
||
// Слово «стоп» стоит в самом имени файла, и его достаточно.
|
||
mkdirSync(dir, { recursive: true });
|
||
writeFileSync(join(dir, 'w-1.stop.json'), '{ это не json', 'utf8');
|
||
expect(readCommand(dir, 'w-1').cmd).toBe('stop');
|
||
});
|
||
|
||
it('тело команды доезжает целиком — вместе с тем, кто и когда её отдал', () => {
|
||
polozhit('w-1.stop.json', { at: 777, by: 'владелец' });
|
||
const cmd = readCommand(dir, 'w-1');
|
||
expect(cmd.at).toBe(777);
|
||
expect(cmd.by).toBe('владелец');
|
||
});
|
||
|
||
it('🔴 тело файла НЕ перебивает слово из его имени — иначе закрытый список обходится', () => {
|
||
// 🔴 Положи владелец (или кто угодно) в файл «паузы» тело {"cmd":"stopall"} — и работник
|
||
// был бы погашен, хотя имя файла говорит «пауза». Слово команды берётся ТОЛЬКО из имени
|
||
// файла: имя — это и есть закрытый список. Тело несёт сведения, а не приказ.
|
||
polozhit('w-1.pause.json', { at: 1, cmd: 'stopall', common: true, file: 'подделка' });
|
||
const cmd = readCommand(dir, 'w-1');
|
||
expect(cmd.cmd).toBe('pause');
|
||
expect(cmd.common).toBe(false);
|
||
expect(cmd.file).toBe('w-1.pause.json');
|
||
});
|
||
});
|
||
|
||
describe('unknownCommands — владелец узнаёт, что его команду НЕ поняли', () => {
|
||
it('непонятое слово НАЗЫВАЕТСЯ — с именем файла и самим словом, и файл остаётся лежать', () => {
|
||
// 🔴🔴 Решение диспетчера. Прежде не происходило ничего: владелец думал, что «пауза»
|
||
// нажата, а её никто не понял, и файл лежал до утра молча.
|
||
polozhit('w-1.udali-vsyo.json');
|
||
const spisok = unknownCommands(dir, 'w-1');
|
||
expect(spisok.length).toBe(1);
|
||
expect(spisok[0].word).toBe('udali-vsyo');
|
||
expect(spisok[0].file).toBe('w-1.udali-vsyo.json');
|
||
// Не съедено молча: файл владельца на месте, он и есть доказательство.
|
||
expect(existsSync(join(dir, 'w-1.udali-vsyo.json'))).toBe(true);
|
||
});
|
||
|
||
it('общее непонятое слово тоже называется — «всем» касается и этого работника', () => {
|
||
polozhit('all.vsyo-brosit.json');
|
||
const spisok = unknownCommands(dir, 'w-9');
|
||
expect(spisok.length).toBe(1);
|
||
expect(spisok[0].common).toBe(true);
|
||
});
|
||
|
||
it('🔴 понятные команды и чужие файлы в этот список НЕ попадают', () => {
|
||
// 🔴 Страховка от прибора, который называет непонятым ВСЁ подряд: владелец получал бы
|
||
// строку «не понял» на каждую исправную команду и перестал бы читать сводку вовсе.
|
||
polozhit('w-1.pause.json'); // понятная — молчим
|
||
polozhit('all.stopall.json'); // понятная общая — молчим
|
||
polozhit('w-2.udali-vsyo.json'); // чужая личная — о ней скажет надзиратель w-2
|
||
polozhit('zametka.txt'); // вообще не команда
|
||
expect(unknownCommands(dir, 'w-1')).toEqual([]);
|
||
});
|
||
|
||
it('каталога команд нет вовсе — пустой список, и не падаем', () => {
|
||
expect(unknownCommands(join(dir, 'нет-такого'), 'w-1')).toEqual([]);
|
||
});
|
||
});
|
||
|
||
describe('consumeCommand — личную убираем сразу, общую только когда исполнили ВСЕ', () => {
|
||
it('прочитанная ЛИЧНАЯ команда убирается, чтобы не сработать дважды', () => {
|
||
polozhit('w-1.stop.json');
|
||
const r = consumeCommand(dir, 'w-1', 'stop');
|
||
expect(r.personal_removed).toBe(true);
|
||
expect(readCommand(dir, 'w-1')).toBeNull();
|
||
});
|
||
|
||
it('личной команды не было — так и сказано, и ничего лишнего не тронуто', () => {
|
||
polozhit('all.pause.json');
|
||
const r = consumeCommand(dir, 'w-1', 'stop');
|
||
expect(r.personal_removed).toBe(false);
|
||
expect(existsSync(join(dir, 'all.pause.json'))).toBe(true);
|
||
});
|
||
|
||
it('«стоп всё» НЕ съедается первым прочитавшим — её видят все семеро', () => {
|
||
// 🪤 Ровно эта беда и была: первый исполнивший стирал файл, шестеро работали дальше.
|
||
polozhit('all.stopall.json');
|
||
const semero = ['w-1', 'w-2', 'w-3', 'w-4', 'w-5', 'w-6', 'w-7'];
|
||
for (const w of semero.slice(0, 6)) {
|
||
expect(readCommand(dir, w).cmd).toBe('stopall');
|
||
consumeCommand(dir, w, 'stopall', { concerns: semero });
|
||
}
|
||
// Седьмой всё ещё видит команду — она его тоже касается.
|
||
expect(readCommand(dir, 'w-7').cmd).toBe('stopall');
|
||
const r = consumeCommand(dir, 'w-7', 'stopall', { concerns: semero });
|
||
// Исполнили все — только теперь файла нет.
|
||
expect(r.common_removed).toBe(true);
|
||
expect(readCommand(dir, 'w-7')).toBeNull();
|
||
expect(existsSync(join(dir, 'all.stopall.json'))).toBe(false);
|
||
});
|
||
|
||
it('исполнившие копятся в САМОМ ФАЙЛЕ, а не в памяти одного надзирателя', () => {
|
||
// 🔴 Надзиратели — разные процессы. Держи мы список исполнивших в памяти, каждый
|
||
// видел бы свой собственный, и общая команда не убралась бы никогда.
|
||
polozhit('all.stopall.json');
|
||
consumeCommand(dir, 'w-1', 'stopall', { concerns: ['w-1', 'w-2'] });
|
||
const body = JSON.parse(readFileSync(join(dir, 'all.stopall.json'), 'utf8'));
|
||
expect(body.done).toEqual(['w-1']);
|
||
});
|
||
|
||
it('исполнивший общую команду второй раз её не получает — иначе гасился бы по ней вечно', () => {
|
||
polozhit('all.stopall.json');
|
||
consumeCommand(dir, 'w-1', 'stopall', { concerns: ['w-1', 'w-2'] });
|
||
expect(readCommand(dir, 'w-1')).toBeNull();
|
||
expect(readCommand(dir, 'w-2').cmd).toBe('stopall');
|
||
});
|
||
|
||
it('состав не назван — общий файл НЕ удаляется: лучше промолчать, чем снять команду с тех, до кого она не дошла', () => {
|
||
// 🔴 Это дословное правило договора о стыках. Убери его — и первый же надзиратель,
|
||
// не сумевший прочитать `run.json`, снял бы «стоп всё» со всего прогона.
|
||
polozhit('all.stopall.json');
|
||
const r = consumeCommand(dir, 'w-1', 'stopall', { concerns: [] });
|
||
expect(r.common_removed).toBe(false);
|
||
expect(existsSync(join(dir, 'all.stopall.json'))).toBe(true);
|
||
});
|
||
|
||
it('исполнение ЛИЧНОЙ команды не съедает общую, и наоборот', () => {
|
||
polozhit('all.pause.json');
|
||
polozhit('w-1.stop.json');
|
||
consumeCommand(dir, 'w-1', 'stop', { concerns: ['w-1', 'w-2'] });
|
||
expect(readCommand(dir, 'w-2').cmd).toBe('pause');
|
||
expect(readCommand(dir, 'w-1').cmd).toBe('pause');
|
||
});
|
||
});
|
||
|
||
// ── Кусок 4, задача 2: закрытие пункта и очередь ───────────────────────────────
|
||
|
||
describe('closeItem — «пункт закрыт» это доказательство, а не слово (проверки 38, 39б, 40)', () => {
|
||
const item = { id: '4.11', tests: ['tools/night/a.test.mjs'] };
|
||
|
||
it('коммит есть и проверки этого пункта зелёные — пункт закрыт', () => {
|
||
const r = closeItem({ item, commit_sha: 'abc123', tests_green: true, on_assumption: false });
|
||
expect(r.closed).toBe(true);
|
||
expect(r.reason).toBeNull();
|
||
});
|
||
|
||
it('проверки не зелёные — НЕ засчитан, и названо, какие именно (проверка 38)', () => {
|
||
const r = closeItem({ item, commit_sha: 'abc123', tests_green: false, on_assumption: false });
|
||
expect(r.closed).toBe(false);
|
||
expect(r.reason).toContain('tools/night/a.test.mjs');
|
||
});
|
||
|
||
it('🔴 tests_green не передан вовсе — НЕ засчитан, а не «наверное зелено»', () => {
|
||
// 🪤 «Успех» с пустым ответом — не успех. Прогон проверок мог не состояться вовсе
|
||
// (программа не найдена, вышло время), и молчаливое «наверное зелено» тут страшнее
|
||
// обвала: пункт засчитался бы, ни разу не будучи проверенным.
|
||
const r = closeItem({ item, commit_sha: 'abc123' });
|
||
expect(r.closed).toBe(false);
|
||
});
|
||
|
||
it('коммита нет — НЕ засчитан, сколько бы зелени ни было', () => {
|
||
const r = closeItem({ item, commit_sha: null, tests_green: true, on_assumption: false });
|
||
expect(r.closed).toBe(false);
|
||
expect(r.reason).toMatch(/не сохранен/i);
|
||
});
|
||
|
||
it('у пункта в плане не записано ни одной проверки — закрыть нельзя (проверка 39б)', () => {
|
||
const r = closeItem({ item: { id: 'x', tests: [] }, commit_sha: 'abc123', tests_green: true });
|
||
expect(r.closed).toBe(false);
|
||
expect(r.reason).toMatch(/ни одной проверки/i);
|
||
});
|
||
|
||
it('пункт «в расчёте на ответ», ответа нет — закрыть нельзя, хоть всё зелено (проверка 40)', () => {
|
||
const r = closeItem({ item, commit_sha: 'abc123', tests_green: true, on_assumption: true, answer_received: false });
|
||
expect(r.closed).toBe(false);
|
||
expect(r.reason).toMatch(/в расчёте на ответ/i);
|
||
});
|
||
|
||
it('ответ владельца пришёл — тот же пункт закрывается (проверка 40)', () => {
|
||
const r = closeItem({ item, commit_sha: 'abc123', tests_green: true, on_assumption: true, answer_received: true });
|
||
expect(r.closed).toBe(true);
|
||
});
|
||
|
||
it('🔴 «в расчёте на ответ» названо РАНЬШЕ красных проверок — иначе чинят не то', () => {
|
||
// 🔴 Владелец и работник читают ПЕРВУЮ названную причину. Скажи мы «проверки красные»
|
||
// там, где на деле не пришёл ответ владельца, — работник полночи чинил бы код,
|
||
// который и так верен, а ждать надо было слова хозяина.
|
||
const r = closeItem({ item, commit_sha: 'abc123', tests_green: false, on_assumption: true, answer_received: false });
|
||
expect(r.reason).toMatch(/в расчёте на ответ/i);
|
||
expect(r.reason).not.toMatch(/зелёные/i);
|
||
});
|
||
|
||
it('🔴 пункт не передан вовсе — падает ГРОМКО: это сбой зова, а не свойство пункта', () => {
|
||
expect(() => closeItem({ commit_sha: 'abc123', tests_green: true })).toThrow(/пункт/i);
|
||
});
|
||
|
||
it('🪤 ни один отказ не остаётся БЕЗ причины — работнику кладут её в ящик', () => {
|
||
// 🪤 Пустая причина в ящике — это «не засчитано, а почему — угадай». Проверяем
|
||
// три отказных случая, не связанных с зеленью: нет коммита, нет проверок в плане,
|
||
// «в расчёте на ответ».
|
||
const sluchai = [
|
||
closeItem({ item, commit_sha: null, tests_green: true }),
|
||
closeItem({ item: { id: 'x', tests: [] }, commit_sha: 'abc123', tests_green: true }),
|
||
closeItem({ item, commit_sha: 'abc123', tests_green: true, on_assumption: true, answer_received: false }),
|
||
];
|
||
for (const r of sluchai) {
|
||
expect(typeof r.reason).toBe('string');
|
||
expect(r.reason.length).toBeGreaterThan(10);
|
||
}
|
||
});
|
||
});
|
||
|
||
describe('afterItemDone — закрыл пункт, взял следующий того же плана (проверка 24)', () => {
|
||
const items = [{ id: '1' }, { id: '2' }, { id: '3' }];
|
||
|
||
it('закрыл пункт — назван следующий пункт ТОГО ЖЕ плана', () => {
|
||
const r = afterItemDone({ items, done: ['1'], answered_during_day: false });
|
||
expect(r.next_item.id).toBe('2');
|
||
expect(r.extinguish_reason).toBeNull();
|
||
});
|
||
|
||
it('пункты плана кончились — брать нечего, и это СКАЗАНО', () => {
|
||
const r = afterItemDone({ items, done: ['1', '2', '3'], answered_during_day: false });
|
||
expect(r.next_item).toBeNull();
|
||
expect(r.extinguish_reason).toMatch(/кончились/i);
|
||
});
|
||
|
||
it('очередь идёт по порядку ПЛАНА, а не по порядку закрытия', () => {
|
||
// Закрыт средний пункт — следующим берётся первый незакрытый по плану, а не четвёртый.
|
||
expect(afterItemDone({ items, done: ['2'], answered_during_day: false }).next_item.id).toBe('1');
|
||
});
|
||
|
||
it('🪤 номер пункта числом, а не строкой — всё равно узнан закрытым', () => {
|
||
// 🪤 Владелец пишет в плане то `2`, то `"2"`. Сравнивай мы знак в знак — закрытый
|
||
// пункт выдавался бы работнику снова, и он крутился бы на нём до утра.
|
||
const spisok = [{ id: 1 }, { id: 2 }];
|
||
expect(afterItemDone({ items: spisok, done: ['1'], answered_during_day: false }).next_item.id).toBe(2);
|
||
});
|
||
|
||
it('дождался ответа днём — доделал свой пункт и гаснет, нового не берёт', () => {
|
||
const r = afterItemDone({ items, done: ['1'], answered_during_day: true });
|
||
expect(r.next_item).toBeNull();
|
||
expect(r.extinguish_reason).toMatch(/днём/i);
|
||
});
|
||
|
||
it('дождался ответа днём — сказано, что остаток плана сам не подхватится', () => {
|
||
const r = afterItemDone({ items, done: ['1'], answered_during_day: true });
|
||
expect(r.task_note).toMatch(/только по слову владельца/i);
|
||
});
|
||
|
||
it('🔴 items не список — падает ГРОМКО, а не «пункты кончились»', () => {
|
||
// 🔴 Тихий ответ «кончились» погасил бы всех работников задачи на ровном месте,
|
||
// и владелец прочёл бы утром «работа сделана», когда её никто не делал.
|
||
expect(() => afterItemDone({ items: null, done: [], answered_during_day: false })).toThrow(/список пунктов/i);
|
||
});
|
||
|
||
it('🔴 done не передан — падает ГРОМКО, а не «ничего не закрыто»', () => {
|
||
// 🔴 «Ничего не закрыто» вернуло бы работнику самый первый пункт плана — тот,
|
||
// который он только что и закрыл.
|
||
expect(() => afterItemDone({ items, answered_during_day: false })).toThrow(/закрыт/i);
|
||
});
|
||
});
|
||
|
||
describe('afterTaskDone — задача кончилась, следующая не начинается (проверка 25)', () => {
|
||
it('все работники задачи гаснут — перечислены поимённо', () => {
|
||
const r = afterTaskDone({ task_id: 't-1', workers: ['w-1', 'w-2'] });
|
||
expect(r.task_id).toBe('t-1');
|
||
expect(r.extinguish_workers).toEqual(['w-1', 'w-2']);
|
||
});
|
||
|
||
it('🔴 следующая задача в том же прогоне НЕ начинается', () => {
|
||
expect(afterTaskDone({ task_id: 't-1', workers: ['w-1'] }).start_next_task).toBe(false);
|
||
});
|
||
|
||
it('🔴 поднят флаг «пора приёмщику», и сказано, что заводит его кусок 7', () => {
|
||
// 🟡 Здесь закрыта ЧАСТЬ проверки 25: флаг поднят и ляжет в `run.json` (задача 5).
|
||
// Самого приёмщика по этому флагу заводит кусок 7 — тут его заводить нечем.
|
||
const r = afterTaskDone({ task_id: 't-1', workers: ['w-1'] });
|
||
expect(r.reviewer_starts).toBe(true);
|
||
expect(r.note).toMatch(/приём/i);
|
||
});
|
||
|
||
it('🔴 работников не дали — падает ГРОМКО: гасить некого, это сбой зова', () => {
|
||
// 🪤 Тихий пустой список прочитался бы как «все погашены», и задача считалась бы
|
||
// законченной при живых работниках.
|
||
expect(() => afterTaskDone({ task_id: 't-1', workers: [] })).toThrow(/работник/i);
|
||
});
|
||
});
|
||
|
||
// ── Граница двух дверей: кто ВЫДАЁТ пункт, а кто только говорит «есть ли что брать» ──
|
||
//
|
||
// 🔴🔴 Эти проверки не ищут поломку — они ДЕРЖАТ ДОГОВОРЁННОСТЬ. Словами она записана
|
||
// над `afterItemDone` в `items.mjs`, но слова в коде переживают ровно до первого, кто
|
||
// их не прочтёт. Здесь она замерена числами: на ОДНОМ И ТОМ ЖЕ хозяйстве две работы
|
||
// отвечают РАЗНОЕ, и это нарочно.
|
||
//
|
||
// 🪤 Сторож двусторонний. Он покраснеет и тогда, когда `nextFreeItem` разучится смотреть
|
||
// на занятые файлы (обе двери начнут отвечать «1»), и тогда, когда `afterItemDone`
|
||
// НАУЧАТ занятиям (обе начнут отвечать «2»). Второе — не поломка, а смена решения:
|
||
// покраснев, сторож отправит правщика к владельцу, а не даст поменять подписи молча.
|
||
// Подписи обеих работ ждёт задача 5.
|
||
describe('граница двух дверей — «выдать пункт» и «есть ли что брать» это РАЗНЫЕ работы', () => {
|
||
const punkty = [
|
||
{ id: '1', files: ['app/a.php'] },
|
||
{ id: '2', files: ['app/b.php'] },
|
||
];
|
||
|
||
it('🔴 сосед сидит на пункте 1: выдающая дверь даёт 2, спрашивающая говорит 1', async () => {
|
||
const { claimFiles, nextFreeItem } = await import('./file-claims.mjs');
|
||
claimFiles(dir, 'w-1', ['app/a.php']);
|
||
|
||
// Дверь, которая ВЫДАЁТ: занятые файлы соседа она видит и пункт 1 пропускает.
|
||
expect(nextFreeItem(dir, 'w-2', punkty, { done: [] }).item.id).toBe('2');
|
||
|
||
// Дверь, которая только СПРАШИВАЕТ «есть ли что брать»: занятий она не знает вовсе
|
||
// и называет пункт, на котором сидит сосед. Брать этот ответ за выдачу пункта нельзя.
|
||
expect(afterItemDone({ items: punkty, done: [] }).next_item.id).toBe('1');
|
||
});
|
||
|
||
it('🔴 занятия ей передать НЕЧЕМ — лишние сведения она молча не примет', async () => {
|
||
// 🔴 Ровно тут прячется соблазн: «да передам ей каталог прогона, и всё сойдётся».
|
||
// Пока в её подписи такого входа нет, ответ от него не меняется ни на знак.
|
||
// Появится — эта проверка покраснеет, и разговор с владельцем состоится ДО правки.
|
||
const { claimFiles } = await import('./file-claims.mjs');
|
||
claimFiles(dir, 'w-1', ['app/a.php']);
|
||
const sLishnim = afterItemDone({ items: punkty, done: [], dirPath: dir, workerId: 'w-2' });
|
||
const bezLishnego = afterItemDone({ items: punkty, done: [] });
|
||
expect(sLishnim.next_item.id).toBe(bezLishnego.next_item.id);
|
||
expect(sLishnim.next_item.id).toBe('1');
|
||
});
|
||
});
|
||
|
||
// ── Кусок 4, задача 3: ворота возврата отложенной задачи ───────────────────────
|
||
|
||
describe('deferredTaskGate — отложенная задача сама не подхватывается (проверки 28, 29, 30)', () => {
|
||
it('🔴 не влезла в прогон, слова владельца нет — В РАБОТУ НЕ ИДЁТ (проверка 28)', () => {
|
||
const r = deferredTaskGate({ reason: 'not_finished', owner_said_continue: false, plan_revalidated: true });
|
||
expect(r.may_run).toBe(false);
|
||
expect(r.reason).toMatch(/продолжай/i);
|
||
});
|
||
|
||
it('🔴 не прошла приёмку, слова владельца нет — тоже не идёт (проверка 29а)', () => {
|
||
const r = deferredTaskGate({ reason: 'rejected', owner_said_continue: false, plan_revalidated: true, defect_list: ['кнопка не там'] });
|
||
expect(r.may_run).toBe(false);
|
||
});
|
||
|
||
it('🔴 слово владельца есть, а план заново НЕ проверен — ещё нельзя (проверка 30)', () => {
|
||
// 🔴 Пока задача лежала, продукт менялся. План, годный вчера, сегодня может
|
||
// не годиться вовсе — и работник всю ночь делал бы по нему не то.
|
||
const r = deferredTaskGate({ reason: 'not_finished', owner_said_continue: true, plan_revalidated: false });
|
||
expect(r.may_run).toBe(false);
|
||
expect(r.reason).toMatch(/заново/i);
|
||
});
|
||
|
||
it('слово владельца есть и план перепроверен — можно (проверки 28 и 30, случай «не влезла»)', () => {
|
||
const r = deferredTaskGate({ reason: 'not_finished', owner_said_continue: true, plan_revalidated: true });
|
||
expect(r.may_run).toBe(true);
|
||
expect(r.reason).toBeNull();
|
||
});
|
||
|
||
it('вернулась с приёмки со списком брака — можно, и делает её НОВЫЙ работник (проверка 29г)', () => {
|
||
const r = deferredTaskGate({ reason: 'rejected', owner_said_continue: true, plan_revalidated: true, defect_list: ['кнопка не там'] });
|
||
expect(r.may_run).toBe(true);
|
||
expect(r.fresh_worker_required).toBe(true);
|
||
});
|
||
|
||
it('🔴 вернулась с приёмки, а списка брака нет — нельзя (проверка 29в)', () => {
|
||
const r = deferredTaskGate({ reason: 'rejected', owner_said_continue: true, plan_revalidated: true, defect_list: [] });
|
||
expect(r.may_run).toBe(false);
|
||
expect(r.reason).toMatch(/список брака/i);
|
||
});
|
||
|
||
it('«не влезла в прогон» списка брака НЕ требует — брака и не было', () => {
|
||
// 🪤 Спроси мы список брака у задачи, которая просто не успела, — владелец не смог бы
|
||
// продолжить её вовсе: назвать ему нечего.
|
||
expect(deferredTaskGate({ reason: 'not_finished', owner_said_continue: true, plan_revalidated: true, defect_list: [] }).may_run).toBe(true);
|
||
});
|
||
|
||
it('🔴 разрешено — список брака отдан ДАЛЬШЕ тем же составом', () => {
|
||
// 🔴 Иначе брак до работника не дойдёт: ворота его прочли, кивнули и потеряли,
|
||
// а работник получил бы «тот же план» без единого слова о том, что в нём не так.
|
||
const r = deferredTaskGate({ reason: 'rejected', owner_said_continue: true, plan_revalidated: true, defect_list: ['кнопка не там', 'цвет не тот'] });
|
||
expect(r.defect_list).toEqual(['кнопка не там', 'цвет не тот']);
|
||
});
|
||
|
||
it('🔴 отказано — нового работника заводить НЕ велено', () => {
|
||
const r = deferredTaskGate({ reason: 'not_finished', owner_said_continue: false, plan_revalidated: true });
|
||
expect(r.fresh_worker_required).toBe(false);
|
||
});
|
||
|
||
it('🔴 вид случая не из закрытого списка — падает ГРОМКО, а не «наверное можно»', () => {
|
||
// 🔴 Испорченный или чужой след не должен молча превратиться в «это не rejected,
|
||
// значит списка брака не спрашиваем».
|
||
expect(() => deferredTaskGate({ reason: 'ne-vlezla', owner_said_continue: true, plan_revalidated: true })).toThrow(/not_finished|rejected/);
|
||
});
|
||
|
||
it('🔴🔴 план НЕ ТОТ, по которому задачу откладывали — не идёт, и оба пути названы (проверка 29б)', () => {
|
||
// 🔴🔴 Проверка 29 требует дословно: задача возвращается «с тем же планом». Без этого
|
||
// «продолжай» владельца, сказанное про одну работу, молча пустило бы в дело другую:
|
||
// доделывалось бы НЕ то, что не доделали, и заметить это по зелёным проверкам нельзя.
|
||
const r = deferredTaskGate({
|
||
reason: 'rejected', owner_said_continue: true, plan_revalidated: true,
|
||
defect_list: ['кнопка не там'],
|
||
plan_path: 'docs/superpowers/plans/старый.md',
|
||
plan_path_now: 'docs/superpowers/plans/совсем-другой.md',
|
||
});
|
||
expect(r.may_run).toBe(false);
|
||
expect(r.reason).toMatch(/план не тот/i);
|
||
expect(r.reason).toContain('docs/superpowers/plans/старый.md');
|
||
expect(r.reason).toContain('docs/superpowers/plans/совсем-другой.md');
|
||
// 🪤 Тот же план, записанный с другой косой чертой, — это ТОТ ЖЕ план.
|
||
expect(deferredTaskGate({
|
||
reason: 'not_finished', owner_said_continue: true, plan_revalidated: true,
|
||
plan_path: 'docs\\superpowers\\plans\\один.md',
|
||
plan_path_now: 'docs/superpowers/plans/один.md',
|
||
}).may_run).toBe(true);
|
||
});
|
||
});
|
||
|
||
// ── Отступление от плана задачи 3, названное вслух и замеренное ────────────────
|
||
//
|
||
// 🔴🔴 План велел завести в `items.mjs` СВОЮ мелочь сравнения пути (сводила только косые
|
||
// черты). Не завёл: тот же вопрос «это один и тот же путь?» в затее уже решает `samePath`
|
||
// из `paths.mjs`, и на второй его копии там стоят два сторожа. Ниже — не чистота ради
|
||
// чистоты, а замер: своя мелочь плана ОТКАЗАЛА БЫ ВЛАДЕЛЬЦУ на его же плане.
|
||
describe('🔴 «тот же план» меряется общим сравнивателем затеи, а не своей мелочью', () => {
|
||
const PLAN_OT_KORNYA = 'docs/superpowers/plans/один.md';
|
||
const PLAN_POLNYY = `${process.cwd().replace(/\\/g, '/')}/docs/superpowers/plans/один.md`;
|
||
|
||
it('🔴🔴 в следе путь ОТ КОРНЯ, а владелец запустил ПОЛНЫМ путём — это ТОТ ЖЕ план', () => {
|
||
// 🔴 Случай живой, а не выдуманный: примечание к `samePath` в `paths.mjs` прямо
|
||
// говорит, что владелец набирает полный путь (`night run C:\…\план.md`), а в след
|
||
// ложится путь от корня хранилища. Сведи мы одни косые — ворота ответили бы
|
||
// «план не тот» на ОДНОМ И ТОМ ЖЕ плане: отложенная задача не вернулась бы НИКОГДА,
|
||
// и владелец не смог бы починить это ничем. Защита от подмены плана съела бы
|
||
// сам возврат.
|
||
expect(deferredTaskGate({
|
||
reason: 'not_finished', owner_said_continue: true, plan_revalidated: true,
|
||
plan_path: PLAN_OT_KORNYA, plan_path_now: PLAN_POLNYY,
|
||
}).may_run).toBe(true);
|
||
});
|
||
|
||
it('🔴 «./план.md» и «план.md» — тоже один план', () => {
|
||
expect(deferredTaskGate({
|
||
reason: 'not_finished', owner_said_continue: true, plan_revalidated: true,
|
||
plan_path: PLAN_OT_KORNYA, plan_path_now: `./${PLAN_OT_KORNYA}`,
|
||
}).may_run).toBe(true);
|
||
});
|
||
|
||
it('🔴 и при этом РАЗНЫЕ планы по-прежнему разводятся — защита не размякла', () => {
|
||
// 🪤 Обратная половина: «сделай посвободнее» лечится тем же движением, каким ломается.
|
||
// Полный путь к ДРУГОМУ плану обязан остаться другим планом.
|
||
expect(deferredTaskGate({
|
||
reason: 'not_finished', owner_said_continue: true, plan_revalidated: true,
|
||
plan_path: PLAN_OT_KORNYA,
|
||
plan_path_now: `${process.cwd().replace(/\\/g, '/')}/docs/superpowers/plans/совсем-другой.md`,
|
||
}).may_run).toBe(false);
|
||
});
|
||
|
||
it('🔴 своего сравнивателя пути в `items.mjs` не заведено — он ВВЕЗЁН', () => {
|
||
// 🔴 Сторож на класс «две работы про одно правило разъезжаются молча». Такой же
|
||
// стоит в `paths.test.mjs`; здесь он смотрит с другой стороны — на зовущего.
|
||
//
|
||
// 🪤 Первый вид этого сторожа ловил только ИМЯ (`function odinVidPuti(`) и на живом
|
||
// вырезании 02.08.2026 остался ЗЕЛЁНЫМ: свою мелочь вписали безымянной строкой прямо
|
||
// в теле ворот, и сторож её не увидел. Значит ловим ТЕЛО — само приведение косых, —
|
||
// как это сделано у сторожа в `paths.test.mjs`.
|
||
const tekst = readFileSync(fileURLToPath(new URL('./items.mjs', import.meta.url)), 'utf8');
|
||
expect(tekst).toMatch(/import \{ samePath \} from '\.\/paths\.mjs';/);
|
||
expect(tekst).not.toMatch(/function\s+odinVidPuti\s*\(/);
|
||
expect(tekst).not.toMatch(/function\s+same\w*Path\s*\(/);
|
||
expect(tekst).not.toContain(String.raw`replace(/\\/g, '/')`);
|
||
});
|
||
});
|
||
|
||
// ── Долг куска 5 (Р14) и куска 7 (блок 6а), внесён 01.08 ───────────────────────
|
||
|
||
describe('reshyonnyeSNevypolnimymi — невыполнимый пункт считается решённым, а не висящим', () => {
|
||
it('невыполнимый пункт входит в решённые наравне с засчитанным', () => {
|
||
expect(reshyonnyeSNevypolnimymi(['1', '2'], ['3'])).toEqual(expect.arrayContaining(['1', '2', '3']));
|
||
expect(reshyonnyeSNevypolnimymi(['1', '2'], ['3'])).toHaveLength(3);
|
||
});
|
||
|
||
it('список невыполнимых пуст — ответ совпадает с одними засчитанными', () => {
|
||
expect(reshyonnyeSNevypolnimymi(['1', '2'], [])).toEqual(['1', '2']);
|
||
});
|
||
|
||
it('🪤 один и тот же пункт числом и строкой в разных списках — не задваивается', () => {
|
||
// 🪤 Владелец пишет план то числом, то строкой (та же беда, что у `afterItemDone`).
|
||
// Сравни мы знак в знак — один и тот же пункт попал бы в решённые дважды.
|
||
expect(reshyonnyeSNevypolnimymi([1, '2'], ['2', '3'])).toEqual(['1', '2', '3']);
|
||
});
|
||
});
|