ночь: smoke-longrun проведён через сверку настроек и денежные ворота (дверь одна с run, не копия) + датчик класса ловит любую платную команду без ворот
This commit is contained in:
+99
-28
@@ -124,6 +124,58 @@ export function proverkaNastroekVladeltsa({
|
||||
return sverka(chitatNastoyashchiyFayl() ?? {});
|
||||
}
|
||||
|
||||
// ━━━ 🔴🔴 ВОРОТА ДЕНЕГ — ОДНА ДВЕРЬ ДЛЯ ВСЕХ, КТО ЗАВОДИТ ЖИВОГО ПЛАТНОГО РАБОТНИКА
|
||||
// (задача О-4, 03.08.2026, находка третьего помощника со стороны).
|
||||
//
|
||||
// ДО ЭТОЙ ПРАВКИ разбор ответа замера — что считать отказом, что успехом, какими словами
|
||||
// это сказать владельцу — лежал ГОТОВЫМ КУСКОМ КОДА ТОЛЬКО внутри `run`. У `smoke-longrun`
|
||||
// (проба «работник живёт и через три часа», того же класса трат, что и обычный прогон) такой
|
||||
// двери не было вовсе: единственная команда затеи БЕЗ денежных ворот оказалась ровно той,
|
||||
// которой собирались делать первое живое рождение работника. Завести `smoke-longrun` своей
|
||||
// копией этого куска значило бы построить ВТОРОЕ правило одного смысла — а два правила
|
||||
// одного смысла расходятся молча, тем же классом беды, что уже нашёлся у `readRuns`
|
||||
// и у замка на файлы (см. заметки в шапке файла, «две копии одного правила»). Поэтому кусок
|
||||
// вынесен СЮДА, и `run` теперь тоже зовёт его вместо своей прежней копии — дверь одна
|
||||
// на обоих зовущих, а не по одной на каждого.
|
||||
//
|
||||
// 🪤 `otkaz` — только слова владельцу («Прогон НЕ запущен» / «Проба НЕ запущена»): само
|
||||
// правило (что считать отказом, что — успехом) от этих слов не зависит и не разветвляется.
|
||||
// 🪤 Запись успешного замера в общий журнал (`recordMeasure`) СЮДА НЕ ВХОДИТ намеренно:
|
||||
// у каждого зовущего свой `phase` в журнале («before-run» / «before-smoke» / …), и решать
|
||||
// за зовущего, под каким именем его прирост ляжет в общую историю, эта дверь не должна.
|
||||
// 🪤 РЕШЕНИЕ Р2 (наследуется от прежнего места в `run`, не переизобретается заново): причина
|
||||
// неудачи называется ТА САМАЯ, что пришла от прибора (`z.problem`), а не одна общая строка
|
||||
// на все случаи — здесь уже однажды платили десятью днями работы за придуманную причину.
|
||||
export async function vorotaDeneg({
|
||||
cfg, komanda, otkaz = 'Прогон НЕ запущен',
|
||||
zamer = zamerSoSchyotom,
|
||||
attemptsPath = probeAttemptsPath(),
|
||||
err = console.error,
|
||||
} = {}) {
|
||||
let z = null;
|
||||
try {
|
||||
z = await zamer({ cfg, attemptsPath, komanda });
|
||||
} catch (e) {
|
||||
err(`${otkaz}: замер остатка не сделан. ${e?.message ?? e}`);
|
||||
err('Это настройка в вашем night.config.json. Пример строки есть в night.config.example.json.');
|
||||
return { ok: false };
|
||||
}
|
||||
if (!z.ok) {
|
||||
err(`${otkaz}: замеров остатка за смену больше не осталось.`);
|
||||
err(z.reason);
|
||||
return { ok: false };
|
||||
}
|
||||
const m = z.measure;
|
||||
if (!m) {
|
||||
err(`${otkaz}: остаток запаса посчитать не удалось.`);
|
||||
err(` · ${z.problem ?? 'Причина не названа.'}`);
|
||||
err('Решать о деньгах вслепую нельзя, поэтому запуск не начинается вовсе. Прошлые замеры в журнале запуск не разрешают: они могли устареть за один день.');
|
||||
err('Что делать: повторить `npm run night:budget` и посмотреть, отвечает ли прибор.');
|
||||
return { ok: false };
|
||||
}
|
||||
return { ok: true, measure: m };
|
||||
}
|
||||
|
||||
// 🔴 Место мерится по каталогу ХОЗЯЙСТВА, а не по текущему каталогу: у задачи планировщика
|
||||
// рабочий каталог системный, и она мерила бы не тот диск.
|
||||
async function diskNow(cfg = {}) {
|
||||
@@ -598,6 +650,10 @@ export async function main(argv = process.argv, deps = {}) {
|
||||
// 🔴 Ворота сверки настроек с решениями владельца (задача О-3) — умолчание НАСТОЯЩЕЕ,
|
||||
// подставка подаётся явно и только в проверках.
|
||||
proverkaVladeltsa = proverkaNastroekVladeltsa,
|
||||
// 🔴 Денежные ворота (задача О-4) — та же дверь у `run` и у `smoke-longrun`. Умолчание
|
||||
// НАСТОЯЩЕЕ (`vorotaDeneg`, платный зов); подставка подаётся явно и только в проверках —
|
||||
// иначе проверить дорогу нечем без живого платного замера.
|
||||
dengiNaZapusk = vorotaDeneg,
|
||||
} = deps;
|
||||
|
||||
const cmd = argv[2];
|
||||
@@ -883,37 +939,17 @@ export async function main(argv = process.argv, deps = {}) {
|
||||
// Никакой «ещё не протухшей» строки из журнала: между тем замером и этой минутой могла
|
||||
// пройти дневная работа и съесть весь запас, а решать о деньгах вслепую нельзя (проверка 17).
|
||||
//
|
||||
// 🔴 Обёртка обязательна: нет настройки `probe_model` — ворота падают понятной
|
||||
// строкой с именем настройки, и её надо ПОКАЗАТЬ, а не уронить программу со следом стека.
|
||||
//
|
||||
// 🪤 Замер тут делается ДО ворот прогона, значит КАЖДЫЙ отказ ворот тоже стоит замера.
|
||||
// Десяток попыток «поправил — перезапустил» — это десяток замеров, и ровно от такого
|
||||
// расхода счёт за смену и защищает.
|
||||
let z = null;
|
||||
try {
|
||||
z = await zamerSoSchyotom({ cfg, attemptsPath: probeAttemptsPath(), komanda: 'run' });
|
||||
} catch (err) {
|
||||
console.error(`Прогон НЕ запущен: замер остатка не сделан. ${err?.message ?? err}`);
|
||||
console.error('Это настройка в вашем night.config.json. Пример строки есть в night.config.example.json.');
|
||||
return 1;
|
||||
}
|
||||
if (!z.ok) {
|
||||
console.error('Прогон НЕ запущен: замеров остатка за смену больше не осталось.');
|
||||
console.error(z.reason);
|
||||
return 1;
|
||||
}
|
||||
const m = z.measure;
|
||||
if (!m) {
|
||||
// 🔴🔴 РЕШЕНИЕ Р2. Здесь стояла строка «события "rate_limit_event" от системы не пришло»,
|
||||
// и она была ВРАНЬЁМ: событие приходило исправно каждый раз, не совпадал ВИД предела.
|
||||
// Владелец десять дней читал про поломку прибора, а поправить надо было одну строку
|
||||
// в своих настройках. Теперь причина называется та самая, какая есть.
|
||||
console.error('Прогон НЕ запущен: остаток запаса посчитать не удалось.');
|
||||
console.error(` · ${z.problem ?? 'Причина не названа.'}`);
|
||||
console.error('Решать о деньгах вслепую нельзя, поэтому прогон не начинается вовсе. Прошлые замеры в журнале запуск не разрешают: они могли устареть за один день.');
|
||||
console.error('Что делать: повторить `npm run night:budget` и посмотреть, отвечает ли прибор.');
|
||||
return 1;
|
||||
}
|
||||
//
|
||||
// 🔴 РАЗБОР ОТВЕТА — ЧЕРЕЗ ОБЩУЮ ДВЕРЬ `vorotaDeneg` (задача О-4, 03.08.2026), НЕ СВОИМ
|
||||
// КУСКОМ КОДА. Раньше здесь стоял готовый код (try/catch, `z.ok`, `z.measure`) — и когда
|
||||
// `smoke-longrun` понадобилась та же дверь, копия вниз завела бы ВТОРОЕ правило одного
|
||||
// смысла. Дверь вынесена выше по файлу, `run` и `smoke-longrun` зовут ОДНУ и ту же.
|
||||
const dengi = await dengiNaZapusk({ cfg, komanda: 'run' });
|
||||
if (!dengi.ok) return 1;
|
||||
const m = dengi.measure;
|
||||
recordMeasure(budgetMeasuresPath(), {
|
||||
utilization: m.utilization, status: m.status, kind: m.kind,
|
||||
resets_at: m.resets_at, at: Date.now(), phase: 'before-run',
|
||||
@@ -1028,6 +1064,41 @@ export async function main(argv = process.argv, deps = {}) {
|
||||
}
|
||||
|
||||
if (cmd === 'smoke-longrun') {
|
||||
// ━━━ 🔴🔴 ВОРОТА, НАЙДЕННЫЕ ПРОПУЩЕННЫМИ 03.08.2026 (задача О-4, находка третьего
|
||||
// помощника со стороны). Проба заводит ТОГО ЖЕ платного работника, что и `run`, а ворот
|
||||
// у неё не было НИ ОДНИХ: сверка настроек и денежные ворота стояли только в куске `run`,
|
||||
// ДО развилки на `--resume`, — а `smoke-longrun` сравнивает `cmd` ЗАНОВО, отдельной веткой,
|
||||
// и внутрь ветки `run` не заходит вовсе. Единственная команда затеи без денежных ворот
|
||||
// оказалась ровно той, которой собирались делать первое живое рождение работника.
|
||||
//
|
||||
// 🔴 ОБЕ ДВЕРИ — ТЕ ЖЕ САМЫЕ, а не вторые копии: `proverkaVladeltsa` — тот же довод главной
|
||||
// программы, что уже стоит у `run` чуть выше по файлу; `dengiNaZapusk` — дверь, вынесенную
|
||||
// ИЗ `run` специально ради этого (раньше разбор ответа замера был готовым куском кода
|
||||
// только внутри `run`, и своя копия здесь завела бы второе правило одного смысла).
|
||||
//
|
||||
// 🟡 РЕШЕНО НЕ ВЕСТИ ПРОБУ ЧЕРЕЗ ВСЕ ДЕВЯТЬ ВОРОТ обычного прогона (план, приёмка, замысел,
|
||||
// отпечаток, место на диске, потолки задач/работников, пересечение файлов, отложенная
|
||||
// задача) — только через эти две. Причина: проба не план и не задачу заводит, а один
|
||||
// работник на N минут вручную, руками, обычно когда что-то чинят; своего плана/приёмки/
|
||||
// замысла у неё нет и не будет, и требовать их означало бы придумывать бумаги под
|
||||
// диагностический инструмент. Сужение названо, а не спрятано: см. отчёт задачи.
|
||||
try {
|
||||
const slova = proverkaVladeltsa();
|
||||
for (const s of slova) console.log(s.tekst);
|
||||
} catch (err) {
|
||||
console.error('Проба НЕ запущена: настройка ночи разошлась с решением владельца дороже обещанного (либо сверку сделать не вышло — это тоже не повод молча пускать платного работника).');
|
||||
console.error(err?.message ?? String(err));
|
||||
return 1;
|
||||
}
|
||||
|
||||
const dengi = await dengiNaZapusk({ cfg, komanda: 'smoke-longrun', otkaz: 'Проба НЕ запущена' });
|
||||
if (!dengi.ok) return 1;
|
||||
const m = dengi.measure;
|
||||
recordMeasure(budgetMeasuresPath(), {
|
||||
utilization: m.utilization, status: m.status, kind: m.kind,
|
||||
resets_at: m.resets_at, at: Date.now(), phase: 'before-smoke',
|
||||
});
|
||||
|
||||
// Доказательство проверки 1: движение должно быть и через три часа после конца сессии
|
||||
// помощника. Проверками кода это не доказывается — только живым прогоном.
|
||||
const minutes = Number(flag(argv, '--minutes') ?? 200);
|
||||
|
||||
+218
-3
@@ -15,7 +15,7 @@ import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import {
|
||||
main, runMailCommand, collectSummary, runningRuns, idushchieProgony, komandaProdolzheniya, kartochkiRabotnikov,
|
||||
proverkaNastroekVladeltsa,
|
||||
proverkaNastroekVladeltsa, vorotaDeneg,
|
||||
} from './cli.mjs';
|
||||
import { readLines } from './state.mjs';
|
||||
import { razborDovodov, zamerDovody, vhodVorot, strokiOtkaza, odinVid, vyborProdolzhaemogo } from './cli-parts.mjs';
|
||||
@@ -1018,8 +1018,12 @@ describe('часть 9: отказы стоят РАНЬШЕ замера зап
|
||||
// заведомо не пройдёт, не стоит ни одного платного зова. И только при этом порядке
|
||||
// отказ можно посмотреть живьём глазами: запуск не рождает ни замера, ни угла,
|
||||
// ни живого работника.
|
||||
// 🔴 Ищем `dengiNaZapusk(`, а не `zamerSoSchyotom(`: задача О-4 (03.08.2026) вынесла разбор
|
||||
// ответа замера в общую дверь `vorotaDeneg`, и `run` теперь зовёт её, а не прежний код
|
||||
// на месте — самого имени `zamerSoSchyotom(` внутри тела `run` больше нет, оно живёт
|
||||
// внутри умолчания `vorotaDeneg` (см. описание функции), а `smoke-longrun` зовёт ТУ ЖЕ дверь.
|
||||
const iPlan = BLOK_RUN.indexOf('validatePlan(');
|
||||
const iZamer = BLOK_RUN.indexOf('zamerSoSchyotom(');
|
||||
const iZamer = BLOK_RUN.indexOf('dengiNaZapusk(');
|
||||
expect(iPlan).toBeGreaterThan(-1);
|
||||
expect(iZamer).toBeGreaterThan(-1);
|
||||
expect(iPlan).toBeLessThan(iZamer);
|
||||
@@ -1027,7 +1031,7 @@ describe('часть 9: отказы стоят РАНЬШЕ замера зап
|
||||
|
||||
it('🔴 ворота отложенной задачи стоят ДО замера и ДО `planRun`', () => {
|
||||
const iSled = BLOK_RUN.indexOf('deferredTaskGate(');
|
||||
const iZamer = BLOK_RUN.indexOf('zamerSoSchyotom(');
|
||||
const iZamer = BLOK_RUN.indexOf('dengiNaZapusk(');
|
||||
const iPlanRun = BLOK_RUN.indexOf('planRun(');
|
||||
expect(iSled).toBeGreaterThan(-1);
|
||||
expect(iZamer).toBeGreaterThan(-1);
|
||||
@@ -1344,6 +1348,217 @@ describe('О-3: ворота сверки настроек с решениями
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// О-4 (03.08.2026) — ТРЕТЬЯ ДОРОГА БЕЗ ДЕНЕГ, НАЙДЕННАЯ ТРЕТЬИМ ПОМОЩНИКОМ СО СТОРОНЫ.
|
||||
//
|
||||
// `smoke-longrun` заводит ТОГО ЖЕ платного работника, что и `run`/`run --resume`, но стоит
|
||||
// ОТДЕЛЬНОЙ веткой `if (cmd === 'smoke-longrun')` — развилку куска `run` (там, где сверка
|
||||
// настроек и денежные ворота уже стоят) она не проходит вовсе, потому что `cmd` сравнивается
|
||||
// заново, а не входит внутрь ветки `run`. Лечение: `smoke-longrun` зовёт ТЕ ЖЕ функции,
|
||||
// что и `run` — `proverkaVladeltsa()` (тот же довод главной программы, без своей копии)
|
||||
// и `vorotaDeneg()` (кусок, вынесенный ИЗ `run` сюда же: раньше разбор ответа замера лежал
|
||||
// ГОТОВЫМ КОДОМ только в теле `run`, и вторая копия для `smoke-longrun` разъехалась бы
|
||||
// с первой молча — ровно та беда, что уже случалась у `readRuns`/`file-claims`, см. заметки
|
||||
// в шапке `cli.mjs`). `run` теперь тоже зовёт `vorotaDeneg()` — дверь одна на обоих, а не своя
|
||||
// у каждого.
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
describe('О-4: vorotaDeneg — денежные ворота, вынесенные из `run`, теперь звонят оба зовущих', () => {
|
||||
it('замер бросил — отказ словами владельцу, а не следом стека', async () => {
|
||||
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const g = await vorotaDeneg({
|
||||
cfg: CFG, komanda: 'run',
|
||||
zamer: async () => { throw new Error('прибор недоступен'); },
|
||||
});
|
||||
expect(g.ok).toBe(false);
|
||||
expect(errSpy.mock.calls.map(c => String(c[0])).join('\n')).toContain('замер остатка не сделан');
|
||||
errSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('замеров за смену больше нет — отказ, причина сторожа названа', async () => {
|
||||
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const g = await vorotaDeneg({
|
||||
cfg: CFG, komanda: 'run',
|
||||
zamer: async () => ({ ok: false, reason: 'исчерпан лимит замеров за смену' }),
|
||||
});
|
||||
expect(g.ok).toBe(false);
|
||||
expect(errSpy.mock.calls.map(c => String(c[0])).join('\n')).toContain('исчерпан лимит замеров за смену');
|
||||
errSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('замер сошёлся, а остатка нет (`measure` пуст) — отказ, ноль не подставляется', async () => {
|
||||
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const g = await vorotaDeneg({
|
||||
cfg: CFG, komanda: 'run',
|
||||
zamer: async () => ({ ok: true, measure: null, problem: 'вид предела не совпал' }),
|
||||
});
|
||||
expect(g.ok).toBe(false);
|
||||
expect(errSpy.mock.calls.map(c => String(c[0])).join('\n')).toContain('вид предела не совпал');
|
||||
errSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('замер вправду посчитан — успех отдаёт измерение как есть', async () => {
|
||||
const izm = { utilization: 0.1, status: 'ok', kind: 'seven_day', resets_at: 123 };
|
||||
const g = await vorotaDeneg({ cfg: CFG, komanda: 'run', zamer: async () => ({ ok: true, measure: izm }) });
|
||||
expect(g.ok).toBe(true);
|
||||
expect(g.measure).toBe(izm);
|
||||
});
|
||||
|
||||
it('слова владельцу меняются доводом `otkaz` («Прогон»/«Проба»), а не выдуманы заново на месте', async () => {
|
||||
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
await vorotaDeneg({
|
||||
cfg: CFG, komanda: 'smoke-longrun', otkaz: 'Проба НЕ запущена',
|
||||
zamer: async () => { throw new Error('бух'); },
|
||||
});
|
||||
expect(errSpy.mock.calls.map(c => String(c[0])).join('\n')).toContain('Проба НЕ запущена: замер остатка не сделан');
|
||||
errSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('довод передаётся замеру как есть: та же `cfg`, тот же путь попыток, то же имя команды', async () => {
|
||||
const zamer = vi.fn(async () => ({ ok: true, measure: { utilization: 0 } }));
|
||||
await vorotaDeneg({ cfg: CFG, komanda: 'smoke-longrun', zamer, attemptsPath: '/тут/попытки.jsonl' });
|
||||
expect(zamer).toHaveBeenCalledWith({ cfg: CFG, attemptsPath: '/тут/попытки.jsonl', komanda: 'smoke-longrun' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('О-4: `main()` — smoke-longrun идёт через ТЕ ЖЕ ворота, что и `run`', () => {
|
||||
it('🔴 дорогое расхождение настроек останавливает пробу ДО денег: денежные ворота не зовутся вовсе', async () => {
|
||||
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const dengiSpy = vi.fn(async () => ({ ok: true, measure: { utilization: 0 } }));
|
||||
const code = await main(['node', 'cli.mjs', 'smoke-longrun'], {
|
||||
readConfig: () => CFG,
|
||||
proverkaVladeltsa: () => { throw new Error('РОВНО ЭТОТ ТЕКСТ ВЛАДЕЛЬЦУ ПРО ПРОБУ'); },
|
||||
dengiNaZapusk: dengiSpy,
|
||||
});
|
||||
const skazano = errSpy.mock.calls.map(c => String(c[0])).join('\n');
|
||||
expect(code).toBe(1);
|
||||
expect(skazano).toContain('РОВНО ЭТОТ ТЕКСТ ВЛАДЕЛЬЦУ ПРО ПРОБУ');
|
||||
expect(dengiSpy).not.toHaveBeenCalled();
|
||||
errSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('дешёвое расхождение настроек — слова печатаются, дорога идёт дальше, к деньгам', async () => {
|
||||
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
const dengiSpy = vi.fn(async () => ({ ok: false }));
|
||||
const code = await main(['node', 'cli.mjs', 'smoke-longrun'], {
|
||||
readConfig: () => CFG,
|
||||
proverkaVladeltsa: () => [{ tekst: 'МЯГКОЕ СЛОВО ПРО ПРОБУ' }],
|
||||
dengiNaZapusk: dengiSpy,
|
||||
});
|
||||
expect(logSpy.mock.calls.map(c => String(c[0]))).toContain('МЯГКОЕ СЛОВО ПРО ПРОБУ');
|
||||
expect(dengiSpy).toHaveBeenCalledWith(expect.objectContaining({ cfg: CFG, komanda: 'smoke-longrun' }));
|
||||
expect(code).not.toBe(0);
|
||||
logSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('🔴 денежные ворота отказали (`ok: false`) — проба НЕ заведена, код отличен от успеха', async () => {
|
||||
const code = await main(['node', 'cli.mjs', 'smoke-longrun'], {
|
||||
readConfig: () => CFG,
|
||||
proverkaVladeltsa: () => [],
|
||||
dengiNaZapusk: async () => ({ ok: false }),
|
||||
});
|
||||
expect(code).not.toBe(0);
|
||||
});
|
||||
|
||||
it('🔴 датчик зовущих: тело `smoke-longrun` зовёт `proverkaVladeltsa()` РАНЬШЕ `dengiNaZapusk(`', () => {
|
||||
// 🪤 Та же вторая нога, что и у куска `run` (проверка выше, часть О-3): поведенческие
|
||||
// проверки доказывают ПОВЕДЕНИЕ через подставку, эта доказывает, что подставка вправду
|
||||
// встала НА МЕСТО настоящего зова, в настоящем порядке, а не осталась неиспользуемым доводом.
|
||||
const iCmd = TEKST_CLI.indexOf("if (cmd === 'smoke-longrun') {");
|
||||
const iSverka = TEKST_CLI.indexOf('proverkaVladeltsa()', iCmd);
|
||||
const iDengi = TEKST_CLI.indexOf('dengiNaZapusk(', iCmd);
|
||||
expect(iCmd).toBeGreaterThan(-1);
|
||||
expect(iSverka).toBeGreaterThan(iCmd);
|
||||
expect(iDengi).toBeGreaterThan(iSverka);
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// О-4, ДАТЧИК КЛАССА — не «у smoke-longrun нет ворот», а «у КАКОЙ УГОДНО команды, заводящей
|
||||
// платного работника, нет ворот». Сегодня дорог оказалось три, а помнили про две — второй
|
||||
// такой случай за день (первый — сверка настроек с нулём зовущих). Ручное перечисление команд
|
||||
// глазами эту беду не лечит: четвёртая дорога послезавтра снова пройдёт мимо, и снова
|
||||
// придётся, чтобы кто-то вспомнил и проверил руками. Здесь — датчик, который САМ находит
|
||||
// в `cli.mjs` все команды и сам решает, кто из них платный.
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
// Вырезает тело каждого `if (cmd === '...') { … }` балансом фигурных скобок — а не до
|
||||
// следующего `if (cmd ===`, чтобы вложенные `if`/функции внутри не обрывали вырезку раньше
|
||||
// времени (внутри `run`, например, лежит целый вложенный `if (razbor.resumeRunId)`).
|
||||
function komandnyeBloki(tekst) {
|
||||
const bloki = [];
|
||||
const re = /if \(cmd === '([\w-]+)'\) \{/g;
|
||||
let m;
|
||||
while ((m = re.exec(tekst))) {
|
||||
const otkrytie = tekst.indexOf('{', m.index);
|
||||
let depth = 0;
|
||||
let j = otkrytie;
|
||||
for (; j < tekst.length; j++) {
|
||||
if (tekst[j] === '{') depth++;
|
||||
else if (tekst[j] === '}') { depth--; if (depth === 0) break; }
|
||||
}
|
||||
bloki.push({ cmd: m[1], telo: tekst.slice(otkrytie, j + 1) });
|
||||
}
|
||||
return bloki;
|
||||
}
|
||||
|
||||
// Имена, по которым датчик узнаёт «эта команда заводит живого ПЛАТНОГО работника» —
|
||||
// а не просто мерит остаток (`budget`) или печатает бумагу (`seal`/`approve-spec`).
|
||||
const ZAVODYAT_PLATNOGO_RABOTNIKA = ['startAll(', 'smokeLongRun(', 'komandaProdolzheniya(', 'zavestiProdolzhenie(', 'podnyat('];
|
||||
// Обе двери, обязательные у КАЖДОЙ такой команды.
|
||||
const VOROTA_OBYAZATELNYE = [
|
||||
{ imya: 'сверка настроек с решениями владельца', marker: 'proverkaVladeltsa(' },
|
||||
{ imya: 'денежные ворота', marker: 'dengiNaZapusk(' },
|
||||
];
|
||||
|
||||
describe('О-4: датчик класса сам по себе — на синтетическом тексте, не на настоящем cli.mjs', () => {
|
||||
it('поддельная команда со спавном и без единой двери — датчик её находит', () => {
|
||||
const poddelnyj = "if (cmd === 'fake-fourth') {\n const r = startAll({ runId: 'x' });\n return 0;\n}\n";
|
||||
const bloki = komandnyeBloki(poddelnyj);
|
||||
expect(bloki.map(b => b.cmd)).toEqual(['fake-fourth']);
|
||||
expect(bloki[0].telo).toContain('startAll(');
|
||||
expect(bloki[0].telo).not.toContain('proverkaVladeltsa(');
|
||||
expect(bloki[0].telo).not.toContain('dengiNaZapusk(');
|
||||
});
|
||||
|
||||
it('та же поддельная команда, но с обеими дверями — датчик видит обе', () => {
|
||||
const horoshaya = "if (cmd === 'fake-fourth') {\n proverkaVladeltsa();\n await dengiNaZapusk({ cfg, komanda: 'fake-fourth' });\n const r = startAll({ runId: 'x' });\n return 0;\n}\n";
|
||||
const bloki = komandnyeBloki(horoshaya);
|
||||
expect(bloki[0].telo).toContain('proverkaVladeltsa(');
|
||||
expect(bloki[0].telo).toContain('dengiNaZapusk(');
|
||||
});
|
||||
|
||||
it('вложенный `if` внутри команды не обрывает вырезку раньше закрывающей скобки', () => {
|
||||
const vlozhennyj = "if (cmd === 'x') {\n if (a) {\n b();\n }\n startAll({});\n}\nif (cmd === 'y') {\n z();\n}\n";
|
||||
const bloki = komandnyeBloki(vlozhennyj);
|
||||
expect(bloki.map(b => b.cmd)).toEqual(['x', 'y']);
|
||||
expect(bloki[0].telo).toContain('startAll(');
|
||||
expect(bloki[0].telo).toContain('if (a)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('О-4: датчик класса на НАСТОЯЩЕМ cli.mjs — каждая платная команда зовёт ОБЕ двери', () => {
|
||||
const bloki = komandnyeBloki(TEKST_CLI);
|
||||
const platnye = bloki.filter(b => ZAVODYAT_PLATNOGO_RABOTNIKA.some(marker => b.telo.includes(marker)));
|
||||
|
||||
it('датчик вправду нашёл все семь команд cli.mjs — не меньше и не какие-то другие', () => {
|
||||
expect(bloki.map(b => b.cmd).sort()).toEqual(
|
||||
['approve-spec', 'budget', 'mail', 'run', 'seal', 'smoke-longrun', 'summary'],
|
||||
);
|
||||
});
|
||||
|
||||
it('🔴 датчик вправду нашёл ОБЕ платных дороги — это класс, а не разбор одного места', () => {
|
||||
expect(platnye.map(b => b.cmd).sort()).toEqual(['run', 'smoke-longrun']);
|
||||
});
|
||||
|
||||
for (const vorota of VOROTA_OBYAZATELNYE) {
|
||||
it(`каждая платная команда зовёт «${vorota.imya}»`, () => {
|
||||
for (const b of platnye) {
|
||||
expect(b.telo, `команда «${b.cmd}» заводит платного работника, но не зовёт ${vorota.imya}`).toContain(vorota.marker);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// КУСОК 6, ЗАДАЧА 7 — ПОДЪЁМ ПРОДОЛЖЕНИЯ.
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user