mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-07-18 00:29:42 +03:00
fix(web-test): resetState замечает провал и не считает рабочий стол грязью
resetState молча сдавался: крутил 10 попыток closeForm, игнорировал их
результат и ничего не возвращал. Контекст с чужой открытой формой уходил в
пул как «чистый», следующий тест кликал в него (заявка пилота: Сц.3 роняет
форму → Сц.5 падает на чужой).
- resetState возвращает вердикт {clean, attempts, form, title, modal}. «Чисто»
определяется не как «форм нет» (form == null — верно только для пустого
стола), а как «закрывать нечего»: closeForm.nothingToClose. Замерено на
стенде пилота — рабочий стол там это form=5, formCount=3, openForms=[5,6,7],
и старое правило объявляло бы чистый контекст грязным ПОСЛЕ КАЖДОГО теста
(clean:false за 8.7с). Теперь clean:true за 0.9с, одна итерация вместо десяти.
- resetOrAbort читает вердикт: не clean → !-строка с именем оставшейся формы +
abortContext. Ровно логика, уже работавшая для пробоя дедлайна.
- Диагностика буферизуется и печатается ПОД строкой своего теста: cleanup идёт
до записи результата, поэтому раньше !-строки вставали над тестом и
приписывались предыдущему (на этом купилась и сама сессия).
- Ранний выход по closed:false НЕ вводим: A/B на живом наборе показал, что
грязная «Приходная накладная» отдаёт closed:false на первой попытке и
закрывается на следующей — выход прерывал бы контекст зря.
Проверено: контракт стабом 5/5; на стенде пилота ложное срабатывание снято
(8.8с→0.9с), стопка форм разбирается до стола; полный регресс 25/25, ноль
строк «not clean» (было две — «Тестовые ошибки»).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
// web-test cli/commands/test v1.7 — regression test runner
|
||||
// web-test cli/commands/test v1.8 — regression test runner
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
import { existsSync, writeFileSync, mkdirSync, renameSync, copyFileSync, unlinkSync } from 'fs';
|
||||
import { resolve, dirname, basename, relative } from 'path';
|
||||
@@ -245,6 +245,20 @@ export async function cmdTest(rawArgs) {
|
||||
const results = [];
|
||||
let passCount = 0, failCount = 0, skipCount = 0;
|
||||
|
||||
// Per-test diagnostics are BUFFERED and flushed right after that test's ✓/✗ line.
|
||||
// A test's cleanup runs before its result is printed, so writing straight to the stream put
|
||||
// `! …` lines ABOVE the test they belong to — i.e. visually under the PREVIOUS test's result.
|
||||
// Anyone reading the log (a model included) attributes them to the wrong test; that misreading
|
||||
// already cost this session a wrong conclusion. Outside a test (hooks, final teardown) there is
|
||||
// nothing to attach to, so lines go straight out.
|
||||
let diagSink = null;
|
||||
const emit = (line) => { if (diagSink) diagSink.push(line); else W.write(line); };
|
||||
const flushDiag = () => {
|
||||
if (!diagSink) return;
|
||||
for (const line of diagSink) W.write(line);
|
||||
diagSink = null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Bounded best-effort await: the replacement for `try { await x } catch {}`.
|
||||
* Same tolerance for failure, but a call that never settles can no longer stall the run,
|
||||
@@ -252,23 +266,37 @@ export async function cmdTest(rawArgs) {
|
||||
*/
|
||||
async function bounded(promise, ms, label) {
|
||||
const r = await softDeadline(promise, ms, label);
|
||||
if (!r.ok) W.write(` ! ${label}: ${r.timedOut ? `timed out after ${ms}ms` : r.err.message.split('\n')[0]}\n`);
|
||||
if (!r.ok) emit(` ! ${label}: ${r.timedOut ? `timed out after ${ms}ms` : r.err.message.split('\n')[0]}\n`);
|
||||
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.
|
||||
* Reset one context between tests — and only reuse it if the reset actually WORKED.
|
||||
*
|
||||
* Reusing a context whose UI was not cleaned leaks someone else's open form into the next test:
|
||||
* silent drift instead of a visible error, the worst possible outcome. Two ways to end up there,
|
||||
* and both must lead here:
|
||||
* - the reset breached its deadline (badly-sized budget, wedged page);
|
||||
* - the reset ran to completion but did not clean anything (a modal that refuses to close) —
|
||||
* this one used to pass as success, because `bounded` only reports timeouts and throws.
|
||||
* Either way: destroy the slot, ensureContext recreates a clean one. The cost is a 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`);
|
||||
if (r.ok && r.value?.clean) return true;
|
||||
|
||||
if (r.ok) {
|
||||
// Name what stayed open — otherwise the next investigation starts from archaeology.
|
||||
const v = r.value || {};
|
||||
const what = v.title ? `"${v.title}"` : `#${v.form}`;
|
||||
emit(` ! resetState(${cn}): not clean — form ${what}${v.modal ? ' (modal)' : ''} still open` +
|
||||
` after ${v.attempts} close attempt(s)` +
|
||||
`${v.lastError ? `, last error: ${v.lastError.message.split('\n')[0]}` : ''}\n`);
|
||||
}
|
||||
emit(` ! context "${cn}" left dirty — aborting it, the next test gets a fresh one\n`);
|
||||
await bounded(browser.abortContext(cn), D.closeContext, `abortContext(${cn})`);
|
||||
dropLru(lruOrder, cn);
|
||||
return false;
|
||||
@@ -402,7 +430,7 @@ export async function cmdTest(rawArgs) {
|
||||
try {
|
||||
await ensureContext(defaultContextName);
|
||||
} catch (e) {
|
||||
W.write(`\n!! не удалось открыть контекст "${defaultContextName}": ${e.message}\n\n`);
|
||||
W.write(`\n!! cannot open context "${defaultContextName}": ${e.message}\n\n`);
|
||||
try { writeFinalReport('aborted'); } catch {}
|
||||
// process.exit skips the `finally` below, and killing the process does NOT release a 1C
|
||||
// seance — so release what we hold explicitly before leaving.
|
||||
@@ -428,6 +456,8 @@ export async function cmdTest(rawArgs) {
|
||||
let testIdx = 0;
|
||||
for (const t of filtered) {
|
||||
testIdx++;
|
||||
// Buffer this test's diagnostics; they are flushed under its own result line below.
|
||||
diagSink = [];
|
||||
const declaredContexts = t.contexts && t.contexts.length
|
||||
? t.contexts
|
||||
: [t.context || defaultContextName];
|
||||
@@ -435,6 +465,7 @@ export async function cmdTest(rawArgs) {
|
||||
if (t.skip) {
|
||||
const reason = typeof t.skip === 'string' ? t.skip : '';
|
||||
W.write(` ○ ${t.name}${reason ? ` (skip: ${reason})` : ' (skip)'}\n`);
|
||||
flushDiag();
|
||||
recordResult({ name: t.name, file: t.file, tags: t.tags, contexts: declaredContexts, status: 'skipped', duration: 0, attempts: 0, steps: [], output: '', error: null, screenshot: null });
|
||||
skipCount++;
|
||||
continue;
|
||||
@@ -483,6 +514,7 @@ export async function cmdTest(rawArgs) {
|
||||
touchLru(lruOrder, testContextNames);
|
||||
} catch (e) {
|
||||
W.write(` ✗ ${t.name} (context setup failed: ${e.message})\n`);
|
||||
flushDiag();
|
||||
recordResult({ name: t.name, file: t.file, tags: t.tags, contexts: declaredContexts, status: 'failed', duration: 0, attempts: 0, steps: [], output: '', error: { message: e.message }, screenshot: null });
|
||||
failCount++;
|
||||
if (opts.bail) break;
|
||||
@@ -635,7 +667,7 @@ export async function cmdTest(rawArgs) {
|
||||
diagnosis = { verdict, probe, net: diag?.net };
|
||||
e.message = `${e.message} — ${lines[0]}`;
|
||||
output.push(...lines);
|
||||
W.write(lines.map(l => ` ${l}\n`).join(''));
|
||||
emit(lines.map(l => ` ${l}\n`).join(''));
|
||||
}
|
||||
|
||||
const dead = diagnosis && (diagnosis.verdict === 'hang' || diagnosis.verdict === 'browser-dead');
|
||||
@@ -728,9 +760,15 @@ export async function cmdTest(rawArgs) {
|
||||
if (lastError?.screenshot) W.write(` screenshot: ${lastError.screenshot}\n`);
|
||||
}
|
||||
|
||||
flushDiag();
|
||||
|
||||
if (opts.bail && testResult.status === 'failed') break;
|
||||
}
|
||||
|
||||
// Out of the per-test scope (also on `break`): afterAll and the final teardown have no test
|
||||
// to nest under, so their diagnostics go straight to the stream again.
|
||||
flushDiag();
|
||||
|
||||
if (hooks.afterAll) await bounded(hooks.afterAll(ctx), D.hooks, 'hooks.afterAll');
|
||||
|
||||
} finally {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// web-test cli/test-runner/discover v1.1 — test file discovery + state reset between tests
|
||||
// web-test cli/test-runner/discover v1.3 — test file discovery + state reset between tests
|
||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
import { existsSync, readdirSync } from 'fs';
|
||||
import { resolve } from 'path';
|
||||
@@ -29,15 +29,54 @@ export function discoverTests(testPaths) {
|
||||
return [...new Set(files)].sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the context to a clean desktop between tests — and REPORT whether that worked.
|
||||
*
|
||||
* The verdict is the point. closeForm does not throw when a form refuses to close: it returns
|
||||
* `{closed:false}`, and this loop used to drop that on the floor, so a context with someone else's
|
||||
* modal still open went back into the pool as "clean" and the next test clicked into it. Measured
|
||||
* on the pilot's stand: 10 idle iterations, `closed:false` every time, state unchanged — and the
|
||||
* runner called it a success.
|
||||
*
|
||||
* @returns {Promise<{clean: boolean, attempts: number, form?: any, title?: string, modal?: boolean, lastError?: Error}>}
|
||||
* `clean:false` also when the check itself failed — not being able to confirm is not being clean.
|
||||
*/
|
||||
export async function resetState(ctx) {
|
||||
try { if (typeof ctx.dismissPendingErrors === 'function') await ctx.dismissPendingErrors(); } catch {}
|
||||
let attempts = 0;
|
||||
let lastError = null;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
try {
|
||||
const state = await ctx.getFormState();
|
||||
// form === null means no form open (desktop). form === 0 is a real background form
|
||||
// 1C exposes in some states — must still close it to fully reset.
|
||||
if (state.form == null) break;
|
||||
await ctx.closeForm({ save: false });
|
||||
} catch { break; }
|
||||
if (state.form == null) return { clean: true, attempts };
|
||||
attempts++;
|
||||
const r = await ctx.closeForm({ save: false });
|
||||
// The platform found nothing closable → this is the desktop, however many forms sit on it.
|
||||
// Without this the check would be "form == null", which is only true for an EMPTY desktop:
|
||||
// on a real application the home page keeps its own forms (measured: form=5, formCount=3,
|
||||
// no cross), so the old rule declared a perfectly clean context dirty after every test.
|
||||
if (r?.nothingToClose) return { clean: true, attempts, desktop: true };
|
||||
// Deliberately NOT bailing out on `closed:false`: measured A/B on the live suite — a dirty
|
||||
// «Приходная накладная *» reports closed:false on the first round and closes on a later one,
|
||||
// so an early exit aborted a context that was about to be clean. `closed` compares form
|
||||
// numbers, so an intermediate step (a popup going away) reads as "nothing happened" even
|
||||
// though progress was made. The verdict below judges the END state, which is what matters.
|
||||
} catch (e) { lastError = e; break; }
|
||||
}
|
||||
|
||||
// Control check — the loop proves nothing on its own: it can also exit via `catch` above.
|
||||
try {
|
||||
const state = await ctx.getFormState();
|
||||
if (state.form == null) return { clean: true, attempts };
|
||||
return {
|
||||
clean: false, attempts, lastError,
|
||||
form: state.form,
|
||||
title: state.activeTab || null,
|
||||
modal: !!state.modal,
|
||||
};
|
||||
} catch (e) {
|
||||
return { clean: false, attempts, lastError: lastError || e };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user