прибор «ворота без денег»: показывает, что ночь видит в плане, не тратя ни копейки
This commit is contained in:
@@ -0,0 +1,281 @@
|
||||
#!/usr/bin/env node
|
||||
// ━━━ ПРИБОР «ВОРОТА БЕЗ ДЕНЕГ» ━━━
|
||||
//
|
||||
// Показывает, ЧТО НОЧЬ УВИДИТ В ПЛАНЕ и какие ворота она пройдёт, — и не тратит ни копейки.
|
||||
// Ничего не заводит, ничего не пишет: только читает и печатает простым русским.
|
||||
//
|
||||
// 🔴 ЗАЧЕМ ОН ЕСТЬ. Обычный запуск (`cli.mjs run`) отвечает на тот же вопрос, но по дороге
|
||||
// платит: перед воротами запаса он зовёт платный замер остатка. Замерено по следу оплаты
|
||||
// в `.night/probe-raw/`: три замера «перед прогоном», после которых старта не случилось,
|
||||
// стоили 0,10682 $. Владельцу, который просто хочет посмотреть, годен ли его план, платить
|
||||
// за это незачем. Здесь тот же вопрос задаётся даром.
|
||||
//
|
||||
// 🔴🔴 ПРАВИЛА НЕ ПЕРЕПИСАНЫ И НЕ СКОПИРОВАНЫ НИ ОДНО. Зовутся ТЕ ЖЕ самые:
|
||||
// · `vhodVorot` (cli-parts.mjs) — читает план и раскладывает ответы по кормушкам ворот;
|
||||
// · `validatePlan`(plan-model.mjs) — ворота самого плана;
|
||||
// · `besplatnyeVorotaDoDeneg` + `VOROTA_DENEG` (cli.mjs) — тот же бесплатный предпроход,
|
||||
// который стоит в живом запуске ПЕРЕД деньгами;
|
||||
// · `GATE_ORDER` (gates.mjs) — единственное место, где записан состав и порядок ворот.
|
||||
// Вторая копия любого из них разъехалась бы с первой МОЛЧА, и прибор показывал бы не ту ночь,
|
||||
// которая вправду запустится.
|
||||
//
|
||||
// 🔴🔴 ВОРОТА «ЗАПАС» ЗДЕСЬ НЕ СПРАШИВАЮТСЯ ВОВСЕ, и это сказано владельцу прямым текстом.
|
||||
// Подставить им зелёный ответ нельзя — это был бы выдуманный замер. Назвать их пройденными
|
||||
// нельзя тем более: человек прочёл бы «денег хватает» там, где никто не спрашивал. Ровно
|
||||
// от этой неразличимости в затее завели метку `unguarded` у пропускающих заглушек.
|
||||
//
|
||||
// Как звать:
|
||||
// node tools/night/vorota-bez-deneg.mjs <путь-к-плану> [--priyomka <лист>] [--spec <бумага>]
|
||||
//
|
||||
// 🟡 Приёмочный лист и бумага замысла — необязательны. Не назвали — двое ворот («бумаги
|
||||
// сохранены», «замысел проверен») честно отвечают «не спросили, значит не проверили», и прибор
|
||||
// говорит об этом словами, а не выдаёт молчание за успех.
|
||||
|
||||
import { resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { readJson } from './state.mjs';
|
||||
import { REPO_ROOT, configPath, planSealPath, specApprovalPath } from './paths.mjs';
|
||||
import { checkSeal } from './plan-fingerprint.mjs';
|
||||
import { freeGb, diskState, porogMesta } from './disk.mjs';
|
||||
import { specApproved, papersCommitted, paperPathsFor } from './paper-gates.mjs';
|
||||
import { vhodVorot } from './cli-parts.mjs';
|
||||
import { validatePlan, komandaProverokPunkta } from './plan-model.mjs';
|
||||
import { GATE_ORDER } from './gates.mjs';
|
||||
import { besplatnyeVorotaDoDeneg, VOROTA_DENEG, idushchieProgony, strokiProProgony } from './cli.mjs';
|
||||
|
||||
// 🔴 Слова про платные ворота — ОДНА строка на весь прибор. Второй текст того же смысла
|
||||
// разошёлся бы с первым молча, а читает их человек, который по ним решает, проверено или нет.
|
||||
export const SLOVA_ZA_DENGI = 'эти ворота проверяются только за деньги, здесь не проверены';
|
||||
|
||||
// Метки состояния. Слово «пройдены» стоит РОВНО У ОДНОЙ из них — у той, где ворота вправду
|
||||
// прошли. Ни платные, ни ненаписанные его не получают ни при каком исходе.
|
||||
const METKA = {
|
||||
da: ' [да] ',
|
||||
net: ' [НЕТ] ',
|
||||
dengi: ' [деньги] ',
|
||||
net_pravila: ' [нет правила] ',
|
||||
};
|
||||
|
||||
// ━━━ ЧТО НОЧЬ ВИДИТ В ПЛАНЕ ━━━
|
||||
// 🔴 Печатается ВСЁ, чем ночь пользуется: номер, название, проверки (ими пункт закрывается),
|
||||
// файлы (их работник правит и ими же пункт запирается), помета «длинный» и команда, которой
|
||||
// проверки этого пункта вправду гоняются. Ничего из этого не сочиняется здесь — берётся
|
||||
// из разбора плана и из `komandaProverokPunkta`.
|
||||
function strokiProPunkty(punkty = []) {
|
||||
const out = [];
|
||||
for (const i of punkty ?? []) {
|
||||
out.push(` · Task ${i?.id ?? '?'}: ${i?.title ?? '(без названия)'}${i?.long ? ' [помечен длинным]' : ''}`);
|
||||
const proverki = (i?.tests ?? []).filter(Boolean);
|
||||
out.push(proverki.length
|
||||
? ` чем закрывается: ${proverki.join(', ')}`
|
||||
: ' чем закрывается: НИЧЕМ — проверок не названо, такой пункт закрыть нечем.');
|
||||
const fajly = (i?.files ?? []).filter(Boolean);
|
||||
out.push(fajly.length
|
||||
? ` что правит: ${fajly.join(', ')}`
|
||||
: ' что правит: НЕ НАЗВАНО — запереть пункт нечем, файлы за ним держать не будут.');
|
||||
const komanda = komandaProverokPunkta(i);
|
||||
out.push(komanda
|
||||
? ` команда проверок: ${komanda}`
|
||||
: ' команда проверок: собрать не из чего.');
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// ━━━ СОСТОЯНИЕ КАЖДЫХ ВОРОТ ━━━
|
||||
// 🔴 Состав и порядок берутся из `GATE_ORDER` — единственного места, где они записаны.
|
||||
// Свой список имён здесь не заводится ни в каком виде: вторая копия разъезжается молча
|
||||
// ровно в ту ночь, когда воротам добавят десятые.
|
||||
export function sostoyanieVorot(prigovor) {
|
||||
const failed = prigovor?.failed ?? [];
|
||||
const unguarded = prigovor?.unguarded ?? [];
|
||||
return GATE_ORDER.map(gate => {
|
||||
if (gate === VOROTA_DENEG) return { gate, vid: 'dengi', reason: null };
|
||||
if (unguarded.includes(gate)) return { gate, vid: 'net_pravila', reason: null };
|
||||
const beda = failed.find(f => f?.gate === gate);
|
||||
if (beda) return { gate, vid: 'net', reason: beda.reason ?? 'Причина не названа.' };
|
||||
return { gate, vid: 'da', reason: null };
|
||||
});
|
||||
}
|
||||
|
||||
// ━━━ ВЕСЬ ОТЧЁТ СТРОКАМИ ━━━
|
||||
// 🔴 Отдельной функцией, а не печатью на месте: слова, которые читает владелец, обязаны
|
||||
// проверяться прямо, а не через живой запуск.
|
||||
export function stroki(o = {}) {
|
||||
const out = [];
|
||||
out.push('Прибор «ворота без денег»: показывает, что ночь увидит в плане.');
|
||||
out.push('Ничего не заводит и НЕ ТРАТИТ НИ КОПЕЙКИ — платный замер остатка здесь не запускается.');
|
||||
out.push(`План: ${o.planPath ?? '(не назван)'}`);
|
||||
out.push('');
|
||||
|
||||
out.push('━━ ЧТО НОЧЬ ВИДИТ В ЭТОМ ПЛАНЕ ━━');
|
||||
if (o.oshibkaPlana) {
|
||||
out.push('Прочитать план не удалось — ночь по нему тоже не пойдёт.');
|
||||
out.push(` · ${o.oshibkaPlana}`);
|
||||
} else {
|
||||
const p = o.planOk ?? {};
|
||||
out.push(`Пунктов: ${p.items_count ?? (o.punkty ?? []).length}. Из них помечено длинными: ${p.long_count ?? 0}.`);
|
||||
out.push(...strokiProPunkty(o.punkty));
|
||||
if (p.ok === true) {
|
||||
out.push('Ворота плана: план годен — по такому ночь запускают.');
|
||||
} else if (p.ok === false) {
|
||||
out.push('Ворота плана: план НЕ годен. Почему:');
|
||||
out.push(` · ${p.reason ?? 'причина не названа'}`);
|
||||
out.push(' · это правится в самом плане, а не ночью.');
|
||||
}
|
||||
}
|
||||
out.push('');
|
||||
|
||||
out.push('━━ ВОРОТА ЗАПУСКА ━━');
|
||||
for (const s of sostoyanieVorot(o.prigovor)) {
|
||||
if (s.vid === 'dengi') {
|
||||
out.push(`${METKA.dengi}${s.gate} — ${SLOVA_ZA_DENGI}.`);
|
||||
continue;
|
||||
}
|
||||
if (s.vid === 'net_pravila') {
|
||||
out.push(`${METKA.net_pravila}${s.gate} — правила у этих ворот ещё нет, они пропускают всё. Сторожа за ними НЕТ.`);
|
||||
continue;
|
||||
}
|
||||
if (s.vid === 'net') {
|
||||
out.push(`${METKA.net}${s.gate} — не пройдены. Почему:`);
|
||||
out.push(` ${s.reason}`);
|
||||
continue;
|
||||
}
|
||||
out.push(`${METKA.da}${s.gate} — пройдены.`);
|
||||
}
|
||||
if (!o.bumagiNazvany) {
|
||||
out.push('');
|
||||
out.push('Приёмочный лист и бумагу замысла вы не назвали, поэтому двое ворот выше отвечают');
|
||||
out.push('«не спросили — значит не проверили». Хотите проверить и их — добавьте');
|
||||
out.push('ключи --priyomka <приёмочный лист> и --spec <бумага замысла>.');
|
||||
}
|
||||
|
||||
out.push('');
|
||||
out.push('━━ ЧТО ЭТО ЗНАЧИТ ━━');
|
||||
const netVorot = sostoyanieVorot(o.prigovor).filter(s => s.vid === 'net');
|
||||
const planPloh = o.oshibkaPlana || o.planOk?.ok === false;
|
||||
if (planPloh) {
|
||||
out.push('Ночь по этому плану сейчас НЕ запустится: не годится сам план.');
|
||||
} else if (netVorot.length) {
|
||||
out.push(`Ночь по этому плану сейчас НЕ запустится: не пройдено ворот — ${netVorot.length}.`);
|
||||
} else {
|
||||
out.push('Всё, что можно проверить даром, — пройдено. Остаются ворота «запас»: хватает ли');
|
||||
out.push('денег, здесь не спрашивали, и узнать это можно только платным замером при запуске.');
|
||||
}
|
||||
out.push('Ни один работник не заведён, ничего не потрачено.');
|
||||
return out;
|
||||
}
|
||||
|
||||
// ━━━ СБОР ОТЧЁТА ━━━
|
||||
// 🔴 Каждый замер ниже стоит НОЛЬ: чтение файлов, счёт свободного места и вызовы `git`.
|
||||
// Платного здесь нет ничего, и появиться ему тут нельзя — на это стоит сторож в проверках,
|
||||
// смотрящий на сам исходник.
|
||||
//
|
||||
// 🪤 Все живые замеры вынесены в подставляемые доводы: без этого проверки правила ходили бы
|
||||
// на настоящий диск и в настоящее хранилище и краснели бы от соседей, а не от поломки.
|
||||
export async function sobratOtchyot({
|
||||
planPath,
|
||||
priyomkaPath = null,
|
||||
specPath = null,
|
||||
cfg = null,
|
||||
chitatNastroyki = () => readJson(configPath()) ?? {},
|
||||
// Свободное место — тем же правилом и тем же порогом, каким его меряет живой запуск.
|
||||
mesto = async nastroyki => diskState(await freeGb(REPO_ROOT), porogMesta({
|
||||
rabotnikov: nastroyki.max_workers_per_task,
|
||||
zapasGb: nastroyki.disk_free_min_gb,
|
||||
})),
|
||||
otpechatok = put => checkSeal(planSealPath(put), put),
|
||||
idushchie = () => idushchieProgony(),
|
||||
bumagi = puti => papersCommitted(REPO_ROOT, puti),
|
||||
otmetkaZamysla = () => readJson(specApprovalPath()),
|
||||
} = {}) {
|
||||
const nastroyki = cfg ?? chitatNastroyki();
|
||||
|
||||
// Разбор плана — ЭТО ВХОД ворот, а не ворота. Падает он только на настоящей беде плана
|
||||
// (незакрытая рамка-образец) либо когда файла нет вовсе; в обоих случаях говорим словами.
|
||||
let punkty = [];
|
||||
let oshibkaPlana = null;
|
||||
try {
|
||||
punkty = vhodVorot({ planPath }).items;
|
||||
} catch (err) {
|
||||
oshibkaPlana = err?.message ?? String(err);
|
||||
}
|
||||
|
||||
const planOk = oshibkaPlana ? null : validatePlan(punkty);
|
||||
|
||||
const schyot = idushchie();
|
||||
const running = schyot?.running ?? [];
|
||||
|
||||
// 🟡 Бумаги спрашиваются, только когда владелец их назвал. Не назвал — сюда идёт `null`,
|
||||
// и ворота честно отвечают «ответа не дали вовсе»: это отказ, а не пропуск.
|
||||
let papers = null;
|
||||
let spec = null;
|
||||
const bumagiNazvany = Boolean(priyomkaPath && specPath);
|
||||
if (bumagiNazvany) {
|
||||
papers = bumagi(paperPathsFor({ planPath, priyomkaPath, specPath }));
|
||||
spec = specApproved(otmetkaZamysla(), specPath);
|
||||
}
|
||||
|
||||
const vhod = vhodVorot({
|
||||
cfg: nastroyki,
|
||||
items: punkty,
|
||||
budget: null,
|
||||
disk: await mesto(nastroyki),
|
||||
planSeal: otpechatok(planPath),
|
||||
papers,
|
||||
spec,
|
||||
running,
|
||||
taskId: null,
|
||||
});
|
||||
|
||||
// 🔴 Тот же бесплатный предпроход, что стоит в живом запуске перед деньгами. Ворота
|
||||
// «запас» он из приговора выкидывает сам — их отказ здесь означал бы не «нельзя»,
|
||||
// а «ещё не мерили».
|
||||
const prigovor = besplatnyeVorotaDoDeneg({ vhod });
|
||||
|
||||
return {
|
||||
planPath,
|
||||
punkty,
|
||||
planOk,
|
||||
oshibkaPlana,
|
||||
prigovor,
|
||||
bumagiNazvany,
|
||||
schyot,
|
||||
ok: Boolean(!oshibkaPlana && planOk?.ok && prigovor.ok),
|
||||
};
|
||||
}
|
||||
|
||||
export async function main(argv = process.argv) {
|
||||
const put = argv[2] && !String(argv[2]).startsWith('--') ? argv[2] : null;
|
||||
if (!put) {
|
||||
console.error('Надо так: node tools/night/vorota-bez-deneg.mjs <путь-к-плану> [--priyomka <лист>] [--spec <бумага замысла>]');
|
||||
return 2;
|
||||
}
|
||||
const klyuch = name => {
|
||||
const i = argv.indexOf(name);
|
||||
const v = i >= 0 && i + 1 < argv.length ? argv[i + 1] : null;
|
||||
return v && !String(v).startsWith('--') ? v : null;
|
||||
};
|
||||
const o = await sobratOtchyot({
|
||||
planPath: put,
|
||||
priyomkaPath: klyuch('--priyomka'),
|
||||
specPath: klyuch('--spec'),
|
||||
});
|
||||
for (const s of stroki(o)) console.log(s);
|
||||
// Разбор хозяйства — теми же словами, какими его говорит живой запуск. Мёртвые карточки
|
||||
// владелец обязан увидеть и здесь: они занимают место в потолке задач.
|
||||
const proProgony = strokiProProgony(o.schyot);
|
||||
if (proProgony.length) {
|
||||
console.log('');
|
||||
console.log('━━ ЧТО СЕЙЧАС В ХОЗЯЙСТВЕ ━━');
|
||||
for (const s of proProgony) console.log(s);
|
||||
}
|
||||
return o.ok ? 0 : 1;
|
||||
}
|
||||
|
||||
// Точка входа звонит только когда файл запущен САМ, а не подключён проверкой через import —
|
||||
// иначе проверка сама себе позвала бы `process.exit` при подключении файла. Приём тот же,
|
||||
// что и у `cli.mjs`, и заведён он там ровно от этой беды.
|
||||
const isMain = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
||||
if (isMain) {
|
||||
main().then(code => process.exit(code));
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
// Проверки прибора «ворота без денег» (`vorota-bez-deneg.mjs`).
|
||||
//
|
||||
// 🔴 ГЛАВНОЕ, ЧТО ЗДЕСЬ СТЕРЕЖЁТСЯ, — не красота слов, а ДВЕ вещи, которые ломаются молча:
|
||||
// 1) ворота «запас» НИКОГДА не объявляются пройденными. Они платные, прибор их не зовёт,
|
||||
// и назови он их пройденными — владелец прочёл бы «денег хватает» там, где никто
|
||||
// не спрашивал. Это ровно тот класс, из-за которого в затее уже заводили метку
|
||||
// `unguarded`: пропускающая заглушка была неотличима от честно пройденной двери;
|
||||
// 2) в самом приборе нет ни одного платного зова. Сторож смотрит НА ИСХОДНИК, потому что
|
||||
// зов, добавленный завтра, ни одной проверке поведения не виден: он просто спишет
|
||||
// деньги владельца при запуске.
|
||||
//
|
||||
// 🪤 Проверки пишут только во ВРЕМЕННЫЙ каталог: живое рабочее дерево трогать нельзя
|
||||
// (сторож `storozh-zhivogo-dereva.mjs` краснеет на любую запись отсюда).
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { mkdtempSync, writeFileSync, rmSync, readFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { stroki, sobratOtchyot, SLOVA_ZA_DENGI } from './vorota-bez-deneg.mjs';
|
||||
|
||||
const ISHODNIK = fileURLToPath(new URL('./vorota-bez-deneg.mjs', import.meta.url));
|
||||
|
||||
const PUNKT = {
|
||||
id: '1',
|
||||
title: 'Сторож смотрит на каждую команду',
|
||||
tests: ['tools/a.test.mjs'],
|
||||
files: ['tools/a.mjs'],
|
||||
long: false,
|
||||
};
|
||||
|
||||
function otchyotObrazets(pravki = {}) {
|
||||
return {
|
||||
planPath: 'docs/superpowers/plans/plan.md',
|
||||
punkty: [PUNKT],
|
||||
planOk: { ok: true, reason: null, items_count: 1, long_count: 0 },
|
||||
oshibkaPlana: null,
|
||||
prigovor: {
|
||||
ok: false,
|
||||
failed: [{ gate: 'бумаги сохранены', reason: 'Эти бумаги не сохранены в хранилище — в хранилище их нет вовсе: docs/plan.md.' }],
|
||||
unguarded: ['отклик владельца', 'от прошлого прогона никого'],
|
||||
},
|
||||
bumagiNazvany: false,
|
||||
...pravki,
|
||||
};
|
||||
}
|
||||
|
||||
const vTekst = o => stroki(o).join('\n');
|
||||
|
||||
describe('прибор «ворота без денег» — что он говорит владельцу', () => {
|
||||
it('🔴 ворота «запас» НЕ объявляются пройденными: они платные и здесь не проверялись', () => {
|
||||
const t = vTekst(otchyotObrazets());
|
||||
expect(t).toContain('запас');
|
||||
expect(t).toContain(SLOVA_ZA_DENGI);
|
||||
// Ни одной строки, где «запас» назван пройденным.
|
||||
const proZapas = stroki(otchyotObrazets()).filter(s => s.includes('запас'));
|
||||
expect(proZapas.length).toBeGreaterThan(0);
|
||||
for (const s of proZapas) expect(s).not.toMatch(/пройден/i);
|
||||
});
|
||||
|
||||
it('🔴 в приборе нет ни одного платного зова — сторож смотрит на сам исходник', () => {
|
||||
const src = readFileSync(ISHODNIK, 'utf8');
|
||||
// Строки кода, а не примечания: примечание про деньги здесь как раз обязано быть.
|
||||
const kod = src.split('\n').filter(s => !s.trim().startsWith('//')).join('\n');
|
||||
for (const platnoe of ['probeUtilization', 'zamerSoSchyotom', 'vorotaPribora', 'priborZamera', 'ANTHROPIC_API_KEY']) {
|
||||
expect(kod, `в приборе появился платный зов «${platnoe}»`).not.toContain(platnoe);
|
||||
}
|
||||
});
|
||||
|
||||
it('говорит, что ночь видит в плане: сколько пунктов, их проверки и файлы', () => {
|
||||
const t = vTekst(otchyotObrazets());
|
||||
expect(t).toContain('Пунктов: 1');
|
||||
expect(t).toContain('Task 1');
|
||||
expect(t).toContain(PUNKT.title);
|
||||
expect(t).toContain('tools/a.test.mjs');
|
||||
expect(t).toContain('tools/a.mjs');
|
||||
});
|
||||
|
||||
it('по каждым непройденным воротам называет причину целиком', () => {
|
||||
const t = vTekst(otchyotObrazets());
|
||||
expect(t).toContain('бумаги сохранены');
|
||||
expect(t).toContain('в хранилище их нет вовсе: docs/plan.md');
|
||||
});
|
||||
|
||||
it('ворота без правила названы поимённо и не выданы за пройденные', () => {
|
||||
const nezapertye = stroki(otchyotObrazets()).filter(s => s.includes('отклик владельца'));
|
||||
expect(nezapertye.length).toBeGreaterThan(0);
|
||||
for (const s of nezapertye) expect(s).not.toMatch(/пройден/i);
|
||||
expect(vTekst(otchyotObrazets())).toContain('от прошлого прогона никого');
|
||||
});
|
||||
|
||||
it('отказ ворот плана печатается своими словами, а список ворот не выдумывается', () => {
|
||||
const t = vTekst(otchyotObrazets({
|
||||
planOk: { ok: false, reason: 'В плане нет ни одного пункта — запускать нечего.', items_count: 0, long_count: 0 },
|
||||
punkty: [],
|
||||
}));
|
||||
expect(t).toContain('В плане нет ни одного пункта');
|
||||
});
|
||||
|
||||
it('не названы приёмочный лист и бумага замысла — говорится прямо, а не молчком', () => {
|
||||
expect(vTekst(otchyotObrazets({ bumagiNazvany: false }))).toMatch(/--priyomka/);
|
||||
expect(vTekst(otchyotObrazets({ bumagiNazvany: true }))).not.toMatch(/--priyomka/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('сбор отчёта — читает настоящий план, ни к чему платному не ходит', () => {
|
||||
it('берёт пункты из файла плана и судит их воротами плана', async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'vorota-bez-deneg-'));
|
||||
const put = join(dir, 'plan.md');
|
||||
try {
|
||||
writeFileSync(put, '# План\n\n### Task 1: Первый\n<!-- night: tests=tools/a.test.mjs; files=tools/a.mjs -->\n');
|
||||
const o = await sobratOtchyot({
|
||||
planPath: put,
|
||||
cfg: { base_branch: 'x', max_tasks_at_once: 1, max_workers_per_task: 7 },
|
||||
// Живые замеры подменены: проверка не ходит ни на диск, ни в хранилище.
|
||||
mesto: () => ({ ok: true, reason: null }),
|
||||
otpechatok: () => ({ ok: true, reason: null }),
|
||||
idushchie: () => ({ ok: true, running: [], mertvye: [], neyasnye: [], reason: null }),
|
||||
bumagi: () => ({ ok: true, reason: null }),
|
||||
otmetkaZamysla: () => null,
|
||||
});
|
||||
expect(o.punkty.map(i => i.id)).toEqual(['1']);
|
||||
expect(o.punkty[0].files).toEqual(['tools/a.mjs']);
|
||||
expect(o.planOk.ok).toBe(true);
|
||||
// Ворота «запас» в приговоре бесплатного прохода не судятся вовсе.
|
||||
expect((o.prigovor.failed ?? []).map(f => f.gate)).not.toContain('запас');
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('план не читается — прибор говорит это словами, а не падает молча', async () => {
|
||||
const o = await sobratOtchyot({
|
||||
planPath: join(tmpdir(), 'takogo-plana-net-vovse-1234.md'),
|
||||
cfg: { base_branch: 'x', max_tasks_at_once: 1, max_workers_per_task: 7 },
|
||||
mesto: () => ({ ok: true, reason: null }),
|
||||
otpechatok: () => ({ ok: true, reason: null }),
|
||||
idushchie: () => ({ ok: true, running: [], mertvye: [], neyasnye: [], reason: null }),
|
||||
bumagi: () => ({ ok: true, reason: null }),
|
||||
otmetkaZamysla: () => null,
|
||||
});
|
||||
expect(o.oshibkaPlana).toBeTruthy();
|
||||
expect(vTekst(o)).toMatch(/план/i);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user