mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-08-29 22:49: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>
123 lines
5.3 KiB
JavaScript
123 lines
5.3 KiB
JavaScript
// web-test _suite-root/check v1.0 — offline verdict for the suite-root resolver
|
|
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
//
|
|
// findSuiteRoot decides where `webtest.config.mjs` and `_hooks.mjs` are loaded from. Getting it
|
|
// wrong is silent: the run picks up someone else's hooks, or none at all, and still goes green.
|
|
// Every rule of the climb is pinned here on throw-away trees — no 1C stand, no browser.
|
|
//
|
|
// node tests/web-test/_suite-root/check.mjs
|
|
//
|
|
// Exit codes: 0 — resolver behaves; 1 — a rule regressed.
|
|
import { mkdirSync, writeFileSync } from 'fs';
|
|
import { resolve, dirname } from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
import { tmpdir } from 'os';
|
|
// fs.rmSync/fs.cpSync are never called directly: on Windows they silently do nothing when
|
|
// the path argument contains non-ASCII characters. Build matrix and details live in the module.
|
|
import { removePathSync } from '../../common/fsutil.mjs';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const REPO = resolve(__dirname, '../../..');
|
|
const { findSuiteRoot } = await import(
|
|
'file:///' + resolve(REPO, '.claude/skills/web-test/scripts/cli/test-runner/suite-root.mjs').replace(/\\/g, '/')
|
|
);
|
|
|
|
const BASE = resolve(tmpdir(), 'web-test-suite-root-check');
|
|
removePathSync(BASE);
|
|
|
|
/** Build a tree from a list of relative paths; a path ending in `/` is a dir, otherwise a file. */
|
|
function tree(name, entries) {
|
|
const root = resolve(BASE, name);
|
|
for (const e of entries) {
|
|
const full = resolve(root, e);
|
|
if (e.endsWith('/')) mkdirSync(full, { recursive: true });
|
|
else { mkdirSync(dirname(full), { recursive: true }); writeFileSync(full, '// fixture\n'); }
|
|
}
|
|
return root;
|
|
}
|
|
|
|
let failed = 0;
|
|
function check(label, actual, expected) {
|
|
const a = actual === null ? 'null' : actual;
|
|
const e = expected === null ? 'null' : expected;
|
|
if (a === e) { console.log(` ok ${label}`); return; }
|
|
console.log(` FAIL ${label}\n expected: ${e}\n actual: ${a}`);
|
|
failed++;
|
|
}
|
|
const rootOf = (r) => (r ? r.root : null);
|
|
|
|
// 1. Config one level up — the IRG case: `test tests/irg/00-smoke/`.
|
|
{
|
|
const t = tree('t1', ['suite/webtest.config.mjs', 'suite/00-smoke/x.test.mjs']);
|
|
check('config one level up', rootOf(findSuiteRoot(resolve(t, 'suite/00-smoke'), { cwd: t })), resolve(t, 'suite'));
|
|
}
|
|
|
|
// 2. Two levels up, and the marker is _hooks.mjs only — a suite with no config still resolves,
|
|
// otherwise its stand preparation would be silently skipped.
|
|
{
|
|
const t = tree('t2', ['suite/_hooks.mjs', 'suite/a/b/x.test.mjs']);
|
|
check('hooks-only marker, two levels up', rootOf(findSuiteRoot(resolve(t, 'suite/a/b'), { cwd: t })), resolve(t, 'suite'));
|
|
}
|
|
|
|
// 3. A file path resolves from its own directory.
|
|
{
|
|
const t = tree('t3', ['suite/webtest.config.mjs', 'suite/a/x.test.mjs']);
|
|
check('file path → its directory', rootOf(findSuiteRoot(resolve(t, 'suite/a/x.test.mjs'), { cwd: t })), resolve(t, 'suite'));
|
|
}
|
|
|
|
// 4. Nearest marker wins — a nested suite is not swallowed by its parent (the `_hang/` case).
|
|
{
|
|
const t = tree('t4', ['suite/webtest.config.mjs', 'suite/inner/webtest.config.mjs', 'suite/inner/x.test.mjs']);
|
|
check('nearest marker wins', rootOf(findSuiteRoot(resolve(t, 'suite/inner'), { cwd: t })), resolve(t, 'suite/inner'));
|
|
}
|
|
|
|
// 5-6. Boundaries stop the climb: a config above `.git` / `.v8-project.json` is NOT ours.
|
|
{
|
|
const t = tree('t5', ['webtest.config.mjs', 'proj/.git/', 'proj/a/x.test.mjs']);
|
|
check('.git bounds the climb', rootOf(findSuiteRoot(resolve(t, 'proj/a'), { cwd: t })), null);
|
|
}
|
|
{
|
|
const t = tree('t6', ['webtest.config.mjs', 'proj/.v8-project.json', 'proj/a/x.test.mjs']);
|
|
check('.v8-project.json bounds the climb', rootOf(findSuiteRoot(resolve(t, 'proj/a'), { cwd: t })), null);
|
|
}
|
|
|
|
// 7. The boundary directory is itself examined — a marker sitting next to `.git` is found.
|
|
{
|
|
const t = tree('t7', ['proj/.git/', 'proj/webtest.config.mjs', 'proj/a/x.test.mjs']);
|
|
check('boundary dir is inclusive', rootOf(findSuiteRoot(resolve(t, 'proj/a'), { cwd: t })), resolve(t, 'proj'));
|
|
}
|
|
|
|
// 8. With no repo marker, cwd bounds the climb — nothing above the working directory is picked up.
|
|
{
|
|
const t = tree('t8', ['webtest.config.mjs', 'work/a/x.test.mjs']);
|
|
check('cwd bounds the climb', rootOf(findSuiteRoot(resolve(t, 'work/a'), { cwd: resolve(t, 'work') })), null);
|
|
}
|
|
|
|
// 9. cwd is inclusive too.
|
|
{
|
|
const t = tree('t9', ['work/webtest.config.mjs', 'work/a/x.test.mjs']);
|
|
check('cwd is inclusive', rootOf(findSuiteRoot(resolve(t, 'work/a'), { cwd: resolve(t, 'work') })), resolve(t, 'work'));
|
|
}
|
|
|
|
// 10. No marker anywhere → null, and the caller falls back to the passed directory.
|
|
{
|
|
const t = tree('t10', ['proj/.git/', 'proj/a/x.test.mjs']);
|
|
check('no marker → null', rootOf(findSuiteRoot(resolve(t, 'proj/a'), { cwd: t })), null);
|
|
}
|
|
|
|
// 11. Real repo: this fixture's own directory carries the markers, so `nested/` climbs one level.
|
|
{
|
|
const here = resolve(REPO, 'tests/web-test/_suite-root/nested');
|
|
check('repo fixture: nested/ → _suite-root/', rootOf(findSuiteRoot(here, { cwd: REPO })), dirname(here));
|
|
}
|
|
|
|
// 12. Real repo: the flat suite resolves to itself.
|
|
{
|
|
const suite = resolve(REPO, 'tests/web-test');
|
|
check('repo: tests/web-test/ → itself', rootOf(findSuiteRoot(suite, { cwd: REPO })), suite);
|
|
}
|
|
|
|
removePathSync(BASE);
|
|
console.log(failed ? `\n${failed} check(s) FAILED\n` : '\nall checks passed\n');
|
|
process.exit(failed ? 1 : 0);
|