532 lines
32 KiB
JavaScript
532 lines
32 KiB
JavaScript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||
import { mkdtempSync, rmSync, writeFileSync, readFileSync } from 'node:fs';
|
||
import { tmpdir } from 'node:os';
|
||
import { join } from 'node:path';
|
||
import { GATE_ORDER, startGates, capacityFree, filesDisjoint } from './gates.mjs';
|
||
import { diskState } from './disk.mjs';
|
||
import { budgetState, recordMeasure } from './budget.mjs';
|
||
import { sealPlan, checkSeal } from './plan-fingerprint.mjs';
|
||
import { specApproved, papersCommitted } from './paper-gates.mjs';
|
||
|
||
// 🔴 ЭТОТ ФАЙЛ НЕ ЗАПУСКАЕТ НИ ОДНОЙ ВНЕШНЕЙ ПРОГРАММЫ — и не должен начать.
|
||
// Раньше две проверки ниже заводили настоящее хранилище через `git init/add/commit`. Предмет
|
||
// у них другой: держат ли ВОРОТА, когда настоящее правило `papersCommitted` сказало «нет».
|
||
// Настоящий `git` этого не доказывал ничего, зато на занятой машине отвечал то за полсекунды,
|
||
// то за пять — и ворота краснели от соседей, а не от поломки. Теперь хранилище подставное,
|
||
// а само правило — настоящее, как и было. Живой `git` проверяют две отдельные проверки
|
||
// в `paper-gates.test.mjs`. Сторож `zhivoy-zapusk-v-proverkah.test.mjs` покраснеет, если
|
||
// внешнюю программу сюда вернут.
|
||
function podstavnoeHranilishche({ sohraneny = [] } = {}) {
|
||
return (_git, args) => {
|
||
const komanda = args[0];
|
||
const put = args[args.length - 1];
|
||
if (komanda === 'ls-files') {
|
||
if (!sohraneny.includes(put)) throw new Error(`нет в хранилище: ${put}`);
|
||
return `${put}\n`;
|
||
}
|
||
if (komanda === 'status') return '';
|
||
throw new Error(`подставное хранилище не знает команды «${komanda}»`);
|
||
};
|
||
}
|
||
|
||
// Бумага замысла, по которой «запускают» в этих проверках: отметка «замысел проверен»
|
||
// привязана к бумаге, и без пути к ней настоящий `specApproved` отказывает (задача 4).
|
||
const SPEC = 'docs/superpowers/specs/2026-07-29-dispetcher-tri-kuska-chto-i-zachem.md';
|
||
|
||
// Вход ворот — теми же именами полей и той же формы, что уже принимает `planRun` куска 1.
|
||
const good = {
|
||
budget: { may_start: true },
|
||
owner: { last_summary_at: null },
|
||
disk: { ok: true, free_gb: 40, min_gb: 5 },
|
||
spec: { ok: true },
|
||
papers: { ok: true },
|
||
planSeal: { ok: true },
|
||
previousWorkers: [],
|
||
capacity: { tasks_running: 0, max_tasks: 2 },
|
||
newTaskFiles: ['a.php'],
|
||
runningTasksFiles: [],
|
||
};
|
||
|
||
describe('startGates — состав и порядок ворот', () => {
|
||
it('ворот ровно девять, имена и порядок — дословно из договора о стыках', () => {
|
||
expect(GATE_ORDER).toEqual([
|
||
'запас',
|
||
'отклик владельца',
|
||
'место на сервере',
|
||
'замысел проверен',
|
||
'бумаги сохранены',
|
||
'отпечаток плана',
|
||
'от прошлого прогона никого',
|
||
'потолки задач и работников',
|
||
'задачи не пересекаются по файлам',
|
||
]);
|
||
});
|
||
|
||
it('все ответы даны и все хорошие — запуск разрешён', () => {
|
||
const r = startGates(good);
|
||
expect(r.ok).toBe(true);
|
||
expect(r.failed).toEqual([]);
|
||
expect(r.reason).toBeNull();
|
||
});
|
||
|
||
it('запас проверяется ПЕРВЫМ: при дыре в учёте назван он, а не место', () => {
|
||
const r = startGates({
|
||
...good,
|
||
budget: { may_start: false, reason: 'Остаток посчитать не удалось' },
|
||
disk: { ok: false, reason: 'На сервере кончается место' },
|
||
});
|
||
expect(r.ok).toBe(false);
|
||
expect(r.reason).toMatch(/посчитать не удалось/i);
|
||
});
|
||
|
||
it('пять готовых ворот при плохих ответах держат ВСЕ ПЯТЬ — именами и в порядке договора', () => {
|
||
const r = startGates({
|
||
...good,
|
||
budget: { may_start: false, reason: 'запаса нет' },
|
||
disk: { ok: false, reason: 'места нет' },
|
||
spec: { ok: false, reason: 'замысел не проверен' },
|
||
papers: { ok: false, reason: 'бумаги не сохранены' },
|
||
planSeal: { ok: false, reason: 'план изменён после проверки' },
|
||
});
|
||
expect(r.failed.map(f => f.gate)).toEqual([
|
||
'запас',
|
||
'место на сервере',
|
||
'замысел проверен',
|
||
'бумаги сохранены',
|
||
'отпечаток плана',
|
||
]);
|
||
});
|
||
|
||
it('называет ВСЕ непройденные ворота списком, а не только первые', () => {
|
||
const r = startGates({
|
||
...good,
|
||
budget: { may_start: false, reason: 'нет запаса' },
|
||
disk: { ok: false, reason: 'нет места' },
|
||
});
|
||
expect(r.failed.length).toBe(2);
|
||
expect(r.all_reasons).toEqual(['запас: нет запаса', 'место на сервере: нет места']);
|
||
});
|
||
|
||
it('в `failed` лежат ПАРЫ «ворота — причина», а не строки и не одно имя', () => {
|
||
// 🔴 Шов с задачей 8: она печатает владельцу имя ворот и причину по отдельности.
|
||
// Приняла бы `failed` за одно имя — владелец увидел бы в отказе мусор вместо причины,
|
||
// и никакая проверка причин этого бы не показала.
|
||
const r = startGates({
|
||
...good,
|
||
budget: { may_start: false, reason: 'нет запаса' },
|
||
disk: { ok: false, reason: 'нет места' },
|
||
});
|
||
expect(Array.isArray(r.failed)).toBe(true);
|
||
for (const f of r.failed) {
|
||
expect(typeof f.gate).toBe('string');
|
||
expect(typeof f.reason).toBe('string');
|
||
}
|
||
expect(r.failed.map(f => f.gate)).toEqual(['запас', 'место на сервере']);
|
||
expect(r.reason).toBe('нет запаса');
|
||
});
|
||
|
||
it('двое ворот куска 7 ещё пропускают, а двое ворот куска 3 уже ДЕРЖАТ', () => {
|
||
// Кусок 2 оставил здесь честную запись «все четверо пропускают». Кусок 3 написал
|
||
// свои двое, и теперь запись другая: отклик владельца и «от прошлого прогона никого»
|
||
// по-прежнему пропускают (их пишет кусок 7), а потолки и пересечение по файлам держат.
|
||
const r = startGates({
|
||
...good,
|
||
owner: { ok: false, reason: 'владелец не отозвался' },
|
||
previousWorkers: [{ worker_id: 'w-2', status: 'waiting_answer_idle' }],
|
||
capacity: { tasks_running: 9, max_tasks: 2, workers_in_task: 0, max_workers: 7 },
|
||
newTaskFiles: ['a.php'],
|
||
runningTasksFiles: [['a.php']],
|
||
});
|
||
expect(r.ok).toBe(false);
|
||
expect(r.failed.map(f => f.gate)).toEqual([
|
||
'потолки задач и работников',
|
||
'задачи не пересекаются по файлам',
|
||
]);
|
||
});
|
||
});
|
||
|
||
describe('ответа не дали вовсе — отказ, а не пропуск', () => {
|
||
it('по запасу ответа нет — отказ, и названы эти ворота', () => {
|
||
const r = startGates({ ...good, budget: undefined });
|
||
expect(r.ok).toBe(false);
|
||
expect(r.failed[0].gate).toBe('запас');
|
||
expect(r.reason).toMatch(/ответа не дали/i);
|
||
});
|
||
|
||
it('по месту на сервере ответа нет — отказ', () => {
|
||
const r = startGates({ ...good, disk: undefined });
|
||
expect(r.ok).toBe(false);
|
||
expect(r.failed[0].gate).toBe('место на сервере');
|
||
});
|
||
|
||
it('по замыслу ответа нет — отказ', () => {
|
||
const r = startGates({ ...good, spec: undefined });
|
||
expect(r.ok).toBe(false);
|
||
expect(r.failed[0].gate).toBe('замысел проверен');
|
||
});
|
||
|
||
it('по бумагам ответа нет — отказ', () => {
|
||
const r = startGates({ ...good, papers: undefined });
|
||
expect(r.ok).toBe(false);
|
||
expect(r.failed[0].gate).toBe('бумаги сохранены');
|
||
});
|
||
|
||
it('по отпечатку плана ответа нет — отказ', () => {
|
||
const r = startGates({ ...good, planSeal: undefined });
|
||
expect(r.ok).toBe(false);
|
||
expect(r.failed[0].gate).toBe('отпечаток плана');
|
||
});
|
||
});
|
||
|
||
describe('startGates с НАСТОЯЩИМИ ответами правил задач 1–4', () => {
|
||
let dir;
|
||
beforeEach(() => { dir = mkdtempSync(join(tmpdir(), 'night-gates-')); });
|
||
afterEach(() => { rmSync(dir, { recursive: true, force: true }); });
|
||
|
||
it('место замерить не удалось — настоящий diskState держит ворота места', () => {
|
||
const r = startGates({ ...good, disk: diskState(null, 5) });
|
||
expect(r.ok).toBe(false);
|
||
expect(r.failed[0].gate).toBe('место на сервере');
|
||
expect(r.reason).toMatch(/замерить не удалось/i);
|
||
});
|
||
|
||
it('места хватает — настоящий diskState эти ворота не держит', () => {
|
||
expect(startGates({ ...good, disk: diskState(40, 5) }).ok).toBe(true);
|
||
});
|
||
|
||
it('замысел без рецензентов — настоящий specApproved держит ворота замысла (проверка 44)', () => {
|
||
const spec = specApproved({ spec_path: SPEC, reviewed_by: [], owner_said_yes_at: 123 }, SPEC);
|
||
const r = startGates({ ...good, spec });
|
||
expect(r.ok).toBe(false);
|
||
expect(r.failed[0].gate).toBe('замысел проверен');
|
||
});
|
||
|
||
it('рецензенты прошли, а владелец молчит — те же ворота держат (вторая половина проверки 44)', () => {
|
||
const spec = specApproved({ spec_path: SPEC, reviewed_by: ['исполнитель', 'недоверчивый'], owner_said_yes_at: null }, SPEC);
|
||
const r = startGates({ ...good, spec });
|
||
expect(r.ok).toBe(false);
|
||
expect(r.failed[0].gate).toBe('замысел проверен');
|
||
});
|
||
|
||
it('замысел проверен, но ПО ДРУГОЙ бумаге — те же ворота держат', () => {
|
||
// 🔴 Отметка одна на всё хозяйство: без сверки с бумагой вчерашнее «да» открывало бы
|
||
// запуск по любому другому замыслу.
|
||
const spec = specApproved(
|
||
{ spec_path: SPEC, reviewed_by: ['исполнитель', 'недоверчивый'], owner_said_yes_at: 123 },
|
||
'docs/superpowers/specs/2026-08-01-sovsem-drugaya-bumaga.md',
|
||
);
|
||
const r = startGates({ ...good, spec });
|
||
expect(r.ok).toBe(false);
|
||
expect(r.failed[0].gate).toBe('замысел проверен');
|
||
});
|
||
|
||
it('есть и рецензенты, и слово владельца по ЭТОЙ бумаге — ворота замысла не держат', () => {
|
||
const spec = specApproved({ spec_path: SPEC, reviewed_by: ['исполнитель', 'недоверчивый'], owner_said_yes_at: 123 }, SPEC);
|
||
expect(startGates({ ...good, spec }).ok).toBe(true);
|
||
});
|
||
|
||
it('бумаги не сохранены — настоящий papersCommitted держит ворота бумаг (проверка 45)', () => {
|
||
// 🔴 Правило здесь настоящее — подставлено только хранилище, у которого оно спрашивает.
|
||
const run = podstavnoeHranilishche({ sohraneny: [] });
|
||
const r = startGates({ ...good, papers: papersCommitted(dir, ['docs/plan.md'], { run }) });
|
||
expect(r.ok).toBe(false);
|
||
expect(r.failed[0].gate).toBe('бумаги сохранены');
|
||
expect(r.reason).toContain('docs/plan.md');
|
||
});
|
||
|
||
it('бумаги сохранены — ворота бумаг не держат', () => {
|
||
const run = podstavnoeHranilishche({ sohraneny: ['docs/plan.md'] });
|
||
expect(startGates({ ...good, papers: papersCommitted(dir, ['docs/plan.md'], { run }) }).ok).toBe(true);
|
||
});
|
||
|
||
it('план изменён после проверки — настоящий checkSeal держит ворота отпечатка (проверка 42)', () => {
|
||
const planPath = join(dir, 'plan.md');
|
||
const sealPath = join(dir, 'plan-seal.json');
|
||
writeFileSync(planPath, 'шаг 1: сделать хорошо\n');
|
||
sealPlan(sealPath, planPath, { approved_by: 'владелец', at: Date.parse('2026-08-01T10:00:00.000Z') });
|
||
writeFileSync(planPath, 'шаг 1: сделать хорошо\nшаг 2: и ещё немножко\n');
|
||
const r = startGates({ ...good, planSeal: checkSeal(sealPath, planPath) });
|
||
expect(r.ok).toBe(false);
|
||
expect(r.failed[0].gate).toBe('отпечаток плана');
|
||
});
|
||
|
||
it('замеров запаса нет вовсе — настоящий budgetState держит ворота запаса (проверка 17)', () => {
|
||
const cfg = { rate_limit_type: 'seven_day', run_share: 1 / 7, start_floor_share: 5 / 7 };
|
||
const budget = budgetState(join(dir, 'нет-такого-журнала.jsonl'), cfg, Date.now(), {
|
||
runId: 'r-2026-08-01-0100',
|
||
workerId: 'w-1',
|
||
workersCount: 1,
|
||
});
|
||
const r = startGates({ ...good, budget });
|
||
expect(r.ok).toBe(false);
|
||
expect(r.failed[0].gate).toBe('запас');
|
||
});
|
||
|
||
it('замер свежий и запаса хватает — настоящий budgetState ворота запаса НЕ держит', () => {
|
||
// 🔴 Без этой проверки ворота могли бы читать у `budgetState` не то поле: ответ вышел бы
|
||
// `undefined`, отказ случался бы ВСЕГДА, а обе проверки на отказ остались бы зелёными.
|
||
const cfg = { rate_limit_type: 'seven_day', run_share: 1 / 7, start_floor_share: 5 / 7 };
|
||
const now = Date.now();
|
||
const mpath = join(dir, 'budget-measures.jsonl');
|
||
// Светофор и вид предела с 03.08.2026 обязательны: решение о запуске принимает светофор,
|
||
// а доля его лишь уточняет. Здесь предмет проверки другой — что ворота запаса вправду
|
||
// питаются НАСТОЯЩИМ `budgetState`, — поэтому светофор берётся спокойный.
|
||
recordMeasure(mpath, {
|
||
utilization: 0.30, status: 'allowed_warning', kind: 'seven_day',
|
||
resets_at: now + 3 * 24 * 3600_000, at: now - 60_000,
|
||
});
|
||
const r = startGates({ ...good, budget: budgetState(mpath, cfg, now) });
|
||
expect(r.ok).toBe(true);
|
||
expect(r.failed).toEqual([]);
|
||
});
|
||
|
||
it('план не трогали после проверки — настоящий checkSeal ворота отпечатка НЕ держит', () => {
|
||
// Та же дыра с другой стороны: одна сторона отказа ничего не доказывает про пропуск.
|
||
const planPath = join(dir, 'plan.md');
|
||
const sealPath = join(dir, 'plan-seal.json');
|
||
writeFileSync(planPath, 'шаг 1: сделать хорошо\n');
|
||
sealPlan(sealPath, planPath, { approved_by: 'владелец', at: Date.parse('2026-08-01T10:00:00.000Z') });
|
||
const r = startGates({ ...good, planSeal: checkSeal(sealPath, planPath) });
|
||
expect(r.ok).toBe(true);
|
||
expect(r.failed).toEqual([]);
|
||
});
|
||
});
|
||
|
||
describe('capacityFree — правило потолков (проверки 26, 27)', () => {
|
||
it('идут две задачи — третья не запускается: две это потолок (проверка 26)', () => {
|
||
const r = capacityFree({ tasks_running: 2, max_tasks: 2, workers_in_task: 0, max_workers: 7 });
|
||
expect(r.ok).toBe(false);
|
||
expect(r.reason).toMatch(/задач/i);
|
||
expect(r.reason).toContain('2');
|
||
});
|
||
|
||
it('идёт одна задача — вторую можно', () => {
|
||
expect(capacityFree({ tasks_running: 1, max_tasks: 2, workers_in_task: 0, max_workers: 7 }).ok).toBe(true);
|
||
});
|
||
|
||
it('станет восемь работников внутри задачи — восьмой не запускается (проверка 27)', () => {
|
||
// 🔴 `workers_in_task` — это «сколько СТАНЕТ». Восемь на входе значит «семеро идут,
|
||
// просят восьмого». Он и есть лишний.
|
||
const r = capacityFree({ tasks_running: 1, max_tasks: 2, workers_in_task: 8, max_workers: 7 });
|
||
expect(r.ok).toBe(false);
|
||
expect(r.reason).toMatch(/работник/i);
|
||
expect(r.reason).toContain('7');
|
||
});
|
||
|
||
it('🔴 ГРАНИЦА: станет ровно семь — СЕДЬМОГО ПУСКАЕМ (проверка 46)', () => {
|
||
// 🔴 Сторож ошибки на единицу. Поставь в правиле `>=` вместо `>` — эта проверка
|
||
// покраснеет, и вот что она ловит: шестеро уже идут, просят седьмого, на входе стоит 7.
|
||
// При `>=` живой потолок «семь» молча превращается в «шесть», семеро одной задачи
|
||
// не соберутся никогда, а проверка 46 бумаги («семеро работников одной задачи шли
|
||
// разом») станет недостижимой. Отдельно от проверки выше — та стережёт восьмого,
|
||
// эта стережёт седьмого, и порознь они ловят разные беды.
|
||
expect(capacityFree({ tasks_running: 1, max_tasks: 2, workers_in_task: 7, max_workers: 7 }).ok).toBe(true);
|
||
});
|
||
|
||
it('станет шестеро — можно и подавно', () => {
|
||
expect(capacityFree({ tasks_running: 1, max_tasks: 2, workers_in_task: 6, max_workers: 7 }).ok).toBe(true);
|
||
});
|
||
|
||
it('потолки берутся из настроек владельца, а не из постоянной в коде', () => {
|
||
// 🔴 Правило куска 2: строка в файле владельца всегда главнее постоянной в коде.
|
||
expect(capacityFree({ tasks_running: 1, max_tasks: 1, workers_in_task: 0, max_workers: 7 }).ok).toBe(false);
|
||
expect(capacityFree({ tasks_running: 0, max_tasks: 2, workers_in_task: 4, max_workers: 3 }).ok).toBe(false);
|
||
});
|
||
|
||
it('потолков не дали — берутся запасные две задачи и семь работников', () => {
|
||
// Потолок — это решение владельца, а не замер живого мира: его отсутствие не «мы
|
||
// не знаем», а «владелец не сказал ничего особенного». Поэтому запасное значение.
|
||
expect(capacityFree({ tasks_running: 2, workers_in_task: 0 }).ok).toBe(false);
|
||
expect(capacityFree({ tasks_running: 1, workers_in_task: 8 }).ok).toBe(false);
|
||
expect(capacityFree({ tasks_running: 1, workers_in_task: 7 }).ok).toBe(true);
|
||
});
|
||
|
||
it('🔴 потолок написан строкой — ОТКАЗ, а не молча запасное число', () => {
|
||
// 🪤 `Number.isFinite("3")` — ложь, и запасная семёрка встала бы вместо тройки владельца:
|
||
// он написал «трое», а работать пошли бы СЕМЕРО, и никто бы ему об этом не сказал.
|
||
// Кавычки вокруг числа в `night.config.json` ставятся рукой на раз.
|
||
const r = capacityFree({ tasks_running: 1, max_tasks: 2, workers_in_task: 1, max_workers: '3' });
|
||
expect(r.ok).toBe(false);
|
||
expect(r.reason).toContain('max_workers_per_task');
|
||
expect(r.reason).toContain('3');
|
||
});
|
||
|
||
it('🔴 потолок ноль или отрицательный — ОТКАЗ, и владельцу сказано, что он написал', () => {
|
||
// 🪤 Ноль проходит разбор насквозь: `Number.isFinite(0)` — правда. Без этой проверки
|
||
// ночь просто не начиналась бы, а причина звучала бы «идёт задач: 0, а потолок — 0»,
|
||
// и владелец полночи искал бы поломку в коде вместо опечатки в своём файле.
|
||
const r = capacityFree({ tasks_running: 0, max_tasks: 0, workers_in_task: 0, max_workers: 7 });
|
||
expect(r.ok).toBe(false);
|
||
expect(r.reason).toContain('max_tasks_at_once');
|
||
expect(r.reason).toContain('0');
|
||
expect(capacityFree({ tasks_running: 0, max_tasks: 2, workers_in_task: 0, max_workers: -1 }).ok).toBe(false);
|
||
});
|
||
|
||
it('счётчика идущих задач не дали — ОТКАЗ, а не «наверное, ноль»', () => {
|
||
// 🔴 Ворота, которым нечем питаться, всегда отвечают «всё хорошо». Замер живого мира,
|
||
// которого не сделали, — это запрет, а не разрешение.
|
||
const r = capacityFree({ workers_in_task: 0, max_tasks: 2, max_workers: 7 });
|
||
expect(r.ok).toBe(false);
|
||
expect(r.reason).toMatch(/не посчитали|не дали/i);
|
||
});
|
||
|
||
it('счётчиков не дали вовсе — отказ', () => {
|
||
expect(capacityFree().ok).toBe(false);
|
||
expect(capacityFree(null).ok).toBe(false);
|
||
});
|
||
|
||
it('🟡 счётчика работников не дали — эта половина ворот молчит, и это названо вслух', () => {
|
||
// 🟡 Единственное место во всём куске 3, где отсутствие ответа НЕ отказ. Причина —
|
||
// чужие проверки куска 1 (`okInput` в `run.test.mjs`) подают потолки без этого числа,
|
||
// а переписывать их кусок 3 права не имеет. Дыру закрывает задача 6: `vhodVorot`
|
||
// кладёт `workers_in_task` ВСЕГДА, даже когда оно ноль.
|
||
expect(capacityFree({ tasks_running: 0, max_tasks: 2 }).ok).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe('filesDisjoint — правило пересечения задач по файлам (проверка 50)', () => {
|
||
it('задачи не пересекаются по файлам — можно', () => {
|
||
expect(filesDisjoint(['app/A.php', 'app/B.php'], [['app/C.php']]).ok).toBe(true);
|
||
});
|
||
|
||
it('задачи задевают один файл — вторая не запускается, и файл НАЗВАН владельцу', () => {
|
||
const r = filesDisjoint(['app/A.php', 'app/B.php'], [['app/B.php', 'app/Z.php']]);
|
||
expect(r.ok).toBe(false);
|
||
expect(r.reason).toContain('app/B.php');
|
||
});
|
||
|
||
it('пересечений несколько — названы ВСЕ, а не только первое', () => {
|
||
// Починив одно и упёршись во второе, владелец потерял бы ночь.
|
||
const r = filesDisjoint(['app/A.php', 'app/B.php'], [['app/A.php'], ['app/B.php']]);
|
||
expect(r.reason).toContain('app/A.php');
|
||
expect(r.reason).toContain('app/B.php');
|
||
});
|
||
|
||
it('идущих задач нет вовсе — пустой список пропускает', () => {
|
||
expect(filesDisjoint(['app/A.php'], []).ok).toBe(true);
|
||
});
|
||
|
||
it('списка файлов НОВОЙ задачи не дали — отказ', () => {
|
||
const r = filesDisjoint(undefined, []);
|
||
expect(r.ok).toBe(false);
|
||
expect(r.reason).toMatch(/какие файлы/i);
|
||
});
|
||
|
||
it('список файлов новой задачи пуст — тоже отказ: задач без файлов не бывает', () => {
|
||
// 🪤 Пустой список выглядит как «ни с кем не пересекается» и пропускает что угодно.
|
||
// Это ровно та пустышка, что отвечает «всё хорошо».
|
||
expect(filesDisjoint([], [['app/A.php']]).ok).toBe(false);
|
||
});
|
||
|
||
it('списка файлов ИДУЩИХ задач не дали — отказ, а не «наверное, никто ничего не занял»', () => {
|
||
const r = filesDisjoint(['app/A.php'], undefined);
|
||
expect(r.ok).toBe(false);
|
||
expect(r.reason).toMatch(/не посчитали|не дали/i);
|
||
});
|
||
|
||
it('🔴 РАЗНЫЕ КОСЫЕ: владелец написал `app\\A.php`, идущая задача держит `app/A.php` — пересечение НАЙДЕНО', () => {
|
||
// 🔴🔴 Самая тихая беда во всей затее: сравни мы имена знак в знак — ворота
|
||
// ответили бы «не пересекаются», обе задачи пошли бы на один файл, и НИ ОДНА
|
||
// проверка не покраснела бы. Владелец пишет пути из окна Windows обратными косыми,
|
||
// `git` отдаёт прямые. Сводим обе стороны и только потом сравниваем.
|
||
const r = filesDisjoint(['app\\A.php'], [['app/A.php']]);
|
||
expect(r.ok).toBe(false);
|
||
expect(r.reason).toContain('app/A.php');
|
||
});
|
||
|
||
it('🔴 РАЗНЫЕ КОСЫЕ наоборот, и `./` спереди тоже сводится', () => {
|
||
// Обратная сторона: обратные косые пришли от идущей задачи, а `./app/A.php` — от новой.
|
||
const r = filesDisjoint(['./app/A.php'], [['app\\A.php']]);
|
||
expect(r.ok).toBe(false);
|
||
expect(r.reason).toContain('app/A.php');
|
||
});
|
||
});
|
||
|
||
describe('ворота ЗОВУТ оба новых правила, а не держат заглушки', () => {
|
||
it('идут две задачи — держат ворота потолков, и названы они своим именем (проверка 26)', () => {
|
||
const r = startGates({ ...good, capacity: { tasks_running: 2, max_tasks: 2, workers_in_task: 0, max_workers: 7 } });
|
||
expect(r.ok).toBe(false);
|
||
expect(r.failed[0].gate).toBe('потолки задач и работников');
|
||
});
|
||
|
||
it('станет восемь — те же ворота держат восьмого (проверка 27)', () => {
|
||
const r = startGates({ ...good, capacity: { tasks_running: 1, max_tasks: 2, workers_in_task: 8, max_workers: 7 } });
|
||
expect(r.ok).toBe(false);
|
||
expect(r.failed[0].gate).toBe('потолки задач и работников');
|
||
});
|
||
|
||
it('🔴 ГРАНИЦА: станет ровно семь — ворота ПРОПУСКАЮТ (проверка 46)', () => {
|
||
// Через ворота целиком, а не только правилом: тут ловится и знак сравнения,
|
||
// и то, что чужие ворота на этом же входе не держат седьмого по своей причине.
|
||
const r = startGates({ ...good, capacity: { tasks_running: 1, max_tasks: 2, workers_in_task: 7, max_workers: 7 } });
|
||
expect(r.ok).toBe(true);
|
||
});
|
||
|
||
it('новая задача задевает файл идущей — держат ворота пересечения (проверка 50)', () => {
|
||
const r = startGates({ ...good, newTaskFiles: ['app/A.php'], runningTasksFiles: [['app/A.php']] });
|
||
expect(r.ok).toBe(false);
|
||
expect(r.failed[0].gate).toBe('задачи не пересекаются по файлам');
|
||
});
|
||
|
||
it('владельцу сказано ПОЧЕМУ: в причине этих ворот назван сам файл (проверка 50)', () => {
|
||
const r = startGates({ ...good, newTaskFiles: ['app/A.php'], runningTasksFiles: [['app/A.php']] });
|
||
expect(r.reason).toContain('app/A.php');
|
||
});
|
||
|
||
it('не пройдены обои — потолки идут раньше пересечения, как в договоре о стыках', () => {
|
||
const r = startGates({
|
||
...good,
|
||
capacity: { tasks_running: 2, max_tasks: 2, workers_in_task: 0, max_workers: 7 },
|
||
newTaskFiles: ['app/A.php'],
|
||
runningTasksFiles: [['app/A.php']],
|
||
});
|
||
expect(r.failed.map(f => f.gate)).toEqual([
|
||
'потолки задач и работников',
|
||
'задачи не пересекаются по файлам',
|
||
]);
|
||
});
|
||
});
|
||
|
||
// 🔴🔴 СТОРОЖ НА КЛАСС, а не на место. Проверки «эти ворота зовут своё правило»
|
||
// стерегут места поимённо — и молчат про десятые ворота, которых ещё нет. Этот сторож
|
||
// стережёт САМ КЛАСС беды: «список ворот и список ответов разъехались» и «ворота тихо
|
||
// остались заглушкой». Смотрит он не на поведение, а на САМУ СТРОКУ в `gates.mjs`.
|
||
describe('🔴 СТОРОЖ НА КЛАСС: ворота и ответы не разъезжаются молча', () => {
|
||
const ISHODNIK = readFileSync(new URL('./gates.mjs', import.meta.url), 'utf8');
|
||
|
||
// Строки списка `answers` внутри `startGates` — как они написаны в файле.
|
||
function strokiOtvetov() {
|
||
const nachalo = ISHODNIK.indexOf('const answers = [');
|
||
expect(nachalo).toBeGreaterThan(-1); // список переименовали — сторож обязан упасть, а не смолчать
|
||
const konec = ISHODNIK.indexOf('\n ];', nachalo);
|
||
expect(konec).toBeGreaterThan(nachalo);
|
||
return ISHODNIK.slice(nachalo, konec)
|
||
.split('\n')
|
||
.slice(1)
|
||
.map(s => s.trim())
|
||
.filter(s => s.length > 0);
|
||
}
|
||
|
||
it('ответов ровно столько же, сколько ворот — лишний ответ не проверялся бы ВООБЩЕ', () => {
|
||
// 🪤 Тихая половина беды: `startGates` перебирает `GATE_ORDER`, а не `answers`.
|
||
// Допиши кто-нибудь десятый ответ, забыв имя в `GATE_ORDER`, — эти ворота считались бы
|
||
// каждый раз и НИ РАЗУ не смотрелись бы. Ни одна проверка поведения этого не увидит:
|
||
// отказ просто не случится. Обратная половина (имя без ответа) падает громко сама.
|
||
expect(strokiOtvetov().length).toBe(GATE_ORDER.length);
|
||
});
|
||
|
||
it('ни одни ворота не остались заглушкой МОЛЧА — у каждой сказано, чей это кусок', () => {
|
||
// 🔴 Заглушка пропускает всё. Значит она имеет право стоять только там, где вслух
|
||
// написано, кто её снимет. Безымянная заглушка — это ворота, которых нет, и по виду
|
||
// кода этого не отличить.
|
||
const zaglushki = strokiOtvetov().filter(s => s.startsWith('notYetWritten()'));
|
||
for (const s of zaglushki) {
|
||
expect(s).toMatch(/кусок \d+ подставит/);
|
||
}
|
||
// 🔴 И их ровно двое — обе куска 7. Кусок 3 свои двое СНЯЛ. Вернёт их кто-нибудь
|
||
// назад (или не снимет кусок 7, дописав правило мимо списка) — покраснеет здесь.
|
||
expect(zaglushki.length).toBe(2);
|
||
expect(zaglushki.every(s => /кусок 7 подставит/.test(s))).toBe(true);
|
||
});
|
||
});
|