infra: portable Windows gate, CI toolchain pins, task packet, deterministic wait, gate:small --smokes

Windows portability (the three red tests on the owner's machine at green CI):
- scripts/spawn-portable.mjs: isMainModule via pathToFileURL (the
  `file://${argv[1]}` form gives file:///C:/C:/... and the CLI stays silent);
  portableCommand — a shell only for npm/npx/.cmd, node and git run directly
  (spawn via shell dropped the quotes of `node -e "…"`). Applied to
  classify-changes, mutation-gate-report, review-doc-guard, check-inputs,
  merge-candidate, gate-small, rebase-on-dev.
- the rebase-on-dev test pins core.autocrlf=false / core.eol=lf through
  GIT_CONFIG_* for its temp repository instead of touching the user's git
  config; test/windows-portability.test.mjs forbids both anti-patterns.

Pins: scripts/toolchain-pins.mjs reads Node/Python from validate.yml, the HA
stack from tests_backend/requirements.txt, Playwright/Chromium from the
lockfile — no second dictionary; `npm run toolchain:check` compares the
machine; .nvmrc/.python-version are derived and tested equal;
scripts/wsl-setup.sh provisions WSL/Linux with those pins.

scripts/task-packet.mjs: one derived view of an issue (status, rights, owner
decisions, branch vs dev, Validate on the tip, previous verdict with recorded
tree, AC → evidence, unwitnessed). scripts/wait-verdict.mjs: polls labels,
pipeline comments and optionally Validate, prints only on state change, exit
0/3/4, writes nothing.

gate:small --smokes: after build the browser phase runs bundle-sync and then
the directly matched and registered smokes, two at a time; broad matches stay
with the reviewer. package.json changed, so the bundle is rebuilt here.

Issue: #496
User-Visible: no
This commit is contained in:
Claude
2026-09-09 01:11:24 +03:00
parent 6e95c649e6
commit f0c8ae24ba
69 changed files with 1327 additions and 365 deletions
+28
View File
@@ -0,0 +1,28 @@
// Переносимый запуск команд (#496).
//
// Две Windows-ловушки, из-за которых тесты инфраструктуры падали на машине
// владельца при зелёном Linux CI:
//
// 1. `new URL(\`file://${process.argv[1]}\`)` для `C:\...\x.mjs` даёт
// `file:///C:/C:/...` — сравнение с `import.meta.url` ложно, CLI молчит.
// Правильно — `pathToFileURL`, он знает про буквы дисков.
// 2. `spawn(cmd, args, { shell: true })` склеивает аргументы в строку без
// экранирования: `node -e "…"` с кавычками и пробелами разваливается. Оболочка
// нужна ТОЛЬКО для `.cmd`/`.bat` (Node ≥ 18.20 иначе бросает EINVAL), то есть
// для `npm` на Windows; `node` и `git` запускаются напрямую.
import { pathToFileURL } from 'node:url';
/** Скрипт запущен как CLI, а не импортирован (переносимая форма). */
export function isMainModule(metaUrl, argv1 = process.argv[1]) {
if (!argv1) return false;
try { return pathToFileURL(argv1).href === metaUrl; } catch { return false; }
}
/** Что и как запускать: `{ cmd, shell }` для `spawn`/`spawnSync`. */
export function portableCommand(cmd, platform = process.platform) {
if (platform !== 'win32') return { cmd, shell: false };
if (cmd === 'npm' || cmd === 'npx') return { cmd: `${cmd}.cmd`, shell: true };
if (/\.(cmd|bat)$/i.test(cmd)) return { cmd, shell: true };
return { cmd, shell: false };
}