599 lines
37 KiB
JavaScript
599 lines
37 KiB
JavaScript
import { describe, it, expect, vi } from 'vitest';
|
||
import {
|
||
billableMs, hoursLimitHit, morningCutoff, serviceWaitHit,
|
||
SERVICE_WAIT_LIMIT_HOURS, MORNING_CUTOFF_HOUR, checkSelfLimits, resumeChecks,
|
||
} from './limits.mjs';
|
||
import { moskovskoeVremya } from './budget.mjs';
|
||
|
||
const H = 3600_000;
|
||
const M = 60_000;
|
||
|
||
describe('billableMs — что съедает восемь часов', () => {
|
||
it('обычная работа считается', () => {
|
||
expect(billableMs([{ kind: 'working', ms: 3 * H }])).toBe(3 * H);
|
||
});
|
||
|
||
it('доделывание шага перед остановкой считается', () => {
|
||
expect(billableMs([{ kind: 'stopping', ms: 1 * H }])).toBe(1 * H);
|
||
});
|
||
|
||
it('ожидание ответа С РАБОТОЙ считается наравне', () => {
|
||
expect(billableMs([{ kind: 'waiting_answer_busy', ms: 5 * H }])).toBe(5 * H);
|
||
});
|
||
|
||
it('чистый простой НЕ считается', () => {
|
||
expect(billableMs([{ kind: 'waiting_answer_idle', ms: 5 * H }])).toBe(0);
|
||
});
|
||
|
||
it('ожидание чужой службы НЕ считается — проверка 13, вторая половина', () => {
|
||
expect(billableMs([{ kind: 'waiting_service', ms: 2 * H }])).toBe(0);
|
||
});
|
||
|
||
it('пауза не считается — он молчит по команде владельца', () => {
|
||
expect(billableMs([{ kind: 'paused', ms: 10 * H }])).toBe(0);
|
||
});
|
||
|
||
it('смесь: работал 3 часа, ждал с работой 5 — предел выбран (проверка 5)', () => {
|
||
const ms = billableMs([{ kind: 'working', ms: 3 * H }, { kind: 'waiting_answer_busy', ms: 5 * H }]);
|
||
expect(hoursLimitHit(ms, 8)).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe('hoursLimitHit — восемь часов (проверка 5)', () => {
|
||
it('ровно восемь — предел УЖЕ выбран', () => {
|
||
expect(hoursLimitHit(8 * H, 8)).toBe(true);
|
||
});
|
||
|
||
it('семь часов пятьдесят девять минут — ещё нет', () => {
|
||
expect(hoursLimitHit(8 * H - M, 8)).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe('hoursLimitHit — ПРЕДЕЛА НЕ ДАЛИ (дыра 3)', () => {
|
||
// 🔴 Раньше здесь была одна строка `ms >= hours * H`, и на потерянной настройке она давала
|
||
// `ms >= NaN` — то есть ВСЕГДА «предел не выбран». Предел восьми часов исчезал молча.
|
||
// Вырежи проверки в hoursLimitHit — покраснеет весь этот раздел.
|
||
it('предела нет вовсе — падаем громко, а не отвечаем «ещё не выбрал»', () => {
|
||
expect(() => hoursLimitHit(100 * H, undefined)).toThrow(/hours_limit/);
|
||
});
|
||
|
||
it('предел пустой или не число — тоже громко', () => {
|
||
expect(() => hoursLimitHit(100 * H, null)).toThrow(/предел/i);
|
||
expect(() => hoursLimitHit(100 * H, NaN)).toThrow(/предел/i);
|
||
expect(() => hoursLimitHit(100 * H, '8')).toThrow(/предел/i);
|
||
});
|
||
|
||
it('отработанное время не посчитано — тоже громко, а не «предел не выбран»', () => {
|
||
expect(() => hoursLimitHit(undefined, 8)).toThrow(/billable_ms/);
|
||
expect(() => hoursLimitHit(NaN, 8)).toThrow(/сравнивать/i);
|
||
});
|
||
|
||
it('предел ноль — это настоящий ноль, а не «не дали»: выбран сразу', () => {
|
||
expect(hoursLimitHit(0, 0)).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe('serviceWaitHit — проверка 13: «служба не ответила»', () => {
|
||
it('порог — два часа, и он назван числом, а не спрятан в коде', () => {
|
||
expect(SERVICE_WAIT_LIMIT_HOURS).toBe(2);
|
||
});
|
||
|
||
it('ждёт службу час — не гасим, служба ещё может ответить', () => {
|
||
const r = serviceWaitHit([{ kind: 'working', ms: 3 * H }, { kind: 'waiting_service', ms: 1 * H }]);
|
||
expect(r.extinguish).toBe(false);
|
||
});
|
||
|
||
it('ждёт службу два часа — ГАСНЕТ, и причина названа теми же словами, что в бумаге', () => {
|
||
const r = serviceWaitHit([{ kind: 'working', ms: 3 * H }, { kind: 'waiting_service', ms: 2 * H }]);
|
||
expect(r.extinguish).toBe(true);
|
||
expect(r.reason).toMatch(/служба не ответила/i);
|
||
});
|
||
|
||
it('ждёт службу три часа — тоже гаснет', () => {
|
||
const r = serviceWaitHit([{ kind: 'waiting_service', ms: 3 * H }]);
|
||
expect(r.extinguish).toBe(true);
|
||
});
|
||
|
||
it('ждал службу два часа ДАВНО, а сейчас работает — не гасим: она ответила', () => {
|
||
// Порог считается по ТЕКУЩЕМУ непрерывному ожиданию. Иначе гасили бы за то,
|
||
// что уже кончилось хорошо, — и это была бы неправда в сводке.
|
||
const r = serviceWaitHit([{ kind: 'waiting_service', ms: 2 * H }, { kind: 'working', ms: 10 * M }]);
|
||
expect(r.extinguish).toBe(false);
|
||
});
|
||
|
||
it('восемь коротких ожиданий по десять минут — не «прождал два часа»', () => {
|
||
const spans = [];
|
||
for (let i = 0; i < 8; i += 1) {
|
||
spans.push({ kind: 'waiting_service', ms: 10 * M });
|
||
spans.push({ kind: 'working', ms: 20 * M });
|
||
}
|
||
spans.push({ kind: 'waiting_service', ms: 10 * M });
|
||
expect(serviceWaitHit(spans).extinguish).toBe(false);
|
||
});
|
||
|
||
it('отрезков нет вовсе — не гасим: он ещё ничего не ждал', () => {
|
||
expect(serviceWaitHit([]).extinguish).toBe(false);
|
||
expect(serviceWaitHit(null).extinguish).toBe(false);
|
||
});
|
||
|
||
it('эти два часа восемь часов НЕ съели — вторая половина проверки 13', () => {
|
||
// Он проработал 7 часов и два часа ждал службу. По часам предел НЕ выбран,
|
||
// а погашен он по «служба не ответила» — и в сводке будет написано именно это.
|
||
const spans = [{ kind: 'working', ms: 7 * H }, { kind: 'waiting_service', ms: 2 * H }];
|
||
expect(hoursLimitHit(billableMs(spans), 8)).toBe(false);
|
||
expect(billableMs(spans)).toBe(7 * H);
|
||
expect(serviceWaitHit(spans).extinguish).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe('morningCutoff — отсечка 8:00 (проверки 31, 34, 35)', () => {
|
||
const at = iso => Date.parse(iso);
|
||
|
||
it('работающего в 8:00 гасим (проверка 31)', () => {
|
||
const r = morningCutoff({ status: 'working', runStartedAt: at('2026-07-29T22:00:00+03:00') }, at('2026-07-30T08:00:00+03:00'));
|
||
expect(r.extinguish).toBe(true);
|
||
});
|
||
|
||
it('прогон целиком вне тихих часов — отсечка не применяется (проверка 34)', () => {
|
||
const r = morningCutoff({ status: 'working', runStartedAt: at('2026-07-30T09:00:00+03:00') }, at('2026-07-30T15:00:00+03:00'));
|
||
expect(r.extinguish).toBe(false);
|
||
expect(r.reason).toMatch(/вне тихих часов/i);
|
||
});
|
||
|
||
it('запущенный в 22:00 гаснет в 8:00, хотя восемь часов не вышли (проверка 35)', () => {
|
||
const r = morningCutoff({ status: 'working', runStartedAt: at('2026-07-29T22:00:00+03:00') }, at('2026-07-30T08:00:00+03:00'));
|
||
expect(r.extinguish).toBe(true);
|
||
expect(r.reason).toMatch(/8:00/);
|
||
});
|
||
|
||
it('до 8:00 не гасим', () => {
|
||
const r = morningCutoff({ status: 'working', runStartedAt: at('2026-07-29T22:00:00+03:00') }, at('2026-07-30T07:59:00+03:00'));
|
||
expect(r.extinguish).toBe(false);
|
||
});
|
||
|
||
it('ждущего ответа, который ЧТО-ТО ДЕЛАЕТ, тоже гасим', () => {
|
||
// Проверка 32 бумаги формально стоит в куске 5, но ветка живёт в этой же функции:
|
||
// без неё она позеленела бы «по умолчанию», а не по написанному правилу.
|
||
const r = morningCutoff({ status: 'waiting_answer_busy', runStartedAt: at('2026-07-29T22:00:00+03:00') }, at('2026-07-30T08:00:00+03:00'));
|
||
expect(r.extinguish).toBe(true);
|
||
});
|
||
|
||
it('стоящего в чистом простое НЕ гасим', () => {
|
||
// Проверка 33 бумаги — тоже кусок 5, ветка та же функция. Он ничего не тратит
|
||
// и доживает до ответа владельца.
|
||
const r = morningCutoff({ status: 'waiting_answer_idle', runStartedAt: at('2026-07-29T22:00:00+03:00') }, at('2026-07-30T08:00:00+03:00'));
|
||
expect(r.extinguish).toBe(false);
|
||
});
|
||
|
||
it('в 8:00 ПАУЗНОГО не гасим — пауза это команда самого владельца', () => {
|
||
// 🔴 Решение владельца. Пауза поставлена его же рукой: погасить по ней значит отменить
|
||
// его команду. Он вернётся и продолжит с того места, где остановил.
|
||
// Вырежи ветку `paused` — покраснеет здесь: паузного погасят в 8:00 вместе с работающими.
|
||
const r = morningCutoff({ status: 'paused', runStartedAt: at('2026-07-29T22:00:00+03:00') }, at('2026-07-30T08:00:00+03:00'));
|
||
expect(r.extinguish).toBe(false);
|
||
expect(r.reason).toMatch(/паузе|пауза/i);
|
||
});
|
||
|
||
it('в 8:00 ЖДУЩЕГО ЧУЖУЮ СЛУЖБУ не гасим — у него свой срок в два часа', () => {
|
||
// 🔴 Решение владельца. У ждущего службу уже есть собственный предел — два часа подряд
|
||
// (serviceWaitHit). Второй предел ему не нужен: он погаснет по своему, с честной причиной
|
||
// «служба не ответила», а не безымянно по отсечке.
|
||
// Вырежи ветку `waiting_service` — покраснеет здесь.
|
||
const r = morningCutoff({ status: 'waiting_service', runStartedAt: at('2026-07-29T22:00:00+03:00') }, at('2026-07-30T08:00:00+03:00'));
|
||
expect(r.extinguish).toBe(false);
|
||
expect(r.reason).toMatch(/служб/i);
|
||
});
|
||
|
||
it('в 8:00 РАБОТАЮЩИЙ всё равно ПОГАШЕН — послабления его не задели', () => {
|
||
// 🔴 Страховка к двум послаблениям выше. Расширяя список «кого не гасим», легко
|
||
// расширить его на всех — и отсечка 8:00 перестанет работать вовсе, молча и зелено.
|
||
// Эта проверка стережёт именно это: работающего в 8:00 гасят как гасили.
|
||
const r = morningCutoff({ status: 'working', runStartedAt: at('2026-07-29T22:00:00+03:00') }, at('2026-07-30T08:00:00+03:00'));
|
||
expect(r.extinguish).toBe(true);
|
||
expect(r.reason).toMatch(/8:00/);
|
||
});
|
||
|
||
it('УЖЕ ПОГАШЕННОГО второй раз не гасим — проверка 5, вторая половина', () => {
|
||
// 🪤 Погашенный считается мёртвым. Без этой ветки его гасили бы каждый круг заново,
|
||
// и владелец получал бы вторую строку про то же самое — а на другое утро третью.
|
||
// Вырежи ветку — покраснеет здесь.
|
||
const r = morningCutoff({ status: 'extinguished', runStartedAt: at('2026-07-29T22:00:00+03:00') }, at('2026-07-30T08:00:00+03:00'));
|
||
expect(r.extinguish).toBe(false);
|
||
expect(r.reason).toMatch(/уже погашен/i);
|
||
});
|
||
|
||
it('прогон начат в 22:45, смотрим в 23:10 — тихие часы ЗАСЧИТАНЫ, а не проскочены', () => {
|
||
// 🪤 Обход идёт получасовыми шагами: 22:45, следом 23:15 — а он уже за «сейчас».
|
||
// Смотри только шаги — про этот прогон было бы сказано «идёт целиком вне тихих часов»,
|
||
// хотя тихие часы наступили десять минут назад. Гасить в 23:10 ещё рано (это сама ночь),
|
||
// но ВРАТЬ про тихие часы нельзя: назавтра в 8:00 на этом вранье он и уцелел бы.
|
||
// Вырежи осмотр концов промежутка — покраснеет здесь.
|
||
const r = morningCutoff(
|
||
{ status: 'working', runStartedAt: at('2026-07-30T22:45:00+03:00') },
|
||
at('2026-07-30T23:10:00+03:00'),
|
||
);
|
||
expect(r.extinguish).toBe(false);
|
||
expect(r.reason ?? '').not.toMatch(/вне тихих часов/i);
|
||
});
|
||
|
||
// 🆕 Долг перед куском 5 (решение Р9 / проверка П24): час отсечки не спрятан внутри
|
||
// функции, а торчит наружу постоянной — иначе кусок 5 не может сравнить свою восьмёрку
|
||
// (`TIHIE_CHASY.to`) с этой и заметить, если они когда-нибудь разъедутся. Числа
|
||
// намеренно НЕ сведены в один источник (тихие часы — про вопросы владельцу, отсечка —
|
||
// про гашение работающих), но каждое из них теперь названо, а не спрятано.
|
||
it('час отсечки — постоянная, а не спрятанное число, и функция считает по ней', () => {
|
||
expect(MORNING_CUTOFF_HOUR).toBe(8);
|
||
const r = morningCutoff(
|
||
{ status: 'working', runStartedAt: at('2026-07-29T22:00:00+03:00') },
|
||
at(`2026-07-30T${String(MORNING_CUTOFF_HOUR).padStart(2, '0')}:00:00+03:00`),
|
||
);
|
||
expect(r.extinguish).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe('дорога к московскому часу — ОДНА (дыра 4)', () => {
|
||
const at = iso => Date.parse(iso);
|
||
|
||
it('час берётся у ЕДИНОГО прибора budget.moskovskoeVremya, а не у своей второй дороги', async () => {
|
||
// 🔴 Настоящий сторож второй дороги: подменяем ЕДИНЫЙ прибор и смотрим, послушался ли
|
||
// его этот файл. Заведи здесь свой `Intl.DateTimeFormat` обратно — подмена будет
|
||
// проигнорирована, и проверка покраснеет.
|
||
//
|
||
// Времена выбраны так, чтобы настоящий и подменённый приборы отвечали ПО-РАЗНОМУ:
|
||
// по-настоящему это полдень (тихих часов нет, гасить некого), а подменённый прибор
|
||
// говорит «начали в 23, сейчас 9» — то есть тихие часы были и настало утро.
|
||
const START = at('2026-07-30T12:00:00+03:00');
|
||
const NOW = at('2026-07-30T13:00:00+03:00');
|
||
|
||
// Сперва замеряем настоящий ответ — чтобы проверка не зеленела случайно.
|
||
expect(moskovskoeVremya(START).hour).toBe(12);
|
||
const realno = morningCutoff({ status: 'working', runStartedAt: START }, NOW);
|
||
expect(realno.extinguish).toBe(false);
|
||
expect(realno.reason).toMatch(/вне тихих часов/i);
|
||
|
||
vi.resetModules();
|
||
vi.doMock('./budget.mjs', async importOriginal => ({
|
||
...(await importOriginal()),
|
||
moskovskoeVremya: ms => ({ day: '2026-07-30', hour: ms >= NOW ? 9 : 23 }),
|
||
}));
|
||
try {
|
||
const { morningCutoff: podmenennyy } = await import('./limits.mjs');
|
||
const r = podmenennyy({ status: 'working', runStartedAt: START }, NOW);
|
||
expect(r.extinguish).toBe(true);
|
||
expect(r.reason).toMatch(/8:00/);
|
||
} finally {
|
||
vi.doUnmock('./budget.mjs');
|
||
vi.resetModules();
|
||
}
|
||
});
|
||
|
||
it('тихие часы тоже считаются ПО ТОМУ ЖЕ прибору, а не по своему', async () => {
|
||
// Обратная сторона: подменённый прибор говорит «всегда полдень» — значит тихих часов
|
||
// не было вовсе, и работающего в настоящие 8 утра гасить не за что.
|
||
const START = at('2026-07-29T22:00:00+03:00');
|
||
const NOW = at('2026-07-30T08:00:00+03:00');
|
||
expect(morningCutoff({ status: 'working', runStartedAt: START }, NOW).extinguish).toBe(true);
|
||
|
||
vi.resetModules();
|
||
vi.doMock('./budget.mjs', async importOriginal => ({
|
||
...(await importOriginal()),
|
||
moskovskoeVremya: () => ({ day: '2026-07-30', hour: 12 }),
|
||
}));
|
||
try {
|
||
const { morningCutoff: podmenennyy } = await import('./limits.mjs');
|
||
const r = podmenennyy({ status: 'working', runStartedAt: START }, NOW);
|
||
expect(r.extinguish).toBe(false);
|
||
expect(r.reason).toMatch(/вне тихих часов/i);
|
||
} finally {
|
||
vi.doUnmock('./budget.mjs');
|
||
vi.resetModules();
|
||
}
|
||
});
|
||
});
|
||
|
||
describe('checkSelfLimits — пределы, видные ВО ВРЕМЯ прогона (проверка 14)', () => {
|
||
// 22:00 по Москве — прогон начат в тихие часы, как настоящая ночь.
|
||
const START = Date.parse('2026-07-29T22:00:00.000+03:00');
|
||
|
||
// Здоровая середина ночи: час отработан, места хватает, оба предела запаса целы.
|
||
const ok = {
|
||
billable_ms: 1 * H,
|
||
hours_limit: 8,
|
||
budget: { run_over_limit: false, worker_over_limit: false, may_start: true },
|
||
disk: { ok: true, reason: null },
|
||
worker: { status: 'working', runStartedAt: START },
|
||
now: START + 1 * H,
|
||
current_item: '4.11',
|
||
};
|
||
|
||
it('всё в норме — работаем дальше, и место работника названо', () => {
|
||
const r = checkSelfLimits(ok);
|
||
expect(r.extinguish).toBe(false);
|
||
expect(r.wake_owner).toBe(false);
|
||
expect(r.reason).toBeNull();
|
||
expect(r.at_item).toBe('4.11');
|
||
});
|
||
|
||
it('выбрал восемь часов — гаснет, и место названо', () => {
|
||
const r = checkSelfLimits({ ...ok, billable_ms: 8 * H });
|
||
expect(r.extinguish).toBe(true);
|
||
expect(r.reason).toMatch(/восемь часов/i);
|
||
expect(r.at_item).toBe('4.11');
|
||
});
|
||
|
||
it('прогон выбрал свою седьмую часть — гаснет по запасу (проверка 14)', () => {
|
||
const r = checkSelfLimits({ ...ok, budget: { run_over_limit: true, worker_over_limit: false, may_start: true } });
|
||
expect(r.extinguish).toBe(true);
|
||
expect(r.reason).toMatch(/запас/i);
|
||
expect(r.reason).toMatch(/седьмую часть/i);
|
||
});
|
||
|
||
it('по общему пределу гаснет и тот, кто не выбрал НИ своих часов, ни своей доли', () => {
|
||
// 🔴 Дословно из бумаги, проверка 14: «погасли все работники, даже те, кто не выбрал
|
||
// свои часы». У этого работника всего десять минут работы и своя доля цела —
|
||
// и он всё равно гаснет.
|
||
const r = checkSelfLimits({
|
||
...ok,
|
||
billable_ms: 10 * M,
|
||
budget: { run_over_limit: true, worker_over_limit: false, may_start: true },
|
||
});
|
||
expect(r.extinguish).toBe(true);
|
||
expect(r.reason).toMatch(/седьмую часть/i);
|
||
});
|
||
|
||
it('упёрлись и в запас, и в восемь часов — в сводке сказано про ЗАПАС, а не про время', () => {
|
||
// 🔴 Третья половина проверки 14. Если сказать «упёрся в восемь часов», владелец
|
||
// решит, что дело во времени, и запустит следующую ночь — по пустому запасу.
|
||
const r = checkSelfLimits({
|
||
...ok,
|
||
billable_ms: 8 * H,
|
||
budget: { run_over_limit: true, worker_over_limit: false, may_start: true },
|
||
});
|
||
expect(r.extinguish).toBe(true);
|
||
expect(r.reason).toMatch(/запас/i);
|
||
expect(r.reason).not.toMatch(/восемь часов/i);
|
||
});
|
||
|
||
it('работник выбрал СВОЮ долю — гаснет он один, и причина названа его словами', () => {
|
||
const r = checkSelfLimits({ ...ok, budget: { run_over_limit: false, worker_over_limit: true, may_start: true } });
|
||
expect(r.extinguish).toBe(true);
|
||
expect(r.reason).toMatch(/свою долю/i);
|
||
expect(r.wake_owner).toBe(false);
|
||
});
|
||
|
||
it('сосед свою долю не выбрал — работает дальше, хотя разогнавшийся уже погашен', () => {
|
||
const r = checkSelfLimits({ ...ok, budget: { run_over_limit: false, worker_over_limit: false, may_start: true } });
|
||
expect(r.extinguish).toBe(false);
|
||
});
|
||
|
||
it('общий предел важнее личного: названа причина, по которой гаснут ВСЕ', () => {
|
||
const r = checkSelfLimits({ ...ok, budget: { run_over_limit: true, worker_over_limit: true, may_start: true } });
|
||
expect(r.extinguish).toBe(true);
|
||
expect(r.reason).toMatch(/седьмую часть/i);
|
||
expect(r.reason).not.toMatch(/свою долю/i);
|
||
});
|
||
|
||
it('кончилось место — гаснет сразу и просит будить владельца', () => {
|
||
const r = checkSelfLimits({ ...ok, disk: { ok: false, reason: 'На сервере кончается место: свободно 1.5 ГБ при пороге 5 ГБ.' } });
|
||
expect(r.extinguish).toBe(true);
|
||
expect(r.wake_owner).toBe(true);
|
||
expect(r.reason).toContain('1.5');
|
||
});
|
||
|
||
it('место смотрится РАНЬШЕ запаса: кончилось и то и другое — сказано про место', () => {
|
||
// Место чинит только человек, и чинит руками. Запас чинит время. Поэтому будят
|
||
// владельца именно за место, и причина должна быть про него.
|
||
const r = checkSelfLimits({
|
||
...ok,
|
||
disk: { ok: false, reason: 'Свободное место замерить не удалось — работать вслепую нельзя.' },
|
||
budget: { run_over_limit: true, worker_over_limit: true, may_start: true },
|
||
});
|
||
expect(r.extinguish).toBe(true);
|
||
expect(r.wake_owner).toBe(true);
|
||
expect(r.reason).toMatch(/место/i);
|
||
});
|
||
|
||
it('во всех прочих случаях владельца ночью НЕ будим', () => {
|
||
expect(checkSelfLimits({ ...ok, billable_ms: 8 * H }).wake_owner).toBe(false);
|
||
expect(checkSelfLimits({ ...ok, budget: { run_over_limit: true, may_start: true } }).wake_owner).toBe(false);
|
||
expect(checkSelfLimits({ ...ok, budget: { worker_over_limit: true, may_start: true } }).wake_owner).toBe(false);
|
||
});
|
||
|
||
it('настало 8:00 — гаснет по отсечке, и причина про 8:00', () => {
|
||
const r = checkSelfLimits({ ...ok, now: START + 10 * H });
|
||
expect(r.extinguish).toBe(true);
|
||
expect(r.reason).toMatch(/8:00/);
|
||
});
|
||
|
||
it('замера запаса нет вовсе — по запасу посреди ночи НЕ гасим', () => {
|
||
// 🔴 Решение, названное вслух в шапке задачи. Прибор мог промолчать один круг.
|
||
// Слепой СТАРТ запрещён воротами (проверка 17), а начавшийся прогон от одного
|
||
// пропущенного замера не гибнет.
|
||
// 🔴 Как проверять эту проверку вырезанием: ветки «нет запаса — гасим» в коде НЕТ
|
||
// и быть не должно — вырезать тут нечего. Обратный ход: ВСТАВЬ в `checkSelfLimits`
|
||
// перед часами строку `if (!budget) return { extinguish: true, ... }` — эта проверка
|
||
// обязана покраснеть. Не покраснела — значит она смотрит не туда.
|
||
const r = checkSelfLimits({ ...ok, budget: null });
|
||
expect(r.extinguish).toBe(false);
|
||
expect(r.reason).toBeNull();
|
||
});
|
||
|
||
it('ответа про место нет ВОВСЕ — гаснем, будим владельца, и причина названа честно', () => {
|
||
// 🔴 Соседние ворота (задача 5) случай «ответа не дали вовсе» разбирают пятью
|
||
// проверками поимённо. Здесь тот же вход гасил работника и будил владельца ночью
|
||
// с причиной «Причина не названа» — то есть человека поднимали в три часа ночи
|
||
// и не говорили, из-за чего. Направление (гасить и будить) верное: вслепую
|
||
// работать нельзя. А вот молчание про причину — нет.
|
||
const r = checkSelfLimits({ ...ok, disk: undefined });
|
||
expect(r.extinguish).toBe(true);
|
||
expect(r.wake_owner).toBe(true);
|
||
expect(r.reason).toMatch(/ответа про место не было/i);
|
||
expect(r.reason).not.toMatch(/причина не названа/i);
|
||
});
|
||
|
||
it('своя доля смотрится РАНЬШЕ восьми часов: упёрлись и в долю, и в часы — сказано про долю', () => {
|
||
// Четвёртый рубеж порядка. Без этой проверки пара «своя доля → восемь часов»
|
||
// не закреплена ничем, и следующая смена свободно переставит их местами.
|
||
const r = checkSelfLimits({
|
||
...ok,
|
||
billable_ms: 8 * H,
|
||
budget: { run_over_limit: false, worker_over_limit: true, may_start: true },
|
||
});
|
||
expect(r.extinguish).toBe(true);
|
||
expect(r.reason).toMatch(/свою долю/i);
|
||
expect(r.reason).not.toMatch(/восемь часов/i);
|
||
});
|
||
|
||
it('предела часов не дали — ГАСНЕТ, и названа потерянная настройка (дыра 3)', () => {
|
||
// 🔴 Прежде этот вход давал `ms >= NaN` = «предел не выбран», работник не гас никогда
|
||
// и жёг запас до утра — молча и зелено. Вырежи ветку про hours_limit в checkSelfLimits —
|
||
// покраснеет здесь (и заодно надзиратель начнёт падать на `hoursLimitHit`).
|
||
const r = checkSelfLimits({ ...ok, hours_limit: undefined });
|
||
expect(r.extinguish).toBe(true);
|
||
expect(r.reason).toContain('hours_limit');
|
||
expect(r.at_item).toBe('4.11');
|
||
});
|
||
|
||
it('предела часов не дали — гасим, но владельца ночью НЕ будим: место тут ни при чём', () => {
|
||
expect(checkSelfLimits({ ...ok, hours_limit: null }).wake_owner).toBe(false);
|
||
expect(checkSelfLimits({ ...ok, hours_limit: NaN }).extinguish).toBe(true);
|
||
});
|
||
|
||
it('место и запас смотрятся РАНЬШЕ потерянного предела часов — порядок не сбит', () => {
|
||
// 🔴 Ветка про потерянную настройку встала В СЕРЕДИНУ порядка, поэтому её место
|
||
// закреплено с обеих сторон: сверху — место и запас, снизу — отсечка 8:00.
|
||
const bezPredela = { ...ok, hours_limit: undefined };
|
||
const poMestu = checkSelfLimits({ ...bezPredela, disk: { ok: false, reason: 'Свободное место замерить не удалось.' } });
|
||
expect(poMestu.wake_owner).toBe(true);
|
||
expect(poMestu.reason).toMatch(/место/i);
|
||
const poZapasu = checkSelfLimits({ ...bezPredela, budget: { run_over_limit: true, may_start: true } });
|
||
expect(poZapasu.reason).toMatch(/седьмую часть/i);
|
||
// А вот отсечка 8:00 уже ПОЗЖЕ: настало и то и другое — сказано про потерянный предел.
|
||
const poOtsechke = checkSelfLimits({ ...bezPredela, now: START + 10 * H });
|
||
expect(poOtsechke.reason).toContain('hours_limit');
|
||
expect(poOtsechke.reason).not.toMatch(/8:00/);
|
||
});
|
||
|
||
it('восемь часов смотрятся РАНЬШЕ отсечки 8:00: настало и то и другое — сказано про часы', () => {
|
||
// Пятый рубеж порядка. Работник отработал свои восемь часов И на дворе уже утро.
|
||
// Правильный ответ — «упёрся в восемь часов»: он свою работу выработал, а не был
|
||
// обрублен рассветом.
|
||
const r = checkSelfLimits({ ...ok, billable_ms: 8 * H, now: START + 10 * H });
|
||
expect(r.extinguish).toBe(true);
|
||
expect(r.reason).toMatch(/восемь часов/i);
|
||
expect(r.reason).not.toMatch(/8:00/);
|
||
});
|
||
});
|
||
|
||
// ── кусок 6, задача 5: трое ворот кнопки «продолжить» ────────────────────────────────
|
||
describe('resumeChecks — трое ворот кнопки «продолжить», и больше она не проверяет ничего', () => {
|
||
const HOROSHO = { seal: true, budget: true, disk: true };
|
||
|
||
it('П-12: все три ответа хороши — разрешает и ПРЯМО говорит, что это новый прогон', () => {
|
||
const r = resumeChecks(HOROSHO);
|
||
expect(r.ok).toBe(true);
|
||
expect(r.reason).toBe(null);
|
||
// 🔴 «Считается новым прогоном» — это про деньги и часы владельца: восемь часов пойдут
|
||
// заново, а запас будет тратиться дальше. Владелец обязан узнать это словами, а не
|
||
// догадаться. Молчаливое продолжение «как будто той же ночи» — обман по часам.
|
||
expect(r.counts_as_new_run).toBe(true);
|
||
expect(r.summary_line).toMatch(/новым прогоном|новый прогон/i);
|
||
});
|
||
|
||
it('П-14: отпечаток плана не сошёлся — отказ, и новым прогоном это НЕ считается', () => {
|
||
const r = resumeChecks({ ...HOROSHO, seal: false });
|
||
expect(r.ok).toBe(false);
|
||
expect(r.reason).toMatch(/отпечат|план/i);
|
||
expect(r.counts_as_new_run).toBe(false);
|
||
expect(r.summary_line).toMatch(/отпечат|план/i);
|
||
});
|
||
|
||
it('П-15: запас не позволяет — отказ, и в причине назван именно запас', () => {
|
||
const r = resumeChecks({ ...HOROSHO, budget: false });
|
||
expect(r.ok).toBe(false);
|
||
expect(r.reason).toMatch(/запас/i);
|
||
expect(r.counts_as_new_run).toBe(false);
|
||
});
|
||
|
||
it('П-16: места на сервере нет — отказ, и в причине названо именно место', () => {
|
||
const r = resumeChecks({ ...HOROSHO, disk: false });
|
||
expect(r.ok).toBe(false);
|
||
expect(r.reason).toMatch(/мест/i);
|
||
expect(r.counts_as_new_run).toBe(false);
|
||
});
|
||
|
||
it('П-40: плохи все три — названа ПЕРВАЯ по порядку: отпечаток → место → запас', () => {
|
||
// 🔴 Порядок причин — не украшение. Владелец читает ОДНУ строку и по ней решает,
|
||
// что чинить. Назови ему «мало запаса», когда на деле сменился план, — и он пойдёт
|
||
// покупать запас вместо того, чтобы посмотреть на план.
|
||
const vse = resumeChecks({ seal: false, budget: false, disk: false });
|
||
expect(vse.reason).toMatch(/отпечат|план/i);
|
||
expect(vse.reason).not.toMatch(/запас/i);
|
||
// Плох отпечаток, а остаток запаса пришёл ответом «не мерили» — причина всё равно отпечаток.
|
||
const nemerili = resumeChecks({ seal: false, budget: null, disk: true });
|
||
expect(nemerili.ok).toBe(false);
|
||
expect(nemerili.reason).toMatch(/отпечат|план/i);
|
||
// И место раньше запаса, когда отпечаток хорош.
|
||
const mestoIzapas = resumeChecks({ seal: true, budget: false, disk: false });
|
||
expect(mestoIzapas.reason).toMatch(/мест/i);
|
||
expect(mestoIzapas.reason).not.toMatch(/запас/i);
|
||
});
|
||
|
||
it('П-15 (продолжение): «не мерили» про запас — это «нельзя», а не «наверное можно»', () => {
|
||
// 🔴 Дешёвые ворота не сошлись, платный замер не жёгся — остаток НЕИЗВЕСТЕН.
|
||
// Принять неизвестность за разрешение значит поднять работника на пустом запасе.
|
||
const r = resumeChecks({ seal: true, disk: true, budget: null });
|
||
expect(r.ok).toBe(false);
|
||
expect(r.reason).toMatch(/запас/i);
|
||
expect(r.reason).toMatch(/не мерил|неизвест/i);
|
||
expect(r.counts_as_new_run).toBe(false);
|
||
});
|
||
|
||
it('Э7 (сверх плана): ворота не подали вовсе — отказ, а не молчаливое «можно»', () => {
|
||
// 🔴 Третий вопрос вырезания второго рода: что придёт, если пришлют не «да» и не «нет»,
|
||
// а НИЧЕГО? Зовущий может забыть довод — и правило, написанное через `if (!seal)`,
|
||
// на `undefined` отказало бы, а написанное через `seal === false` — разрешило БЫ.
|
||
// Отсутствие ответа воротам — это отсутствие разрешения.
|
||
for (const chego of ['seal', 'budget', 'disk']) {
|
||
const dovody = { ...HOROSHO };
|
||
delete dovody[chego];
|
||
const r = resumeChecks(dovody);
|
||
expect(r.ok, `без ворот ${chego}`).toBe(false);
|
||
expect(r.counts_as_new_run).toBe(false);
|
||
}
|
||
expect(resumeChecks({}).ok).toBe(false);
|
||
expect(resumeChecks().ok).toBe(false);
|
||
});
|
||
|
||
it('Э8 (сверх плана): ответ ворот не «да» и не «нет», а посторонний — отказ', () => {
|
||
// 🔴 Ловушка истинности: строка «нет», ноль, пустой объект. `if (seal)` на строке «нет»
|
||
// отдаёт ИСТИНУ — ворота пропустили бы отказ как разрешение. Здесь слово ворот
|
||
// превращается в подъём работника, поэтому годны только настоящие «да» и «нет».
|
||
for (const musor of ['нет', 'нельзя', 0, 1, '', 'да', {}, []]) {
|
||
const r = resumeChecks({ ...HOROSHO, seal: musor });
|
||
if (musor === true) continue;
|
||
expect(r.ok, `отпечаток = ${JSON.stringify(musor)}`).toBe(false);
|
||
}
|
||
});
|
||
|
||
it('Э9 (сверх плана): список ворот ЗАКРЫТ — четвёртые ворота ничего не решают', () => {
|
||
// 🔴 Дословный текст проверки 9 бумаги: «больше кнопка не проверяет ничего». Появится
|
||
// в доводах чужое поле — правило обязано его не заметить, а не начать им пользоваться.
|
||
// Иначе список тихо разрастётся, и кнопка перестанет срабатывать вообще.
|
||
const sChuzhim = resumeChecks({ ...HOROSHO, owner_responded: false, previous_workers_alive: true });
|
||
expect(sChuzhim.ok).toBe(true);
|
||
const otkazSChuzhim = resumeChecks({ ...HOROSHO, seal: false, owner_responded: true });
|
||
expect(otkazSChuzhim.ok).toBe(false);
|
||
expect(otkazSChuzhim.reason).toMatch(/отпечат|план/i);
|
||
});
|
||
|
||
it('Э10 (сверх плана): ворота отвечают ПОДРОБНО — своя причина не теряется', () => {
|
||
// Живая дорога подаёт не голое «нет», а ответ прибора с причиной словами.
|
||
// Потерять её значит сказать владельцу «места нет» вместо «на диске осталось 0.4 ГБ».
|
||
const r = resumeChecks({ seal: true, disk: { ok: false, reason: 'на диске осталось 0.4 ГБ' }, budget: true });
|
||
expect(r.ok).toBe(false);
|
||
expect(r.reason).toMatch(/мест/i);
|
||
expect(r.reason).toContain('0.4 ГБ');
|
||
});
|
||
});
|