feat(ночь): конец ночи сам кладёт сводку владельцу; штатный работник больше не подан как поломка
This commit is contained in:
+5
-12
@@ -1723,18 +1723,11 @@ export async function main(argv = process.argv, deps = {}) {
|
||||
console.error(`Почему — написано в карточках работников (поле finish_reason): ${runsDir()}`);
|
||||
console.error('Чаще всего это два пункта плана на один и тот же файл либо несобравшийся composer/npm в углу.');
|
||||
console.error('Прогон закрыт сразу — места в потолке задач он не занимает, и следующая попытка ничем не заперта.');
|
||||
// 🔴🔴 НОЧЬ ЗДЕСЬ УЖЕ КОНЧИЛАСЬ, И ВЛАДЕЛЕЦ ОБЯЗАН ПОЛУЧИТЬ БУМАГУ. Ни один работник
|
||||
// не завёлся — надзирателей нет, будить утро некому, а почта отменена. Не положи её
|
||||
// здесь — владелец не узнает о провале НИЧЕГО, пока живой человек не наберёт команду
|
||||
// руками. Ровно этот случай был живьём в ночь с 3 на 4 августа.
|
||||
// 🪤 В успешной ветке ниже бумага НЕ кладётся нарочно: там работники ЖИВЫ, ночь идёт,
|
||||
// и сводка сказала бы «не сделано ничего» про ещё не сделанную работу. Это было бы
|
||||
// враньё, а не забота.
|
||||
const diskR = await measureDisk(cfg);
|
||||
const utroR = polozhitUtro({ disk_free_gb: typeof diskR?.free_gb === 'number' ? Number(diskR.free_gb.toFixed(1)) : null });
|
||||
console.error(utroR.ok
|
||||
? `Сводка владельцу записана: ${utroR.put}`
|
||||
: `Сводку владельцу записать НЕ удалось: ${utroR.error}`);
|
||||
// 🔴 БУМАГА ВЛАДЕЛЬЦУ ЗДЕСЬ НЕ КЛАДЁТСЯ, И ЭТО НЕ ЗАБЫВЧИВОСТЬ. Ночь тут вправду
|
||||
// кончилась, но закрывает её `writeRunOutcome` внутри `startAll` — то же самое место,
|
||||
// что закрывает ночь после последнего погасшего работника. Бумагу кладёт ОНО.
|
||||
// Положи её ещё и здесь — в затее завелось бы второе правило «прогон кончился»,
|
||||
// ровно то, чего `run.mjs` избегает вслух. Одна дверь, один зов.
|
||||
return konec(1);
|
||||
}
|
||||
// След снят: задача пошла в работу. Не сними — следующий запуск отказал бы по нему снова,
|
||||
|
||||
@@ -12,12 +12,16 @@
|
||||
// вопросов и ответов владельца (кусок 5), приёмщика (кусок 7).
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { existsSync, readdirSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { writeJsonAtomic, readJson, readLines } from './state.mjs';
|
||||
import { touchLife, readLife } from './heartbeat.mjs';
|
||||
import { workerInboxDir, lastRunPath, resolveBin, SUMMARY_LINES_FILE } from './paths.mjs';
|
||||
// Русские слова для итоговых состояний живут в одном месте — в сводке (задача 9).
|
||||
import { STATUS_WORD } from './summary.mjs';
|
||||
// 🔴 Утренняя бумага ВЛАДЕЛЬЦУ. Ввозится ОДНОЙ строкой и ОДИН раз — второй ввоз того же
|
||||
// имени валит `node` ещё до первой строки работы, и ночь не запускается, оставаясь зелёной
|
||||
// в проверках. Кольца ввозов тут нет: `utro.mjs` ввозит только `summary.mjs`, а тот — ничего.
|
||||
import { sobratDovody, zapisatUtro, utroPath } from './utro.mjs';
|
||||
// --- дополнение к ввозам в шапке файла (задача 12) ----------------------------
|
||||
import { spawn } from 'node:child_process';
|
||||
import { mkdirSync, openSync } from 'node:fs';
|
||||
@@ -904,8 +908,22 @@ export function amILast(runDirPath) {
|
||||
return cards.length > 0 && cards.every(c => FINAL_STATUSES.includes(c.status));
|
||||
}
|
||||
|
||||
// Настоящее кладение бумаги. Отдельным именем, чтобы его можно было подменить в проверках
|
||||
// и чтобы `writeRunOutcome` осталась читаемой.
|
||||
export function zapisatUtroPoItogu({ koren, diskFreeGb = null }) {
|
||||
const dovody = sobratDovody({ koren });
|
||||
return zapisatUtro({ ...dovody, disk_free_gb: diskFreeGb }, { put: utroPath(koren) });
|
||||
}
|
||||
|
||||
export function writeRunOutcome({
|
||||
runDirPath, runId, lastRunFile = lastRunPath(), now = Date.now(),
|
||||
// 🔴 Свободное место на утро. Замер СПРАШИВАЕТСЯ у зовущего, а не делается здесь:
|
||||
// эта работа синхронная, а замер места — нет. Не дали — в сводке честное «неизвестно»,
|
||||
// выдуманного числа тут не появится никогда.
|
||||
diskFreeGb = null,
|
||||
// 🪤 Кладение бумаги — точка впрыска: иначе проверки этой работы писали бы файл
|
||||
// в НАСТОЯЩЕЕ хозяйство улик рядом с боевым `last-run.json`.
|
||||
polozhitUtro = zapisatUtroPoItogu,
|
||||
// 🪤 Умолчания НАСТОЯЩИЕ: подставка подаётся явно и только в проверках. Иначе
|
||||
// проверки писали бы след в боевой `.night/deferred/`, и следующая живая ночь
|
||||
// упёрлась бы в отказ, оставленный проверкой.
|
||||
@@ -951,6 +969,29 @@ export function writeRunOutcome({
|
||||
// 🪤 `last-run.json` — другой файл, его пишет один надзиратель, и замка ему не нужно.
|
||||
writeJsonAtomic(lastRunFile, outcome);
|
||||
|
||||
// ━━━ 🔴🔴 ПОСЛЕДНЯЯ МИЛЯ ВСЕЙ ЗАТЕИ: НОЧЬ КОНЧИЛАСЬ — ВЛАДЕЛЬЦУ ЛОЖИТСЯ БУМАГА ━━━
|
||||
//
|
||||
// Здесь и только здесь. Это ЕДИНСТВЕННАЯ дверь, через которую проходят ОБА конца ночи:
|
||||
// и «не завёлся ни один работник» (зовёт `run.mjs`), и «погас последний работник»
|
||||
// (зовёт надзиратель выше). Второй копии правила «прогон кончился» в затее нет нарочно —
|
||||
// не заводим её и мы.
|
||||
//
|
||||
// 🔴 ЗАЧЕМ ЭТО ВООБЩЕ. Почта отменена решением владельца, телеграма нет, утреннего
|
||||
// будильника не построено. Значит бумага, кладущаяся вот в эту секунду, — ЕДИНСТВЕННАЯ
|
||||
// существующая дорога до человека. Без неё даже безупречная ночь утром молчит, а тишину
|
||||
// владелец не отличит от «всё хорошо».
|
||||
//
|
||||
// 🪤 СТРОГО ПОСЛЕ `writeJsonAtomic`: сводка читает `last-run.json`. Положи её раньше —
|
||||
// она рассказала бы про ПРОШЛУЮ ночь, и это было бы враньём страшнее молчания.
|
||||
//
|
||||
// 🔴 НИКОГДА НЕ РОНЯЕТ ИТОГ ПРОГОНА. Упади бумага — потерялся бы и след отложенной задачи
|
||||
// ниже, и назавтра задача молча не подхватилась бы. Бумага важна, но не важнее итога.
|
||||
try {
|
||||
polozhitUtro({ koren: dirname(lastRunFile), diskFreeGb });
|
||||
} catch (err) {
|
||||
logEvent(runDirPath, { kind: 'utro_ne_leglo', error: String(err?.message ?? err) }, now);
|
||||
}
|
||||
|
||||
// 🔴 СЛЕД ОТЛОЖЕННОЙ ЗАДАЧИ (проверка 28а). Прогон кончился, а пункты остались —
|
||||
// задача не влезла в ночь. Назавтра она сама не подхватится: след её держит, и снимает
|
||||
// его только слово владельца (задача 6). Кладём след ЗДЕСЬ и только здесь.
|
||||
@@ -1140,6 +1181,9 @@ export async function supervise({
|
||||
logEvent(runDirPath, { kind: 'run_started_at_missing', worker: workerId }, startedAt);
|
||||
}
|
||||
|
||||
// Последнее удавшееся число свободного места — для утренней бумаги (заполняется кругом ниже).
|
||||
let posledniyZamerMesta = null;
|
||||
|
||||
function closeRunIfLast(at) {
|
||||
// 🔴 Прогон закрывает надзиратель ПОСЛЕДНЕГО погасшего работника — больше некому:
|
||||
// оркестратор к этому времени давно умер вместе с сессией помощника.
|
||||
@@ -1149,7 +1193,7 @@ export async function supervise({
|
||||
// боевой `.night/deferred/`, — и проверки круга оставляли бы там настоящие отказы.
|
||||
// Поймано живьём 02.08.2026: вырез этой строки положил в хранилище восемь настоящих
|
||||
// отказов по задачам, которых никто не откладывал.
|
||||
writeRunOutcome({ runDirPath, runId, lastRunFile, now: at, deferredPath });
|
||||
writeRunOutcome({ runDirPath, runId, lastRunFile, now: at, deferredPath, diskFreeGb: posledniyZamerMesta });
|
||||
}
|
||||
|
||||
// 🔴🔴 ЗАКРЫТИЕ ДЫРЫ 03.08.2026, ВТОРОЙ КРУГ, ВРЕЗАНО В ГОРЯЧЕЕ МЕСТО. `claimFiles`
|
||||
@@ -1592,6 +1636,15 @@ export async function supervise({
|
||||
});
|
||||
|
||||
const disk = await probeDisk();
|
||||
// 🔴 Последний удавшийся замер места запоминается для УТРЕННЕЙ БУМАГИ. Конец ночи
|
||||
// синхронный, а замер места — нет, и своего замера он сделать не может. Круг же меряет
|
||||
// место каждые полминуты — берём готовое, вместо того чтобы мерить второй раз или
|
||||
// подсовывать владельцу «неизвестно» там, где число вправду есть.
|
||||
// 🪤 Только УДАВШИЙСЯ: осечку замера запоминать нельзя, иначе к утру она затрёт
|
||||
// последнее настоящее число и владелец прочтёт «неизвестно» вместо правды.
|
||||
if (typeof disk?.free_gb === 'number' && Number.isFinite(disk.free_gb)) {
|
||||
posledniyZamerMesta = Number(disk.free_gb.toFixed(1));
|
||||
}
|
||||
// Осечка замера — не приговор с первого раза (см. DISK_BAD_PROBES_DEFAULT). Хороший
|
||||
// замер счётчик обнуляет: подтверждением считаются только осечки ПОДРЯД.
|
||||
diskBadInARow = disk?.ok ? 0 : diskBadInARow + 1;
|
||||
|
||||
+29
-1
@@ -351,6 +351,24 @@ function shapka(ms) {
|
||||
return `СВОДКА ЗА НОЧЬ · собрана ${vremyaSutok(ms).tvoritelnyy} ${ru(ms)}`;
|
||||
}
|
||||
|
||||
// 🔴 НАЙДЕНО ГЛАЗАМИ 04.08.2026 НА УДАЧНОЙ НОЧИ, И ПРОВЕРКИ ЭТО ПРОПУСТИЛИ. Под заголовком
|
||||
// «ЧТО СЛОМАЛОСЬ» стояли ОБА работника — «w-1: погашен», «w-2: погашен», — хотя ночь удалась
|
||||
// целиком и они просто штатно кончили работу. Владелец читает заголовок, видит там своих
|
||||
// работников и решает, что беда. Тот же класс, что «успех подан как поломка», только вёрсткой,
|
||||
// а не переводом. Мой собственный сторож это охранял: он требовал, чтобы графа была непуста,
|
||||
// — и непустой её делало ровно это враньё.
|
||||
//
|
||||
// Состояния, которые беда САМИ ПО СЕБЕ: работник встал или не родился вовсе.
|
||||
export const TREVOZHNYE_SOSTOYANIYA = ['stalled', 'born_dead'];
|
||||
|
||||
// Беда ли это. Штатно погасший работник («все пункты закрыты», «упёрся в восемь часов») —
|
||||
// не поломка: первое это успех, второе предел, и оба видны в разделе «что сделано».
|
||||
export function bedaLiRabotnik(w) {
|
||||
if (TREVOZHNYE_SOSTOYANIYA.includes(w?.status)) return true;
|
||||
const prichina = String(w?.reason ?? '');
|
||||
return PRIMETY_PRICHIN.some(p => p.primeta.test(prichina));
|
||||
}
|
||||
|
||||
function prigovorPo({ run_id, items }) {
|
||||
if (!run_id) return PRIGOVORY.ne_bylo;
|
||||
const vsego = Number(items?.vsego ?? 0);
|
||||
@@ -417,11 +435,21 @@ export function utrenniyOtchyot({
|
||||
}
|
||||
}
|
||||
|
||||
// 🔴 Работники, кончившие ШТАТНО, названы здесь — а не в графе «что сломалось», где они
|
||||
// пугали бы владельца ни за что. Молчать о них тоже нельзя: он вправе знать, кто работал.
|
||||
const shtatnye = workers.filter(w => !bedaLiRabotnik(w));
|
||||
if (shtatnye.length) {
|
||||
const kto = shtatnye.map(w => `${w.worker_id ?? 'без имени'} — ${slovoSostoyaniya(w.status)}`).join('; ');
|
||||
v.push(`Работали ${shtatnye.length} ${sklonenie(shtatnye.length, 'работник', 'работника', 'работников')}, все кончили штатно: ${kto}.`);
|
||||
}
|
||||
|
||||
// ━━━ 2. ЧТО СЛОМАЛОСЬ ━━━
|
||||
v.push('');
|
||||
v.push(ZAGOLOVKI.slomalos);
|
||||
const bedy = [];
|
||||
for (const w of workers) {
|
||||
// 🔴 Сюда попадают ТОЛЬКО настоящие беды. Штатно кончивший работник — не поломка,
|
||||
// и его место в разделе «что сделано», где он никого не пугает.
|
||||
for (const w of workers.filter(bedaLiRabotnik)) {
|
||||
const imya = String(w.worker_id ?? 'работник без имени');
|
||||
const sostoyanie = slovoSostoyaniya(w.status);
|
||||
// Вот здесь приметы отказа уместны: `reason` — это ровно причина, по которой встали.
|
||||
|
||||
+175
-20
@@ -19,7 +19,7 @@ import {
|
||||
neponyatnyeKuski, ochelovechitPrichinu, ochelovechitNazvanie,
|
||||
merkaCheloveka, merkaAvariynoy, utrenniyOtchyot, zapisatUtro,
|
||||
slovaProZapas, slovoSostoyaniya, sobratDovody, avariynyyTekst,
|
||||
vremyaSutok, moskovskiyChas,
|
||||
vremyaSutok, moskovskiyChas, bedaLiRabotnik,
|
||||
PRIGOVORY, ZAGOLOVKI, IMYA_FAYLA, utroPath, METKA_MASTERA, RAZDELITEL_MASTERU,
|
||||
} from './utro.mjs';
|
||||
// 🔴 Общий счёт времени затеи — только ради сверки с моим. Ввозится в ПРОВЕРКУ, а не
|
||||
@@ -28,6 +28,8 @@ import {
|
||||
import { moskovskoeVremya } from './budget.mjs';
|
||||
// Точка входа затеи — её утренние дела обязаны класть бумагу владельцу сами.
|
||||
import { main } from './cli.mjs';
|
||||
// Единственная дверь, через которую проходят ОБА конца ночи.
|
||||
import { writeRunOutcome } from './supervisor.mjs';
|
||||
|
||||
// 🔴 НЕ ВЫДУМАННЫЙ ОБРАЗЕЦ. Это дословная причина из настоящей улики владельца —
|
||||
// `.night/runs/r-2026-08-03-212935-f0b5d48a/summary-lines.jsonl`, ночь с 3 на 4 августа 2026.
|
||||
@@ -490,28 +492,74 @@ describe('🔴 сводка ложится САМА, а не только по
|
||||
log.mockRestore();
|
||||
});
|
||||
|
||||
it('🔴 ночь кончилась СРАЗУ (не завёлся ни один работник) — бумага ложится тут же', async () => {
|
||||
// 🔴 НАЙДЕНО СУДЬЁЙ 04.08.2026. Зов стоял только в `summary` и `mail`, а ночь
|
||||
// запускается командой `run`. Работников ноль → надзирателей нет → будить утро некому
|
||||
// → почта отменена. Владелец не узнал бы о провале НИЧЕГО. Ровно этот случай был
|
||||
// живьём в ночь с 3 на 4 августа.
|
||||
const log = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
const err = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
let polozhili = false;
|
||||
const code = await main(['node', 'cli.mjs', 'run', '--plan', 'нет-такого-плана.md'], {
|
||||
...obshchee,
|
||||
polozhitUtro: () => { polozhili = true; return { ok: true, put: 'куда-то' }; },
|
||||
it('🔴 КОНЕЦ НОЧИ КЛАДЁТ БУМАГУ: зов стоит в единственной двери, через какую идут оба конца', () => {
|
||||
// 🔴 НАЙДЕНО СУДЬЁЙ 04.08.2026. Зов стоял только в `summary` и `mail`, а ночь запускается
|
||||
// командой `run`. Почта отменена, телеграма нет, утреннего будильника не построено —
|
||||
// владелец не узнал бы об итоге НИЧЕГО, пока живой человек не наберёт команду руками.
|
||||
const koren = vremenno();
|
||||
mkdirSync(join(koren, 'runs', 'r-5', 'workers'), { recursive: true });
|
||||
writeFileSync(join(koren, 'runs', 'r-5', 'run.json'), JSON.stringify({
|
||||
run_id: 'r-5', workers: ['w-1'], items: [{ id: '1', title: 'первый пункт' }],
|
||||
}), 'utf8');
|
||||
|
||||
writeRunOutcome({
|
||||
runDirPath: join(koren, 'runs', 'r-5'),
|
||||
runId: 'r-5',
|
||||
lastRunFile: join(koren, 'last-run.json'),
|
||||
now: T0,
|
||||
deferredPath: join(koren, 'deferred'),
|
||||
diskFreeGb: 12.5,
|
||||
});
|
||||
log.mockRestore();
|
||||
err.mockRestore();
|
||||
expect(code).not.toBe(0);
|
||||
// 🪤 Сторож нарочно НЕ требует, чтобы отказ случился именно на нуле работников: до этой
|
||||
// точки в `run` стоит десяток более ранних ворот, и они могут закрыться раньше. Он
|
||||
// требует другого и главного — чтобы ветка нуля работников зов бумаги СОДЕРЖАЛА.
|
||||
|
||||
// Бумага легла РЯДОМ с итогом прогона, в то же хозяйство.
|
||||
const put = utroPath(koren);
|
||||
expect(existsSync(put), 'ночь кончилась, а бумаги владельцу нет').toBe(true);
|
||||
const text = readFileSync(put, 'utf8');
|
||||
expect(merkaCheloveka(text.split(RAZDELITEL_MASTERU)[0])).toEqual([]);
|
||||
// 🪤 И место на сервере доехало замеренным: конец ночи синхронный и сам мерить не умеет.
|
||||
expect(text).toMatch(/12,5/);
|
||||
});
|
||||
|
||||
it('🪤 бумага кладётся СТРОГО ПОСЛЕ итога прогона — иначе рассказала бы про прошлую ночь', () => {
|
||||
const koren = vremenno();
|
||||
mkdirSync(join(koren, 'runs', 'r-6', 'workers'), { recursive: true });
|
||||
writeFileSync(join(koren, 'runs', 'r-6', 'run.json'), JSON.stringify({
|
||||
run_id: 'r-6', workers: [], items: [{ id: '1', title: 'пункт' }],
|
||||
}), 'utf8');
|
||||
// Кладём итог ПРОШЛОЙ ночи — если бумага соберётся до записи новой, она назовёт её.
|
||||
writeFileSync(join(koren, 'last-run.json'), JSON.stringify({ run_id: 'r-STARYY', workers: [] }), 'utf8');
|
||||
|
||||
writeRunOutcome({
|
||||
runDirPath: join(koren, 'runs', 'r-6'), runId: 'r-6',
|
||||
lastRunFile: join(koren, 'last-run.json'), now: T0, deferredPath: join(koren, 'deferred'),
|
||||
});
|
||||
const text = readFileSync(utroPath(koren), 'utf8');
|
||||
expect(text, 'бумага назвала ПРОШЛУЮ ночь').not.toMatch(/r-STARYY/);
|
||||
expect(text).toMatch(/r-6/);
|
||||
});
|
||||
|
||||
it('🔴 бумага упала — итог прогона всё равно записан, ночь не потеряна', () => {
|
||||
// Бумага важна, но не важнее итога: упади с ней весь `writeRunOutcome` — потерялся бы
|
||||
// и след отложенной задачи, и назавтра она молча не подхватилась бы.
|
||||
const koren = vremenno();
|
||||
mkdirSync(join(koren, 'runs', 'r-7', 'workers'), { recursive: true });
|
||||
writeFileSync(join(koren, 'runs', 'r-7', 'run.json'), JSON.stringify({ run_id: 'r-7', workers: [], items: [] }), 'utf8');
|
||||
const itog = writeRunOutcome({
|
||||
runDirPath: join(koren, 'runs', 'r-7'), runId: 'r-7',
|
||||
lastRunFile: join(koren, 'last-run.json'), now: T0, deferredPath: join(koren, 'deferred'),
|
||||
polozhitUtro: () => { throw new Error('бумага не легла'); },
|
||||
});
|
||||
expect(itog.run_id).toBe('r-7');
|
||||
expect(JSON.parse(readFileSync(join(koren, 'last-run.json'), 'utf8')).run_id).toBe('r-7');
|
||||
});
|
||||
|
||||
it('🪤 второй копии правила «прогон кончился» не завелось: в точке входа зова бумаги нет', () => {
|
||||
// Ночь закрывает ОДНА дверь (`writeRunOutcome`), и бумагу кладёт она же. Заведись зов
|
||||
// ещё и в ветке нуля работников — их стало бы два, ровно та беда, от которой `run.mjs`
|
||||
// предостерегает вслух.
|
||||
const src = readFileSync(new URL('./cli.mjs', import.meta.url), 'utf8');
|
||||
const vetka = src.split('НЕ начался: не завёлся НИ ОДИН работник')[1]?.split('return konec(1)')[0] ?? '';
|
||||
expect(vetka, 'в ветке «не завёлся ни один работник» нет зова утренней бумаги').toMatch(/polozhitUtro/);
|
||||
void polozhili;
|
||||
expect(vetka, 'в точке входа завелась вторая копия правила «прогон кончился»').not.toMatch(/polozhitUtro\(/);
|
||||
});
|
||||
|
||||
it('🪤 точка входа ЗАПУСКАЕТСЯ: второй ввоз того же имени валил бы её до первой строки', async () => {
|
||||
@@ -524,6 +572,113 @@ describe('🔴 сводка ложится САМА, а не только по
|
||||
});
|
||||
});
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
describe('🟢 НОРМАЛЬНО ОТРАБОТАВШАЯ НОЧЬ — что владелец прочтёт утром', () => {
|
||||
// Всю смену сводку проверяли на провале. Провал важнее — но удачная ночь случается чаще,
|
||||
// и если она читается плохо, владелец перестанет открывать файл вовсе.
|
||||
function udachnayaNoch() {
|
||||
const koren = vremenno();
|
||||
const rdir = join(koren, 'runs', 'r-udachnaya');
|
||||
mkdirSync(join(rdir, 'workers', 'w-1'), { recursive: true });
|
||||
mkdirSync(join(rdir, 'workers', 'w-2'), { recursive: true });
|
||||
writeFileSync(join(koren, 'last-run.json'), JSON.stringify({
|
||||
run_id: 'r-udachnaya',
|
||||
workers: [
|
||||
{ worker_id: 'w-1', status: 'extinguished', reason: 'Все пункты закрыты, работы больше нет.', closed_items: ['1', '2'], touched: { money: false, rights: false, client_visible: true } },
|
||||
{ worker_id: 'w-2', status: 'extinguished', reason: 'Все пункты закрыты, работы больше нет.', closed_items: ['3'], touched: { money: true, rights: false, client_visible: false } },
|
||||
],
|
||||
}), 'utf8');
|
||||
writeFileSync(join(rdir, 'run.json'), JSON.stringify({
|
||||
items: [
|
||||
{ id: '1', title: 'починить кнопку «продолжить» на экране сделок' },
|
||||
{ id: '2', title: 'завести письмо владельцу о вечерней заливке' },
|
||||
{ id: '3', title: 'списание считать по тарифной ступени, а не по средней' },
|
||||
],
|
||||
}), 'utf8');
|
||||
writeFileSync(join(koren, 'budget-measures.jsonl'), [
|
||||
JSON.stringify({ run_id: 'r-udachnaya', utilization: 0.12 }),
|
||||
JSON.stringify({ run_id: 'r-udachnaya', utilization: 0.29 }),
|
||||
].join('\n'), 'utf8');
|
||||
return koren;
|
||||
}
|
||||
|
||||
it('удачная ночь читается человеком и проходит мерку целиком', () => {
|
||||
const koren = udachnayaNoch();
|
||||
const d = sobratDovody({ koren });
|
||||
const { vladeltsu, prigovor } = utrenniyOtchyot({ ...d, disk_free_gb: 210.4, now: T0 });
|
||||
expect(prigovor).toBe(PRIGOVORY.udalas);
|
||||
expect(merkaCheloveka(vladeltsu)).toEqual([]);
|
||||
});
|
||||
|
||||
it('владелец видит, ЧТО именно сделано — поимённо, а не только счётом', () => {
|
||||
const koren = udachnayaNoch();
|
||||
const { vladeltsu } = utrenniyOtchyot({ ...sobratDovody({ koren }), disk_free_gb: 210.4, now: T0 });
|
||||
expect(vladeltsu).toMatch(/Сделано 3 пункта из 3/);
|
||||
expect(vladeltsu).toMatch(/починить кнопку/);
|
||||
expect(vladeltsu).toMatch(/тарифной ступени/);
|
||||
});
|
||||
|
||||
it('🔴 в удачную ночь под «ЧТО СЛОМАЛОСЬ» НЕТ работников — ничего не сломалось', () => {
|
||||
// 🔴 НАЙДЕНО ГЛАЗАМИ, А НЕ ПРОВЕРКОЙ. Прежде тут стояли оба работника — «w-1: погашен»,
|
||||
// «w-2: погашен», — при том что ночь удалась целиком. Владелец читает заголовок, видит
|
||||
// своих работников и решает, что беда.
|
||||
// 🪤 И мой СОБСТВЕННЫЙ прежний сторож это охранял: он требовал, чтобы графа была
|
||||
// непуста, — а непустой её делало ровно это враньё. Проверка, требующая наличия
|
||||
// строки, слепа к тому, ЧТО в этой строке написано.
|
||||
const koren = udachnayaNoch();
|
||||
const { vladeltsu } = utrenniyOtchyot({ ...sobratDovody({ koren }), disk_free_gb: 210.4, now: T0 });
|
||||
const slomalos = vladeltsu.split(ZAGOLOVKI.slomalos)[1].split(ZAGOLOVKI.potracheno)[0];
|
||||
expect(slomalos).toMatch(/Ничего не сломалось/);
|
||||
expect(slomalos, 'штатно кончивший работник подан владельцу как поломка').not.toMatch(/w-1|w-2|погашен/);
|
||||
});
|
||||
|
||||
it('штатно кончившие работники всё равно НАЗВАНЫ — но там, где не пугают', () => {
|
||||
// Молчать о них нельзя: владелец вправе знать, кто работал.
|
||||
const koren = udachnayaNoch();
|
||||
const { vladeltsu } = utrenniyOtchyot({ ...sobratDovody({ koren }), disk_free_gb: 210.4, now: T0 });
|
||||
const sdelano = vladeltsu.split(ZAGOLOVKI.sdelano)[1].split(ZAGOLOVKI.slomalos)[0];
|
||||
expect(sdelano).toMatch(/Работали 2 работника, все кончили штатно/);
|
||||
expect(sdelano).toMatch(/w-1/);
|
||||
});
|
||||
|
||||
it('🔴 настоящая беда в графу «что сломалось» ПОПАДАЕТ — разбор не проглотил её заодно', () => {
|
||||
// Иначе лечение было бы хуже болезни: тихая графа при мёртвом работнике.
|
||||
const { vladeltsu } = utrenniyOtchyot({
|
||||
run_id: 'r-1', now: T0, items: { vsego: 2, zakryto: 1 },
|
||||
workers: [
|
||||
{ worker_id: 'w-1', status: 'extinguished', reason: 'Все пункты закрыты, работы больше нет.', closed_items: ['1'] },
|
||||
{ worker_id: 'w-2', status: 'stalled', reason: 'Работник молчит третий час.', at_item: '2' },
|
||||
],
|
||||
});
|
||||
const slomalos = vladeltsu.split(ZAGOLOVKI.slomalos)[1].split(ZAGOLOVKI.potracheno)[0];
|
||||
expect(slomalos).toMatch(/w-2/);
|
||||
expect(slomalos, 'штатный работник затесался в поломки').not.toMatch(/w-1/);
|
||||
});
|
||||
|
||||
it('КЛАСС: встал или не родился — всегда беда; штатный конец — никогда', () => {
|
||||
for (const status of ['stalled', 'born_dead']) {
|
||||
expect(bedaLiRabotnik({ status, reason: 'что угодно' }), `«${status}» не признано бедой`).toBe(true);
|
||||
}
|
||||
expect(bedaLiRabotnik({ status: 'extinguished', reason: 'Все пункты закрыты, работы больше нет.' })).toBe(false);
|
||||
expect(bedaLiRabotnik({ status: 'extinguished', reason: 'Упёрся в восемь часов.' })).toBe(false);
|
||||
// А отказ по примете беды — беда, даже если состояние обычное.
|
||||
expect(bedaLiRabotnik({ status: 'extinguished', reason: 'Command failed: npm ci' })).toBe(true);
|
||||
});
|
||||
|
||||
it('деньги за удачную ночь названы числом и оценкой', () => {
|
||||
const koren = udachnayaNoch();
|
||||
const { vladeltsu } = utrenniyOtchyot({ ...sobratDovody({ koren }), disk_free_gb: 210.4, now: T0 });
|
||||
expect(vladeltsu).toMatch(/истрачено 17 процентов/);
|
||||
expect(vladeltsu).toMatch(/меньше половины|запас есть/);
|
||||
});
|
||||
|
||||
it('после удачной ночи владельцу не велят ничего чинить', () => {
|
||||
const koren = udachnayaNoch();
|
||||
const { vladeltsu } = utrenniyOtchyot({ ...sobratDovody({ koren }), disk_free_gb: 210.4, now: T0 });
|
||||
expect(vladeltsu).toMatch(/Ничего делать не нужно/);
|
||||
});
|
||||
});
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
describe('сбор доводов из улик прогона', () => {
|
||||
function polozhitUliki(koren, { last, run, zamery }) {
|
||||
|
||||
Reference in New Issue
Block a user