352 lines
29 KiB
JavaScript
352 lines
29 KiB
JavaScript
// Разбор плана задачи. Пункт — заголовок «### Task N: Название».
|
|
//
|
|
// 🔴 Помету ставит ТОТ, КТО ПИШЕТ ПЛАН, а не работник на ходу — так дословно требует
|
|
// проверка 41. Отсюда и вид: помета — строка-примечание под заголовком, и ищется она
|
|
// в ближайших ТРЁХ строках. Слово «long=yes», написанное в тексте пункта, пометкой
|
|
// не считается: иначе датчик топтания выключался бы изнутри, теми же руками, за которыми
|
|
// он следит.
|
|
//
|
|
// 🔴 `tests=` — то, чем меряется закрытие пункта (проверки 38 и 39). Пусто здесь — и пункт
|
|
// закрывается по одному слову работника. `files=` держит три замка куска 3: ворота
|
|
// «задачи не пересекаются по файлам», разрез по файлам внутри задачи и список чужих файлов
|
|
// в поручении работнику.
|
|
//
|
|
// 🪤 Ни одного «наверное»: подали не текст или не список — падаем ГРОМКО. Пустышка, тихо
|
|
// отвечающая «всё хорошо», за три куска ловилась пять раз, и здесь она стоила бы дороже
|
|
// всего: ворота прочли бы пустой разбор как «в плане нет пунктов» и написали бы владельцу
|
|
// неправду о его целом плане.
|
|
|
|
// 🔴🔴 СПИСОК МЕРНЫХ ПРИБОРОВ ОДИН НА ВСЮ ЗАТЕЮ и живёт в `claims-audit.mjs`. Ввозится,
|
|
// а не переписывается: два списка разъезжаются молча — ворота пускали бы то, что сверка
|
|
// тронутых файлов потом откатывает, и ночь сгорала бы ровно так, как описано у самих ворот.
|
|
// 🔴 Кольца ЗДЕСЬ нет: `claims-audit.mjs` ввозит `file-claims.mjs` и `paths.mjs`, и ни один
|
|
// из троих не ввозит этот файл. Кольцевой ввоз есть у замка занятий — там он назван вслух
|
|
// в его же шапке и сторожится живой проверкой.
|
|
// 🪤 Долг, названный вслух: раньше `plan-model.mjs` не ввозил из затеи НИЧЕГО, и на это
|
|
// прямо ссылается примечание к ввозу в `item-audit.mjs` («кольца тут нет: plan-model
|
|
// не ввозит из затеи ничего»). Теперь ввозит — кольца по-прежнему нет, но то примечание
|
|
// устарело. Чинить его отсюда нельзя: чужой файл.
|
|
import { mernyyPribor } from './claims-audit.mjs';
|
|
|
|
// Заголовок пункта. Ровно три решётки: «####» — это уже подзаголовок внутри пункта.
|
|
const HEAD = /^###\s+Task\s+([\w.]+)\s*:\s*(.+?)\s*$/;
|
|
// Помета. Только строка-примечание целиком — кусок такого вида посреди прозы не годится.
|
|
const MARK = /^<!--\s*night:\s*(.+?)\s*-->$/;
|
|
// Сколько строк под заголовком просматриваем. Число названо в договоре о стыках дословно.
|
|
const OKNO_STROK = 3;
|
|
// Рамка-образец. До трёх пробелов отступа, три и больше кавычек ИЛИ тильд, дальше подпись.
|
|
const RAMKA = /^ {0,3}(`{3,}|~{3,})(.*)$/;
|
|
|
|
function spisok(value) {
|
|
return value.split(',').map(s => s.trim()).filter(Boolean);
|
|
}
|
|
|
|
// 🔴🔴 Где в плане КАРТИНКА пункта, а где сам пункт.
|
|
//
|
|
// Планы показывают, как выглядит пункт, — куском-образцом внутри рамки ``` … ```.
|
|
// Разбор брал эти картинки за работу: в живом плане `2026-08-01-kusok4-...md`
|
|
// он видел СЕМНАДЦАТЬ пунктов при нуле настоящих, в `...kusok3-...md` — четыре.
|
|
// Ворота при этом отвечали «план годен», а помощник соседней задачи вправду получил
|
|
// куски примеров как задание и чуть не записал ложный вывод о самих планах.
|
|
//
|
|
// 🔴 Примета — РАЗМЕТКА, а не слово: никакого «рамка», «пример», «образец» в тексте
|
|
// искать нельзя, этих слов в живых планах нет. Считаем ровно так, как размётку читает
|
|
// сам markdown, и два места читаются по-разному:
|
|
//
|
|
// 🪤 подпись рамки на кавычках не может содержать кавычку. Строка
|
|
// «```` ```verified-context-json ````» — это вставка кода В СТРОКУ, а не начало рамки.
|
|
// Простая примета «начинается с трёх кавычек» открывает на ней рамку, рамка не
|
|
// закрывается никогда — и ТРИ настоящих пункта ниже пропадают МОЛЧА. Не выдумка:
|
|
// эта строка лежит в `docs/superpowers/plans/2026-06-12-router-mentor-activation-wrapper.md`.
|
|
//
|
|
// 🪤 закрывает рамку только та же кавычка (или тильда) и не короче открывающей, и после
|
|
// неё на строке ничего нет. Иначе рамка на четырёх кавычках закрывалась бы тройкой
|
|
// изнутри себя же, и половина образца читалась бы как настоящие пункты.
|
|
//
|
|
// 🔴 Рамка, которую забыли закрыть, глотает всё до конца плана. Съела заголовки — падаем
|
|
// ГРОМКО: съесть план ЦЕЛИКОМ не страшно (ворота скажут «нет пунктов»), а вот съесть
|
|
// ПОЛОВИНУ — это ночь по урезанному плану, о которой владелец не узнает никогда.
|
|
// Не съела ничего — молчим: так кончаются семь живых планов, и ронять их не за что.
|
|
function razmetkaRamok(lines) {
|
|
const vRamke = new Array(lines.length).fill(false);
|
|
let ramka = null;
|
|
let sedeno = 0;
|
|
for (let i = 0; i < lines.length; i += 1) {
|
|
const m = RAMKA.exec(lines[i]);
|
|
if (m) {
|
|
const znak = m[1][0];
|
|
const dlina = m[1].length;
|
|
const podpis = m[2];
|
|
if (ramka) {
|
|
if (znak === ramka.znak && dlina >= ramka.dlina && podpis.trim() === '') {
|
|
ramka = null;
|
|
sedeno = 0;
|
|
} else {
|
|
vRamke[i] = true;
|
|
}
|
|
continue;
|
|
}
|
|
if (znak === '`' && podpis.includes('`')) continue; // вставка в строку, а не рамка
|
|
ramka = { znak, dlina, stroka: i + 1 };
|
|
sedeno = 0;
|
|
vRamke[i] = true;
|
|
continue;
|
|
}
|
|
if (!ramka) continue;
|
|
vRamke[i] = true;
|
|
if (HEAD.test(lines[i])) sedeno += 1;
|
|
}
|
|
if (ramka && sedeno > 0) {
|
|
throw new Error(`parsePlan: рамка-образец открыта на строке ${ramka.stroka} и не закрыта до конца плана. Внутри неё осталось пунктов: ${sedeno} — и молча взять их за примеры нельзя: с тем же успехом это настоящая работа, которую ночь просто не увидит. Закройте рамку («${ramka.znak.repeat(ramka.dlina)}» отдельной строкой) и повторите.`);
|
|
}
|
|
return vRamke;
|
|
}
|
|
|
|
export function parsePlan(text) {
|
|
if (typeof text !== 'string') {
|
|
throw new Error('parsePlan: подан не текст плана. Разбирать нечего, и молчать об этом нельзя: пустой список пунктов ворота прочли бы как «в плане нет ни одного пункта» и написали бы владельцу неправду о его плане.');
|
|
}
|
|
const lines = text.split(/\r?\n/);
|
|
const vRamke = razmetkaRamok(lines);
|
|
const items = [];
|
|
for (let i = 0; i < lines.length; i += 1) {
|
|
if (vRamke[i]) continue; // это картинка пункта в образце, а не пункт
|
|
const h = HEAD.exec(lines[i]);
|
|
if (!h) continue;
|
|
// Умолчания честные и пустые: не написано — значит пусто, а не «наверное всё».
|
|
const item = { id: h[1], title: h[2], tests: [], files: [], long: false }; const doStroki = Math.min(i + 1 + OKNO_STROK, lines.length);
|
|
for (let j = i + 1; j < doStroki; j += 1) {
|
|
// 🔴 Окно обрывается на заголовке СЛЕДУЮЩЕГО пункта. Без этого пункт без пометы
|
|
// дотягивается до пометы соседа и забирает ЕГО проверки: проверка 39 обходится
|
|
// молча, а закрытие пункта меряется чужой зеленью. Ровно от этой беды и заведено
|
|
// окно в три строки — но одного числа мало, когда пункты идут вплотную.
|
|
// 🪤 Помета ИЗ ОБРАЗЦА настоящему пункту не достаётся: мало не считать заголовок
|
|
// из рамки пунктом — отдай ему чужие `tests`, и закрытие пункта померяется
|
|
// зеленью, которой он не касался (проверка 38). Строки рамки просто пропускаем,
|
|
// окно на них не обрывая: настоящая помета может стоять и ниже образца.
|
|
if (vRamke[j]) continue;
|
|
if (HEAD.test(lines[j])) break;
|
|
const m = MARK.exec(lines[j].trim());
|
|
if (!m) continue;
|
|
for (const part of m[1].split(';')) {
|
|
const [rawKey, ...rest] = part.split('=');
|
|
const key = rawKey.trim();
|
|
// Склеиваем обратно: в значении может стоять свой знак «=».
|
|
const value = rest.join('=').trim();
|
|
if (key === 'tests') item.tests = spisok(value);
|
|
if (key === 'files') item.files = spisok(value);
|
|
if (key === 'long') item.long = value.toLowerCase() === 'yes';
|
|
}
|
|
break; // первая помета под заголовком — она и есть помета этого пункта
|
|
}
|
|
items.push(item);
|
|
}
|
|
return items;
|
|
}
|
|
|
|
export function validatePlan(items) {
|
|
if (!Array.isArray(items)) {
|
|
throw new Error('validatePlan: подан не список пунктов. Это сбой зова, а не свойство плана: промахнись зовущий мимо разбора — и «ok» пустило бы в работу план, которого никто не смотрел.');
|
|
}
|
|
const items_count = items.length;
|
|
const long_count = items.filter(i => i?.long === true).length;
|
|
|
|
if (items_count === 0) {
|
|
return { ok: false, reason: 'В плане нет ни одного пункта — запускать нечего. Пункт плана — это заголовок вида «### Task 1: Название».', items_count, long_count };
|
|
}
|
|
|
|
// 🔴🔴 Номер пункта — это КЛЮЧ, а не подпись. По нему ведут очередь, по нему запирают
|
|
// файлы, им работник заявляет «закрыл». Два пункта под одним номером — двое работников
|
|
// закроют друг друга, и оба отчитаются ПРАВДОЙ: каждый закрыл «Task 13».
|
|
//
|
|
// 🔴 Стоит РАНЬШЕ всех остальных отказов намеренно. Ниже виновных зовут по имени
|
|
// («Task 1»), и пока ключ не уникален, любое такое имя двусмысленно: владелец не
|
|
// поймёт, который из двух чинить. Сперва уникальность ключа — потом всё, что на нём
|
|
// держится.
|
|
const schyot = new Map();
|
|
for (const i of items) {
|
|
const id = String(i?.id ?? '?');
|
|
schyot.set(id, (schyot.get(id) ?? 0) + 1);
|
|
}
|
|
const povtory = [...schyot].filter(([, n]) => n > 1);
|
|
if (povtory.length) {
|
|
const imena = povtory.map(([id, n]) => `Task ${id} (${n} раза)`).join(', ');
|
|
return {
|
|
ok: false,
|
|
reason: `В плане повторяются номера пунктов: ${imena}. Номер — ключ, по которому ведут очередь, запирают файлы и заявляют «пункт закрыт»; двое работников под одним номером закроют друг друга, и оба отчитаются правдой. Перенумеруйте так, чтобы каждый номер встречался ровно один раз.`,
|
|
items_count,
|
|
long_count,
|
|
};
|
|
}
|
|
|
|
// 🔴🔴 МЕРНЫЙ ПРИБОР В `files=` ПУНКТА — ПЛАНА НЕТ.
|
|
//
|
|
// 🔴 ЗАЧЕМ ЭТО ЗДЕСЬ, А НЕ ТОЛЬКО В ЗАМКЕ. Беда ловится в трёх местах, и это самое
|
|
// дешёвое из них: владелец правит план утром за минуту. Замерено 02.08.2026, вот путь
|
|
// без этих ворот: план с `package.json` в `files=` признаётся годным (про приборы ворота
|
|
// не знали) → работник честно занимает файл (занятия отвечают «можно»: у прибора нет
|
|
// хозяина) → работник правит и коммитит → сверка тронутых файлов видит прибор, ОТКАТЫВАЕТ
|
|
// правку и ОТБИРАЕТ пункт → работник берёт тот же пункт снова, и так каждые полминуты
|
|
// до утра. Владелец читает «работал восемь часов» и видит ноль сделанного. Это ХУЖЕ
|
|
// отказа: отказ виден сразу, а такая ночь сгорает МОЛЧА и выглядит как работа.
|
|
//
|
|
// 🔴 СТОИТ РАНЬШЕ отказа «пункт без проверок» намеренно: тот пункт владелец и так
|
|
// не запустит, а этот — единственный, который выглядит запущенным и работающим.
|
|
//
|
|
// 🟡 ГРАНИЦА, названная вслух: ворота видят только `files=`. Напиши автор плана «правь
|
|
// package.json» СЛОВАМИ в тексте пункта, а в `files=` поставь другой файл — ворота
|
|
// не увидят ничего, и беду поймает только сверка тронутых файлов, то есть уже ночью.
|
|
// Текст пункта здесь не разбирается вовсе, и заводить его разбор ради этого дороже,
|
|
// чем беда: `files=` — это то, чем пункт запирается, и правит работник ровно его.
|
|
const spribolom = [];
|
|
for (const i of items) {
|
|
for (const f of Array.isArray(i?.files) ? i.files : []) {
|
|
const pribor = mernyyPribor(f);
|
|
if (pribor) spribolom.push(`Task ${i?.id ?? '?'} → ${f} (${pribor.chem})`);
|
|
}
|
|
}
|
|
if (spribolom.length) {
|
|
return {
|
|
ok: false,
|
|
reason: `В плане есть пункты, которые правят мерный прибор — файл, которым хозяйство меряет само себя: ${spribolom.join('; ')}. Такой пункт не «упадёт», он сгорит молча: работник займёт файл, будет править его всю ночь, а сверка каждый раз откатит правку и отберёт пункт — утром в сводке будет «работал восемь часов» и ноль сделанного. Уберите эти файлы из списка файлов пункта (помета «files=»); если работа вправду в них — такую ночь ведут при вас, а не без вас.`,
|
|
items_count,
|
|
long_count,
|
|
};
|
|
}
|
|
|
|
// 🔴🔴 ПУНКТ, ПРОВЕРКИ КОТОРОГО РАЗНОГО РОДА. Довод и живой замер — в шапке
|
|
// `razdvoenieProverok`; здесь только отказ словами владельца.
|
|
//
|
|
// 🔴 ЗАЧЕМ ЭТО ЗДЕСЬ, А НЕ ТОЛЬКО У ЗАМКА — довод тот же, что и у мерного прибора выше:
|
|
// это самое дешёвое из мест. Владелец правит план утром за минуту, и правка простая —
|
|
// разбить пункт надвое. Оставь мы одну защиту у замка — план прошёл бы ворота, работник
|
|
// взял бы пункт, сделал и закрыл, а замок каждый раз отвечал бы «замерить нечем»: пункт
|
|
// не засчитан, работник берёт его снова, и так до утра. Ночь сгорает молча и выглядит
|
|
// как работа — та самая беда, ради которой заведены ворота на приборы.
|
|
//
|
|
// 🔴 СТОИТ РАНЬШЕ отказа «пункт без проверок» намеренно. Обе беды из одной семьи —
|
|
// «пункт нечем померить», — но пункт без проверок видно глазами, а этот выглядит
|
|
// безупречно: проверки названы, имена настоящие, и до сего дня он вправду зеленел.
|
|
// Первым называем менее очевидного.
|
|
const smeshannye = [];
|
|
for (const i of items) {
|
|
const r = razdvoenieProverok(i);
|
|
if (r) smeshannye.push(`Task ${i?.id ?? '?'} (продукт: ${r.php.join(', ')}; затея: ${r.nashi.join(', ')})`);
|
|
}
|
|
if (smeshannye.length) {
|
|
return {
|
|
ok: false,
|
|
reason: `В плане есть пункты, проверки которых разного рода, а одной командой два рода не гоняются: ${smeshannye.join('; ')}. Проверки продукта («.php») запускает одна программа, проверки самой затеи — другая, и чужую каждая из них молча пропускает: замерено — из двух названных проверок отрабатывает одна, ответ выходит без единой поломки, и про вторую не говорится ни слова. Такой пункт засчитывался бы наполовину и молча. Разбейте его на два пункта: в один соберите проверки продукта, в другой — проверки затеи.`,
|
|
items_count,
|
|
long_count,
|
|
};
|
|
}
|
|
|
|
// Проверка 39б: пункт, для которого не записано ни одной проверки, закрыть нельзя,
|
|
// значит и запускать по такому плану нечего. Виновные названы ВСЕ разом: владелец
|
|
// чинит план по этому списку, и получать отказ по одному пункту за ночь ему незачем.
|
|
const golye = items.filter(i => !Array.isArray(i?.tests) || i.tests.length === 0);
|
|
if (golye.length) {
|
|
const imena = golye.map(i => `Task ${i?.id ?? '?'}`).join(', ');
|
|
return {
|
|
ok: false,
|
|
reason: `В плане есть пункты, для которых не записано ни одной проверки: ${imena}. Закрытие пункта меряется зелёными проверками именно этого пункта, а их не названо — закрыть такой пункт нечем. План не годится.`,
|
|
items_count,
|
|
long_count,
|
|
};
|
|
}
|
|
|
|
// Проверка 41: длинными помечено больше половины — плана к запуску нет.
|
|
if (long_count * 2 > items_count) {
|
|
return {
|
|
ok: false,
|
|
reason: `Длинными помечено ${long_count} пунктов из ${items_count} — больше половины. Так выключается единственный датчик топтания, и запуска по такому плану нет. Пометку ставит тот, кто пишет план: перечитайте, вправду ли все эти пункты длинные.`,
|
|
items_count,
|
|
long_count,
|
|
};
|
|
}
|
|
|
|
return { ok: true, reason: null, items_count, long_count };
|
|
}
|
|
|
|
// 🔴🔴 РОД ПРОВЕРКИ — ЧЕМ ЕЁ ВПРАВДУ ГОНЯЮТ. Родов ровно два, и это не деление ради
|
|
// деления: php-шную проверку запускает прогон продукта (`php artisan test`), нашу —
|
|
// прогон затеи (`npm run test:tools`), и ЧУЖУЮ каждый из них не запускает вовсе.
|
|
// Всё, что не кончается на `.php`, считается нашим: наш прогон принимает имя как строку
|
|
// отбора, и «наше» тут — это «то, что ищется среди наших файлов проверок».
|
|
// 🪤 Регистр не важен намеренно: строгое сравнение отправило бы «A.PHP» в наш прогон,
|
|
// то есть ровно в ту беду, от которой всё это и заведено.
|
|
export function rodProverki(t) {
|
|
return /\.php$/i.test(String(t)) ? 'php' : 'nash';
|
|
}
|
|
|
|
// 🔴🔴 ПРОВЕРКИ ПУНКТА РАЗНОГО РОДА — ОДНОЙ КОМАНДОЙ ИХ НЕ ПОМЕРИТЬ.
|
|
//
|
|
// 🔴 ЗАМЕРЕНО ЖИВЬЁМ 02.08.2026, а не выведено рассуждением. Прежнее правило смешанный
|
|
// набор отдавало нашему прогону целиком, с доводом «`php artisan test` наш `.mjs` не
|
|
// запустит вовсе». Довод верен ровно наполовину — и этой половиной опасен. Выполнено руками:
|
|
// npm run test:tools -- app/tests/Feature/ProjectSyncStatusFromPivotTest.php \
|
|
// tools/night/plan-fingerprint.test.mjs
|
|
// Ответ: «Test Files 1 passed (1) · Tests 15 passed (15)», код 0. Из ДВУХ названных
|
|
// проверок отработала ОДНА. Имя php-шного файла наш прогон берёт за строку отбора,
|
|
// ни одного файла по ней не находит и молча идёт дальше — потому что второе имя что-то нашло.
|
|
//
|
|
// 🔴 ПОЧЕМУ ЭТО ХУЖЕ ПАДЕНИЯ. Прогон вышел с нулём И что-то напечатал — значит и заслон
|
|
// «программа ничего не сказала» (`item-audit.mjs`) его не ловит. Замок объявляет проверки
|
|
// зелёными и засчитывает пункт, у которого половина проверок не запускалась. Владелец
|
|
// наутро читает «проверки прогнаны надзирателем и зелены» — про бэкенд, которого не мерил
|
|
// никто. 🪤 Тихо это ТОЛЬКО у смешанного: набор из одних php-шных имён, поданный тому же
|
|
// прогону, отвечает кодом 1 («No test files found») — замерено отдельно.
|
|
//
|
|
// 🟡 ГРАНИЦА, НАЗВАННАЯ ВСЛУХ. Родов здесь два, и второй — «всё остальное». Напиши автор
|
|
// плана в `tests=` имя, которое не находит НИ ОДНОГО нашего файла (опечатка, `.py`, просто
|
|
// слово), — рядом с настоящей нашей проверкой оно так же молча пропадёт, и здесь это не
|
|
// ловится. Ловить это значит требовать, чтобы каждое имя вправду что-то находило, а такого
|
|
// прибора у нас нет: наш прогон отвечает одним кодом на весь набор. Долг записан, не закрыт.
|
|
//
|
|
// 🔴🔴 «ТРЕТЬЕ», КОТОРОЕ СЮДА ПРИХОДИТ. Пункт попадает к замку не только из разбора плана,
|
|
// но и с диска — из карточки работника. Порченый или правленный рукой JSON даёт `tests`
|
|
// строкой, а не списком, и первый вид этой защиты падал на такой строке `TypeError`-ом
|
|
// ВЫШЕ того места, где замок ловит сбои прогона: вместо «пункт не засчитан» умирал бы
|
|
// надзиратель, и не на один пункт, а на весь остаток ночи. Замерено 02.08.2026.
|
|
// Ответ здесь — «не мой вопрос»: родов не видно, судить нечего, молчим. Негодный `tests`
|
|
// ловят двое других — ворота плана (там `Array.isArray` прописан прямо) и сам замок,
|
|
// у которого этот сбой попадает в разбор «замерить нечем».
|
|
export function razdvoenieProverok(item) {
|
|
if (!Array.isArray(item?.tests)) return null;
|
|
const tests = item.tests.filter(Boolean).map(String);
|
|
const php = tests.filter(t => rodProverki(t) === 'php');
|
|
const nashi = tests.filter(t => rodProverki(t) !== 'php');
|
|
if (!php.length || !nashi.length) return null;
|
|
return { php, nashi };
|
|
}
|
|
|
|
// Чем гонять проверки ИМЕННО ЭТОГО пункта.
|
|
//
|
|
// 🔴🔴 Строится ИЗ его `tests`, и это сердце проверки 38. Возьми правило общий набор —
|
|
// «зелень по этому пункту» стала бы «зеленью вообще», и пункт засчитывался бы за чужую
|
|
// работу при полностью зелёных проверках. Ломается ТИХО.
|
|
//
|
|
// 🔴 Правило лежит здесь, а не рядом с командной строкой, потому что нужно оно ДВОИМ:
|
|
// `cli.mjs` — чтобы сказать работнику, чем меряется его первый пункт (задача 6), и
|
|
// надзирателю — чтобы гонять проверки КАЖДОГО судимого пункта (задача 5). Второй дороги
|
|
// к этому правилу не заводить.
|
|
//
|
|
// 🪤 Здесь мы только СТРОИМ строку. Программу по полному пути ищет замок
|
|
// (`progonProverokPunkta`, задача 4) — голым именем отсюда ничего не зовётся.
|
|
export function komandaProverokPunkta(item) {
|
|
const tests = (item?.tests ?? []).filter(Boolean).map(String);
|
|
if (!tests.length) return null;
|
|
// 🔴 Разного рода — команды НЕТ. Довод — в шапке `razdvoenieProverok`. Пустой ответ здесь
|
|
// читается замком как «замерить нечем» и пункт НЕ засчитывается: это единственный честный
|
|
// ответ, пока команда одна, а родов два.
|
|
if (razdvoenieProverok(item)) return null;
|
|
const phpshnye = tests.filter(t => rodProverki(t) === 'php');
|
|
// Все php-шные — гоним прогоном продукта по именам.
|
|
if (phpshnye.length === tests.length) {
|
|
const imena = phpshnye.map(t => t.split(/[\\/]/).pop().replace(/\.php$/i, ''));
|
|
return `php artisan test --filter=${imena.join('|')}`;
|
|
}
|
|
return `npm run test:tools -- ${tests.join(' ')}`;
|
|
}
|