mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-07-18 08:39:40 +03:00
feat(web-test): бюджеты дедлайнов в конфиг + пробой сброса прерывает контекст
Числа были зашиты в код и подобраны на лёгком синтетическом стенде. На тяжёлом
прикладном решении тот же resetState честно идёт дольше — и упирался бы в чужой
дефолт без возможности его поднять.
- `deadlines: {...}` в webtest.config.mjs переопределяет любой бюджет поштучно.
Неизвестный ключ или неположительное значение — ошибка до старта прогона:
опечатка в имени означала бы, что переопределение молча не действует.
- Пробой resetState теперь ПРЕРЫВАЕТ контекст, а не пишет строку и едет дальше.
После неудавшегося сброса состояние UI неизвестно, и переиспользование слота
утекало бы грязным состоянием в следующий тест — худший исход плохо подобранного
бюджета: тихий дрейф вместо видимой ошибки. Теперь слишком тесный бюджет стоит
перезапуска контекста, но никогда — неверного результата теста.
Проверено: опечатка ключа → внятная ошибка с перечнем допустимых; deadlines
{resetState:1} → пробой виден строкой, контекст прерван, следующий тест зелёный.
check.mjs 6/6.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -23,6 +23,9 @@ export async function cmdTest(rawArgs) {
|
||||
// and can hang forever against a wedged renderer (page.evaluate has no timeout of its own),
|
||||
// so none of them may be awaited bare. A breach is always logged — silent swallowing is what
|
||||
// turned the original incident into a 29-minute mystery.
|
||||
//
|
||||
// These defaults are sized for a light stand. A heavy application legitimately needs more —
|
||||
// override per key via `deadlines: {...}` in webtest.config.mjs rather than editing this.
|
||||
const D = {
|
||||
screenshot: 10000,
|
||||
teardown: 15000,
|
||||
@@ -139,6 +142,17 @@ export async function cmdTest(rawArgs) {
|
||||
opts.timeout = ownArgs.some(a => a.startsWith('--timeout=')) ? opts.timeout : (config.timeout || opts.timeout);
|
||||
opts.retry = ownArgs.some(a => a.startsWith('--retry=')) ? opts.retry : (config.retries || opts.retry);
|
||||
opts.globalTimeout = ownArgs.some(a => a.startsWith('--global-timeout=')) ? opts.globalTimeout : (config.globalTimeout || opts.globalTimeout);
|
||||
|
||||
// Per-key deadline overrides. Defaults suit a light stand; a heavy application may honestly
|
||||
// need longer (a big form's resetState, a slow close). Unknown keys are a typo, not a wish —
|
||||
// fail fast rather than silently ignoring an override the author believed was in effect.
|
||||
if (config.deadlines) {
|
||||
for (const [k, v] of Object.entries(config.deadlines)) {
|
||||
if (!(k in D)) die(`Invalid deadlines.${k} in config (expected one of: ${Object.keys(D).join(', ')})`);
|
||||
if (typeof v !== 'number' || !(v > 0)) die(`Invalid deadlines.${k}=${v} (expected a positive number of ms)`);
|
||||
D[k] = v;
|
||||
}
|
||||
}
|
||||
if (config.preserveClipboard === false && !ownArgs.includes('--no-preserve-clipboard')) {
|
||||
browser.setPreserveClipboard(false);
|
||||
}
|
||||
@@ -242,6 +256,24 @@ export async function cmdTest(rawArgs) {
|
||||
return r;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset one context between tests. If the reset breaches its deadline, the slot's UI state is
|
||||
* unknown — reusing it would leak dirty state into the next test, which is the WORST outcome
|
||||
* of a badly-sized budget: silent drift instead of a visible error. So destroy it; the next
|
||||
* test recreates a clean one via ensureContext. A deadline that is merely too tight then costs
|
||||
* a context relaunch, never a wrong test result.
|
||||
*/
|
||||
async function resetOrAbort(cn, ctx) {
|
||||
const sw = await bounded(browser.setActiveContext(cn), D.setActive, `setActiveContext(${cn})`);
|
||||
if (!sw.ok) return false;
|
||||
const r = await bounded(resetState(ctx), D.resetState, `resetState(${cn})`);
|
||||
if (r.ok) return true;
|
||||
W.write(` ! context "${cn}" осталось несброшенным — прерываю его, следующий тест получит чистый\n`);
|
||||
await bounded(browser.abortContext(cn), D.closeContext, `abortContext(${cn})`);
|
||||
dropLru(lruOrder, cn);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Bumped for every attempt. A timed-out test's body keeps running — a promise cannot be
|
||||
// cancelled, so when its pending call finally rejects, its own `finally` would go on to
|
||||
// drive the UI of whichever test is running by then. Silent cross-test corruption.
|
||||
@@ -540,9 +572,7 @@ export async function cmdTest(rawArgs) {
|
||||
if (hooks.afterEach) await bounded(hooks.afterEach(ctx), D.afterEach, 'hooks.afterEach');
|
||||
for (const cn of testContextNames) {
|
||||
if (!browser.hasContext(cn)) continue;
|
||||
const sw = await bounded(browser.setActiveContext(cn), D.setActive, `setActiveContext(${cn})`);
|
||||
if (!sw.ok) break;
|
||||
await bounded(resetState(ctx), D.resetState, `resetState(${cn})`);
|
||||
await resetOrAbort(cn, ctx);
|
||||
}
|
||||
for (const k of scopedKeys) delete ctx[k];
|
||||
|
||||
@@ -633,9 +663,7 @@ export async function cmdTest(rawArgs) {
|
||||
if (!dead) {
|
||||
for (const cn of testContextNames) {
|
||||
if (!browser.hasContext(cn)) continue;
|
||||
const sw = await bounded(browser.setActiveContext(cn), D.setActive, `setActiveContext(${cn})`);
|
||||
if (!sw.ok) break;
|
||||
await bounded(resetState(ctx), D.resetState, `resetState(${cn})`);
|
||||
await resetOrAbort(cn, ctx);
|
||||
}
|
||||
}
|
||||
for (const k of scopedKeys) delete ctx[k];
|
||||
|
||||
@@ -473,6 +473,13 @@ export default {
|
||||
screenshot: 'on-failure', // 'every-step' | 'off'
|
||||
record: false,
|
||||
|
||||
// Бюджеты служебных операций (мс). Дефолты рассчитаны на лёгкий стенд; тяжёлому
|
||||
// прикладному решению честно нужно больше — переопределяются поштучно.
|
||||
deadlines: {
|
||||
resetState: 60000, // встроенный сброс на тяжёлых формах
|
||||
closeContext: 30000,
|
||||
},
|
||||
|
||||
// Дефолтный тег-фильтр. Применяется только если CLI не передал --tags.
|
||||
// Удобно для сценариев «прогон по умолчанию = smoke», при этом --tags=full
|
||||
// (или --tags=) с CLI прозрачно перекрывает.
|
||||
@@ -1031,6 +1038,33 @@ Timeout (30000ms) — verdict: hang (renderer unresponsive, browser alive)
|
||||
- Весь путь очистки (скриншот, `teardown`, `afterEach`, сброс, запись/остановка видео, закрытие контекстов,
|
||||
`disconnect`) ограничен дедлайнами. Пробой дедлайна печатается строкой `! <операция>: timed out after Nms`
|
||||
и прогон едет дальше — молча зависнуть путь очистки больше не может.
|
||||
|
||||
#### Бюджеты служебных операций (`deadlines`)
|
||||
|
||||
Дефолты рассчитаны на лёгкий стенд и переопределяются поштучно ключом `deadlines` в `webtest.config.mjs`
|
||||
(см. §7). Неизвестный ключ или неположительное значение — ошибка до старта прогона (опечатка в имени ключа
|
||||
означала бы, что переопределение молча не действует).
|
||||
|
||||
| Ключ | Default | Что ограничивает |
|
||||
|------|---------|------------------|
|
||||
| `screenshot` | 10000 | Скриншот при падении |
|
||||
| `teardown` / `afterEach` | 15000 | `teardown` теста / хук `afterEach` |
|
||||
| `setActive` | 5000 | Переключение активного контекста |
|
||||
| `resetState` | 20000 | Встроенный сброс состояния (до 10 итераций `getFormState` + `closeForm`) |
|
||||
| `startRecording` / `stopRecording` | 15000 / 40000 | Видео (у ffmpeg внутри свои 30 с) |
|
||||
| `closeContext` | 20000 | Закрытие/прерывание контекста |
|
||||
| `disconnect` | 30000 | Финальное закрытие браузера |
|
||||
| `hooks` | 120000 | `afterAll` / `cleanup` |
|
||||
| `abortAll` | 30000 | Весь блок прерывания одного зависшего теста |
|
||||
| `probe` | 2000 | Пробники рендерера/браузера при таймауте |
|
||||
|
||||
`prepare` и `beforeAll` **не ограничены сознательно**: `prepare` пересобирает стенд, и его честная
|
||||
длительность зависит от размера прикладного решения; его падение обязано оставаться фатальным.
|
||||
|
||||
**Пробой `resetState` прерывает контекст** (а не просто пишет строку): состояние UI после неудавшегося
|
||||
сброса неизвестно, и переиспользование слота утекло бы грязным состоянием в следующий тест — худший исход
|
||||
для плохо подобранного бюджета. Вместо этого слот уничтожается, следующий тест получает чистый через
|
||||
`ensureContext`. Слишком тесный бюджет стоит перезапуска контекста, но никогда — неверного результата теста.
|
||||
- Тело прерванного теста продолжает жить (промис не отменяем). Его поздние вызовы к движку блокируются
|
||||
с ошибкой `test abandoned (timeout)` — иначе `finally` упавшего теста кликал бы в окне следующего.
|
||||
|
||||
|
||||
@@ -86,6 +86,10 @@ node .claude/skills/web-test/scripts/run.mjs test tests/web-test/ -- --rebuild-e
|
||||
- **`contexts.a` / `contexts.b` / `contexts.c`** — независимые 1C-сеансы (разные cookies) на той же URL. `a`,`b` — два «пользователя» мультиконтекст-тестов; `c` задействован только `14-multi-context-routing` и служит топливом для проверки вытеснения пула.
|
||||
- **`defaultContext: 'a'`** — большинство тестов работают в одном контексте.
|
||||
- **`isolation: 'tab'`** — вкладки в одном окне (default). Альтернатива `'window'` — отдельный BrowserContext (полная изоляция cookies).
|
||||
- **`deadlines: {...}`** — бюджеты служебных операций (сброс состояния, закрытие контекста, скриншот…).
|
||||
Дефолты рассчитаны на лёгкий стенд; здесь не переопределяются. Если в выводе появились строки
|
||||
`! resetState(a): timed out after Nms` — стенд стал тяжелее бюджета, поднимай ключ, а не терпи:
|
||||
пробой сброса прерывает контекст (следующий тест получит чистый ценой перезапуска).
|
||||
- **`maxContexts: 2` / `contextPolicy: 'reuse'` / `pinnedContexts: []`** — дай-фуд управления пулом лицензий. Лимит 2 одновременных сеанса: на границе `14 (c) → 15 (a,b)` раннер автоматически вытесняет LRU-контекст `c` (проверяется первым шагом `15-multi-context-handover`). Благодаря лимиту 3 контекста никогда не живут одновременно.
|
||||
|
||||
## Env переменные
|
||||
|
||||
Reference in New Issue
Block a user