mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-07-22 12:41:02 +03:00
fix(web-test): резолвить корень сьюта подъёмом вверх, а не от переданного пути
Конфиг и хуки резолвились строго от каталога первого позиционного пути, поэтому запуск подкаталога сьюта был невозможен: `test tests/app/00-smoke/` падал с «No URL provided and no webtest.config.mjs found» — хотя спека прямо обещает запуск подкаталога («Фильтр по пути с CLI»). Опаснее отказа по URL были два молчаливых следствия: при `--url=` прогон подкаталога терял `_hooks.mjs` и ехал по неподготовленному стенду без единого предупреждения, а `_allure/` не находился. Плюс `file:` в отчёте считался от переданного пути, из-за чего один и тот же тест получал разный ID в зависимости от способа запуска и рвал историю Allure/JUnit. Введён корень сьюта: подъём от каталога пути до первого `webtest.config.mjs` ИЛИ `_hooks.mjs` (конфиг необязателен — сьют только с хуками иначе снова терял бы подготовку), с ограничением подъёма каталогом `.git`/`.v8-project.json`, а при их отсутствии — cwd. Граница ничего не выбирает, только останавливает, так что ложная граница даёт «корень не найден», а не чужой корень. От найденного корня берутся все пять ролей: конфиг, хуки, каталог отчёта, пути в отчёте, `_allure/`. Попутно: пути из разных сьютов в одном прогоне теперь отвергаются (раньше молча выигрывал первый путь, и сьют B ехал по подготовке сьюта A); найденный корень печатается в шапке; отсутствие корня — предупреждение в stderr; диагностика говорит про корень сьюта, а не только про URL. Проверено: 12/12 офлайн-кейсов резолвера; полный регресс 29/29 до и после — `file`/`name`/`status` идентичны; `_suite-root/nested/` (сценарий, который падал) проходит с подхваченными конфигом и хуками; `_hang/` 6/6. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
9c7010a49e
commit
90d8263a05
@@ -0,0 +1,15 @@
|
||||
// Hooks for the suite-root fixture. They prepare nothing — the point is that they RUN at all
|
||||
// when the runner was pointed at `nested/`. Losing hooks is the silent half of the bug this
|
||||
// fixture guards: without them a run proceeds against an unprepared stand and still goes green.
|
||||
// `prepare` writes a marker file the caller can assert on.
|
||||
import { writeFileSync } from 'fs';
|
||||
import { resolve, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
export const MARKER = resolve(HERE, 'prepare-ran.txt');
|
||||
|
||||
export async function prepare({ log }) {
|
||||
writeFileSync(MARKER, new Date().toISOString() + '\n');
|
||||
log('suite-root fixture: prepare() ran — hooks were resolved from the suite root');
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
// 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, rmSync } from 'fs';
|
||||
import { resolve, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { tmpdir } from 'os';
|
||||
|
||||
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');
|
||||
rmSync(BASE, { recursive: true, force: true });
|
||||
|
||||
/** 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);
|
||||
}
|
||||
|
||||
rmSync(BASE, { recursive: true, force: true });
|
||||
console.log(failed ? `\n${failed} check(s) FAILED\n` : '\nall checks passed\n');
|
||||
process.exit(failed ? 1 : 0);
|
||||
@@ -0,0 +1,22 @@
|
||||
// The only test of the suite-root fixture. Kept as light as possible — it is not here to test
|
||||
// the application, it is here to be run as `test tests/web-test/_suite-root/nested/` and prove
|
||||
// that config (URL) and hooks were resolved one level up. Before v1.9 that invocation died with
|
||||
// "No URL provided and no webtest.config.mjs found".
|
||||
import { existsSync } from 'fs';
|
||||
import { resolve, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const SUITE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
|
||||
export const name = 'suite-root: конфиг и хуки подхвачены на уровень выше';
|
||||
export const tags = ['suite-root'];
|
||||
export const timeout = 60000;
|
||||
|
||||
export default async function({ getPageState, assert, log }) {
|
||||
const state = await getPageState();
|
||||
const names = (state.sections || []).map(s => s.name);
|
||||
log('sections: ' + names.join(', '));
|
||||
assert.ok(names.length >= 1, 'сеанс открыт → URL взят из конфига в корне сюиты');
|
||||
assert.ok(existsSync(resolve(SUITE_ROOT, 'prepare-ran.txt')),
|
||||
'prepare() отработал → _hooks.mjs взят из корня сюиты, а не потерян');
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// Config for the suite-root fixture only. Its whole job is to sit HERE, one level above
|
||||
// `nested/`, so that `run.mjs test tests/web-test/_suite-root/nested/` has to climb to find it.
|
||||
// The stand must already be published (this fixture exercises path resolution, not the stand).
|
||||
export default {
|
||||
contexts: {
|
||||
a: { url: 'http://localhost:9191/webtest-runner/ru_RU' },
|
||||
},
|
||||
defaultContext: 'a',
|
||||
timeout: 60000,
|
||||
};
|
||||
Reference in New Issue
Block a user