Files
portal/tools/night/items.test.mjs
T
Дмитрий 32770b26d2 fix(ночь): рез примечаний ел живой код — сторож смены 28 судил по изувеченному тексту
Находка М-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>
2026-08-07 07:43:49 +03:00

622 lines
42 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, 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);
});
});