32770b26d2
Находка М-407, правила 247-250. Промт смены 29 велел проверить остальных сторожей на слепоту смены 28 (правило 244). Замер вывел на беду в самом лекарстве. ЧТО ПОЧИНЕНО. `bezPrimechaniy`, заведённый сменой 28 против «за примечание отвечает код», был двумя грубыми заменами подряд. На живом хозяйстве он ЕЛ ЗЕРНО: - `supervisor.mjs` — 53 объявления → 42, строк 2591 → 2344; - `spawn.mjs` — 25 → 19, строк 770 → 614; - `celo-li-hozyaystvo.mjs` — 13 → 11; - `workspace.test.mjs` — строк 2375 → 602. Причина замерена, а не выведена: внутри обычного `//`-примечания `supervisor.mjs` строкой 5 стоит текст `/*.mjs`. Замена читает это как начало блочного примечания и выкусывает 230 строк ЖИВОГО КОДА до первого попавшегося `*/`. ⇒ Сторож правила 235, ради которого рез и заводился, всё это время судил по тексту, из которого выкушено пол-файла. Он был зелёным — но зелёным по изувеченному. Рез переписан с замен на посимвольный разбор — тем же порядком, каким уже работал `tolkoKod` в том же файле: образец → строка → примечание. Отличие одно и ради него рез существует: строковые значения сохраняются. Переносы внутри вырезанного блока возвращаются на место — кто режет перед построчным обходом, иначе получил бы номера строк от другого файла. Доказано вырезанием: вернул прежнее тело — покраснели ровно 4 проверки, посчитанные ДО прогона. Полный прогон после починки: 2939 чужих проверок зелены, ни одна не сломалась. Живого долга за слепотой не нашлось — так и записано, не больше. ДЕСЯТЬ САМОДЕЛЬНЫХ РЕЗОВ СВЕДЕНЫ В ОДИН, включая две полные копии `tolkoKod`. Замер копии из `cli.test.mjs` против общего по 52 рабочим файлам: расходятся на 19. На самом `cli.mjs` копия видела 148 зовов против 152 — четыре (`imyaOkna`, `bolshoyBukvoy`, `randomUUID`, `priyomkaDir`) сторож «точка входа не зовёт имён, которых у неё нет» не проверял вовсе. ЗЕРКАЛЬНАЯ СЛЕПОТА ЗАМЕРЕНА (правило 243, прибором, а не глазами): 33 места, где утверждается «в коде есть X» по сырому тексту. Живой слепоты — НОЛЬ. Зелены по двум причинам разом — 5. Чисто — 19. Все 33 переведены на рез В МЕСТЕ ВОПРОСА, а не в месте чтения: у `shoot.test.mjs` и `reviewer.test.mjs` к одному файлу стоят рядом три разных вопроса, и обёртка чтения превратила бы запрет на боевой адрес в дыру. Заведён сторож на класс `utverzhdenie-o-kode-ne-po-syromu.test.mjs` — он же стережёт единственность реза. Доказан вырезанием: вернул одно место к сырому — покраснела ровно одна проверка и назвала точный адрес. 247. У РЕЗА СПРАШИВАЙ НЕ ТОЛЬКО «ЧТО ОН РЕЖЕТ», НО И «НА ЧЁМ ЕГО МЕРИЛИ». Правило 234 смене 28 было задано — и получило ответ от подложек. Между ответом подложек и правдой живого хозяйства уместилось 11 объявлений из 53. 248. ПОДЛОЖКА, НЕ ВОСПРОИЗВОДЯЩАЯ БЕДУ, ДАЁТ ЗЕЛЁНЫЙ, КОТОРЫЙ НИЧЕГО НЕ ОХРАНЯЕТ. Две мои подложки были зелены на заведомо сломанном резе — не хватало пары знаков. 249. ОДНО ДЕЛО — ОДНО ТЕЛО, И РАСХОЖДЕНИЕ ТЕЛ ВСЕГДА МОЛЧАЛИВОЕ. Уточнение 239. 250. РЕЗ СТАВЯТ ТАМ, ГДЕ ЗАДАЮТ ВОПРОС, А НЕ ТАМ, ГДЕ ЧИТАЮТ ФАЙЛ. Числа: проверок 2940 в 109 файлах (было 2929/108), ноль красных, код 0. Приговор прибора 613 / 29 — состав сверен поимённо, тот же, что у смены 28. Живых рождений ноль. Ошибок надзирателя три, все названы: правил семь мест программой из-под оболочки вместо Edit (то самое правило, о котором предупреждала смена 28; правка дожила, проверено сразу же); подложки были зелены на сломанном; первый вид сторожа поймал сам себя, второй перепрыгивал через отрицание и врал номером строки. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
622 lines
42 KiB
JavaScript
622 lines
42 KiB
JavaScript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||
import { bezPrimechaniy } from './schyot-zovushchih.mjs';
|
||
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, zadachaKonchena,
|
||
} 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(bezPrimechaniy(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']);
|
||
});
|
||
});
|
||
|
||
// ── кусок 7, задача 6: единственное место, где записано «задача кончена» ──────────────
|
||
describe('zadachaKonchena — что значит «задача кончена»', () => {
|
||
it('П97: все пункты плана есть среди засчитанных — кончена, ничего не осталось', () => {
|
||
const r = zadachaKonchena({ items: ['1', '2', '3'], verified: ['1', '2', '3'], otlozhennye: [] });
|
||
expect(r.konchena).toBe(true);
|
||
expect(r.left).toEqual([]);
|
||
});
|
||
|
||
it('П98: один пункт не засчитан — НЕ кончена, и его номер назван', () => {
|
||
// 🔴 Объяви задачу кончённой при незасчитанном пункте — и приёмщик пойдёт снимать
|
||
// недоделанное, а владелец утром получит «сделано» там, где не сделано.
|
||
const r = zadachaKonchena({ items: ['1', '2', '3'], verified: ['1', '3'], otlozhennye: [] });
|
||
expect(r.konchena).toBe(false);
|
||
expect(r.left).toEqual(['2']);
|
||
expect(r.reason).toContain('2');
|
||
});
|
||
|
||
it('П99: засчитано БОЛЬШЕ, чем есть в плане — счёт по пунктам плана, а не по числу строк', () => {
|
||
// 🪤 Чужой пункт в приговорах (от прошлой задачи, от соседа) не должен «добирать»
|
||
// счёт до конца: считать по длине списка значило бы кончить задачу на чужой работе.
|
||
const r = zadachaKonchena({ items: ['1', '2', '3'], verified: ['1', '2', '9', '9'], otlozhennye: [] });
|
||
expect(r.konchena).toBe(false);
|
||
expect(r.left).toEqual(['3']);
|
||
});
|
||
|
||
it('П128: все пункты засчитаны, но один ОТЛОЖЕН — задача НЕ кончена', () => {
|
||
// 🔴 Сторож находки Н8. Отложенный пункт лежит в `done` очереди (чтобы она не выдала
|
||
// его снова), но в приговоры НЕ попадает — значит по паре {items, verified} его
|
||
// не видно вовсе. Без третьего довода правило объявило бы задачу кончённой там,
|
||
// где кусок 4 её кончённой не считает, и перевод его блока на моё правило СЛОМАЛ БЫ
|
||
// чужое: приёмщик пошёл бы по задаче с незакрытым пунктом.
|
||
const r = zadachaKonchena({ items: ['1', '2', '3'], verified: ['1', '2', '3'], otlozhennye: ['2'] });
|
||
expect(r.konchena).toBe(false);
|
||
expect(r.reason).toContain('2');
|
||
// Отдельным доводом — множество отложенных пусто: кончена.
|
||
expect(zadachaKonchena({ items: ['1', '2', '3'], verified: ['1', '2', '3'], otlozhennye: [] }).konchena).toBe(true);
|
||
// И довод принимается множеством тоже — очередь хранит их именно так.
|
||
expect(zadachaKonchena({ items: ['1'], verified: ['1'], otlozhennye: new Set(['1']) }).konchena).toBe(false);
|
||
});
|
||
|
||
it('Э1 (сверх плана): довода отложенных нет вовсе — задача НЕ объявляется кончённой молча', () => {
|
||
// 🔴 Четвёртый вопрос вырезания: сколько разных состояний у этого входа? Их ТРИ —
|
||
// «отложенных нет» (пустой список), «отложенные есть» и «про отложенные не спросили»
|
||
// (довод забыт зовущим). Третье слипается со первым при `?? []`, и тогда старый
|
||
// зовущий, не знающий про новый довод, молча получал бы «кончена» — то есть правка
|
||
// тихо вернула бы ровно ту беду, ради которой третий довод и заведён.
|
||
const r = zadachaKonchena({ items: ['1'], verified: ['1'] });
|
||
expect(r.konchena).toBe(false);
|
||
expect(r.reason).toMatch(/отложенн/i);
|
||
});
|
||
|
||
it('Э2 (сверх плана): номера сравниваются как имена, а не как числа', () => {
|
||
// Пункты зовутся «4.11», «10», «2» — сравнение чисел здесь врёт молча.
|
||
expect(zadachaKonchena({ items: ['4.11', '10'], verified: [4.11, 10], otlozhennye: [] }).konchena).toBe(true);
|
||
expect(zadachaKonchena({ items: ['10'], verified: ['1'], otlozhennye: [] }).konchena).toBe(false);
|
||
});
|
||
|
||
it('Э3 (сверх плана): пунктов в плане нет вовсе — это НЕ «кончена»', () => {
|
||
// 🔴 Пустой план значит «разбор плана дал ноль пунктов», то есть сбой, а не победа.
|
||
// Отдай здесь «кончена» — и приёмщик пойдёт снимать задачу, которой не было.
|
||
const r = zadachaKonchena({ items: [], verified: [], otlozhennye: [] });
|
||
expect(r.konchena).toBe(false);
|
||
expect(r.reason).toMatch(/ни одного пункта|пуст/i);
|
||
});
|
||
});
|