mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-08-28 22:19:41 +03:00
На Windows fs.rmSync и fs.cpSync молча не делают ничего, когда не-ASCII символы есть в самом аргументе пути (nodejs/node#61067). Для аудитории проекта это боевой сценарий: кириллическое имя пользователя даёт кириллический %TEMP%, где раннер создаёт воркспейсы, плюс кириллические имена объектов 1С в путях внутри кейсов. Замерено на восьми сборках. Затронуты 22.18.0–22.23.2 (последняя LTS Jod) и 24.12.0–24.14.0; исправны 22.15.1, 24.15.0+, 25.x, 26.x. Фикс приехал в fs-слой Node 24.15 и в ветку 22.x не бэкпортирован, поэтому «обновить Node» вопрос не закрывает. Зависимость не монотонна по версиям (24.14 чинит rmSync, но не cpSync) — гард по номеру версии невозможен, решает только сам путь. Симптомы: rmSync молча ничего не удаляет; cpSync с не-ASCII приёмником молча ничего не копирует; cpSync с не-ASCII источником валит процесс нативно (0xC0000409) мимо try/catch. Не затронуты mkdirSync, readdirSync, lstatSync, copyFileSync (включая перезапись), unlinkSync, rmdirSync — на них стоит обход. Что ломалось: под кириллическим %TEMP% фикстуры не доезжали до воркспейсов (meta-info — 15 ложных падений из 26, один кейс ложно-зелёный на пустом воркспейсе); cfe-validate/module-state-flag-without-file был красным даже при ASCII %TEMP% (кириллица в самом deletePath); --update-snapshots молча не сносил старый эталон, то есть портил коммитимые артефакты. Реализация — единый fsutil в двух побайтно одинаковых копиях (tests/common/ и внутри автономного навыка web-test), раскатанный на все 42 точки вызова в 11 файлах: tests/skills/*, tests/web-test/*, hooks/test/run.mjs, движок web-test. Предикат судит по resolve(p), а не по строке аргумента: относительный ASCII-аргумент при не-ASCII cwd платформа роняет так же молча. Ретраи доживают до ручного обхода, перезапись как у cpSync с force: true, настоящие ошибки не глотаются, выживший после удаления путь — громкая ошибка. Гард tests/skills/check-nonascii-fs.mjs в check-all.mjs проверяет хелпер, а не платформу (поэтому зелёный и на исправной Node), сверяет хеши обеих копий и печатает справкой состояние текущей сборки. Co-Authored-By: androman.pro <5669019+andromanpro@users.noreply.github.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
117 lines
6.6 KiB
JavaScript
117 lines
6.6 KiB
JavaScript
// tests/common/fsutil v1.0 — удаление и копирование, устойчивые к не-ASCII путям на Windows.
|
||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||
|
||
// ─── fsutil:begin ───────────────────────────────────────────────────────────
|
||
// Ниже этого маркера файл побайтно совпадает со второй копией. Расхождение ловит
|
||
// tests/skills/check-nonascii-fs.mjs. Правку вносим в обе копии сразу.
|
||
|
||
import { cpSync, copyFileSync, existsSync, lstatSync, mkdirSync, readdirSync,
|
||
rmSync, rmdirSync, unlinkSync } from 'node:fs';
|
||
import { join, resolve } from 'node:path';
|
||
|
||
// На Windows fs.rmSync и fs.cpSync ломаются, когда не-ASCII символы есть в САМОМ аргументе
|
||
// пути (nodejs/node#61067, апстрим открыт). Снято на стенде из восьми сборок:
|
||
//
|
||
// сборка rmSync cpSync → приёмник cpSync ← источник
|
||
// 22.15.1 ок ок ок
|
||
// 22.18.0 … 22.23.2 (LTS Jod) ок молча ничего краш 0xC0000409
|
||
// 24.12.0, 24.13.0 молча молча ничего краш
|
||
// 24.14.0 ок молча ничего краш
|
||
// 24.15.0, 24.19.0, 25.9.0, 26.7.0 ок ок ок
|
||
//
|
||
// Это НЕ обход одного старого релиза: ветка 22 LTS сломана до сих пор — фикс приехал в
|
||
// fs-слой Node 24.15 и в 22.x не бэкпортирован, а README проекта рекомендует «Node.js 18+».
|
||
// Зависимость не монотонна по версиям (24.14 чинит rmSync, но не cpSync), поэтому гард по
|
||
// номеру версии невозможен в принципе: решает только сам путь. Снимать гард можно, лишь
|
||
// когда исправной станет вся поддерживаемая линейка, а не текущая машина разработчика.
|
||
//
|
||
// mkdirSync, readdirSync, lstatSync, existsSync, copyFileSync (включая перезапись),
|
||
// unlinkSync, rmdirSync с не-ASCII исправны на ВСЕХ сломанных сборках — на них и стоит
|
||
// ручной обход. Асинхронные fs.promises.rm/cp не затронуты.
|
||
|
||
const NON_ASCII = /[^\x00-\x7F]/;
|
||
const RETRY_CODES = new Set(['EBUSY', 'EPERM', 'ENOTEMPTY', 'EMFILE']);
|
||
|
||
// Проверяем РАЗРЕШЁННЫЙ путь, а не строку аргумента: относительный ASCII-аргумент при
|
||
// не-ASCII cwd платформа роняет так же молча, а по строке аргумента это не видно.
|
||
export function pathIsUnsafe(p) {
|
||
// Тест-шов только для check-nonascii-fs.mjs: без него ручной обход никогда не исполняется
|
||
// на macOS/Linux и остаётся непокрытым. В обычном прогоне переменная не задана.
|
||
if (process.env.CC1C_FSUTIL_FORCE_WALK === "1") return true;
|
||
return process.platform === 'win32' && NON_ASCII.test(resolve(p));
|
||
}
|
||
|
||
function sleepSync(ms) {
|
||
if (ms > 0) Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
||
}
|
||
|
||
function withRetrySync(fn, maxRetries, retryDelay) {
|
||
for (let attempt = 0; ; attempt++) {
|
||
try { return fn(); }
|
||
catch (e) {
|
||
if (attempt >= maxRetries || !RETRY_CODES.has(e.code)) throw e;
|
||
sleepSync(retryDelay);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Симлинк или junction на каталог: unlinkSync на нём даёт EPERM, снимается rmdirSync.
|
||
// Внутрь не заходим — rmSync тоже сносит саму ссылку, а не её цель.
|
||
function removeLeafSync(p) {
|
||
try { unlinkSync(p); }
|
||
catch (e) {
|
||
if (e.code !== 'EPERM' && e.code !== 'EISDIR') throw e;
|
||
rmdirSync(p);
|
||
}
|
||
}
|
||
|
||
function removeTreeWalkSync(dir, maxRetries, retryDelay) {
|
||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||
const child = join(dir, entry.name);
|
||
if (entry.isDirectory()) removeTreeWalkSync(child, maxRetries, retryDelay);
|
||
else withRetrySync(() => removeLeafSync(child), maxRetries, retryDelay);
|
||
}
|
||
withRetrySync(() => rmdirSync(dir), maxRetries, retryDelay);
|
||
}
|
||
|
||
// Семантика rm -rf: файл, каталог или отсутствующий путь. opts.maxRetries / opts.retryDelay
|
||
// работают на обоих путях — на быстром их принимает сам rmSync, на ручном обходе повторяем сами.
|
||
export function removePathSync(target, opts = {}) {
|
||
const { maxRetries = 0, retryDelay = 100 } = opts;
|
||
|
||
if (!pathIsUnsafe(target)) {
|
||
rmSync(target, { recursive: true, force: true, maxRetries, retryDelay });
|
||
// Тихий отказ ровно здесь и есть болезнь: подтверждаем результат одним existsSync.
|
||
if (!existsSync(target)) return;
|
||
} else if (!existsSync(target)) {
|
||
return;
|
||
}
|
||
|
||
if (lstatSync(target).isDirectory()) removeTreeWalkSync(target, maxRetries, retryDelay);
|
||
else withRetrySync(() => removeLeafSync(target), maxRetries, retryDelay);
|
||
|
||
if (existsSync(target)) throw new Error(`Не удалось удалить путь: ${target}`);
|
||
}
|
||
|
||
function copyTreeWalkSync(src, dest) {
|
||
mkdirSync(dest, { recursive: true });
|
||
for (const entry of readdirSync(src, { withFileTypes: true })) {
|
||
const from = join(src, entry.name);
|
||
const to = join(dest, entry.name);
|
||
// Перезаписываем безусловно — как cpSync с дефолтным force: true.
|
||
if (entry.isDirectory()) copyTreeWalkSync(from, to);
|
||
else copyFileSync(from, to);
|
||
}
|
||
}
|
||
|
||
// Рекурсивная копия КАТАЛОГА с перезаписью. Для одиночного файла хватает copyFileSync:
|
||
// он не затронут и работает с не-ASCII на всех сборках.
|
||
export function copyTreeSync(src, dest) {
|
||
if (!pathIsUnsafe(src) && !pathIsUnsafe(dest)) {
|
||
// Ошибки не глотаем: настоящий ENOENT/EACCES должен долететь до вызывающего.
|
||
cpSync(src, dest, { recursive: true });
|
||
return;
|
||
}
|
||
copyTreeWalkSync(src, dest);
|
||
}
|