From 2a71e6c98069dc9b381d72adcadf738bc7c59a92 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Wed, 15 Jul 2026 21:14:13 +0300 Subject: [PATCH] =?UTF-8?q?feat(web-test):=20=D0=BB=D0=BE=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D0=B4=D0=B8=D0=B0=D0=BB=D0=BE=D0=B3=20=D0=B0?= =?UTF-8?q?=D0=B2=D1=82=D0=BE=D1=80=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D0=B8=20?= =?UTF-8?q?+=20=D1=87=D0=B8=D1=81=D1=82=D0=B0=D1=8F=20=D0=B4=D0=B8=D0=B0?= =?UTF-8?q?=D0=B3=D0=BD=D0=BE=D1=81=D1=82=D0=B8=D0=BA=D0=B0=20=D0=B2=20sta?= =?UTF-8?q?rt/run?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Второй вход в ту же ловушку: публикация без пользователя (нет Usr= в vrd). Клиент показывает диалог «Пользователь/Пароль/Войти», ввод учётных данных движок не поддерживает — но вместо ошибки он ждал 67.7с, а затем closeModals() жал Escape и диалог ИСЧЕЗАЛ. Оставалась пустая страница, объявленная здоровым стартом, и дальше та же ложь про «режим отображения панели». Разведка изменила постановку в двух местах: 1. Улику затирал сам движок. Замерено: одного Escape достаточно, чтобы форма логина пропала. Поэтому детект обязан отработать ДО closeModals. 2. Комментарий «страница логина легитимна» оказался фикцией: войти руками через start было невозможно и раньше — движок сам уничтожал форму. Значит падать тут ничего не ломает, и развилка «в раннере падать, в интерактиве нет» отпадает. Комментарий переписан, чтобы следующий читатель не поверил ему больше, чем коду. Хуже, чем с лицензией: на диалоге авторизации сеанс 1С УСПЕВАЕТ создаться и держит лицензию впустую. Поэтому connect() освобождает его (disconnect) перед тем, как ошибка уйдёт наверх: cmdStart зовёт connect без catch, run.mjs не оборачивает команду, а убийство процесса лицензию не освобождает. - session.mjs (v1.19→v1.20): +1 якорь #authWindow в тот же предикат, своё сообщение с рецептом (-UserName → Usr=/Pwd=), очистка в connect(). - start.mjs (v1.0→v1.1), run.mjs (v1.0→v1.1): стартовый блокер — это диагноз, а не крах. Три строки и код 1 вместо стека, который указывал внутрь session.mjs и читался бы как поломка движка (модель пошла бы чинить не то). Проверено вживую: - шаг 0 (до кода): #authWindow не существует на здоровых стартах — ни в клиенте, ни при загрузке; проверено и на bpdemo с автологином (19.5с, опрос 100мс); - позитивный контроль на bpdemo-auth (BP_DEMO без Usr=): бросок за 2.7с вместо 67.7с, слот убран из реестра, лицензия вернулась — 6/6; - то же через connect(): сообщение, браузер закрыт, сеанс освобождён; - негативный контроль: полный регресс 25/25, ноль ложных срабатываний; start на bpdemo (автологин) поднимается штатно; - попутно на испорченном прогоне (сам съел лицензию соседней сессией) детект отработал в раннере на НАСТОЯЩЕМ дефиците: два мультиконтекстных теста упали с внятной причиной, остальные 23 прошли — прогон поехал дальше. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../web-test/scripts/cli/commands/run.mjs | 11 +++- .../web-test/scripts/cli/commands/start.mjs | 12 +++- .../web-test/scripts/engine/core/session.mjs | 59 +++++++++++++++---- 3 files changed, 67 insertions(+), 15 deletions(-) diff --git a/.claude/skills/web-test/scripts/cli/commands/run.mjs b/.claude/skills/web-test/scripts/cli/commands/run.mjs index cb3631cd..062d117b 100644 --- a/.claude/skills/web-test/scripts/cli/commands/run.mjs +++ b/.claude/skills/web-test/scripts/cli/commands/run.mjs @@ -1,4 +1,4 @@ -// web-test cli/commands/run v1.0 — autonomous connect → exec → disconnect (no server) +// web-test cli/commands/run v1.1 — autonomous connect → exec → disconnect (no server) // Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import { readFileSync } from 'fs'; import { resolve } from 'path'; @@ -13,7 +13,14 @@ export async function cmdRun(url, fileOrDash) { ? await readStdin() : readFileSync(resolve(fileOrDash), 'utf-8'); - await browser.connect(url); + // Same as cmdStart: a startup blocker is a diagnosis, not a crash. connect() has already + // released the seance and closed the browser; a stack trace pointing into session.mjs would + // read as an engine bug and send the reader off to debug the wrong thing. + try { + await browser.connect(url); + } catch (e) { + die(e.message); + } const result = await executeScript(code); await browser.disconnect(); diff --git a/.claude/skills/web-test/scripts/cli/commands/start.mjs b/.claude/skills/web-test/scripts/cli/commands/start.mjs index b83d5ec2..da852c23 100644 --- a/.claude/skills/web-test/scripts/cli/commands/start.mjs +++ b/.claude/skills/web-test/scripts/cli/commands/start.mjs @@ -1,4 +1,4 @@ -// web-test cli/commands/start v1.0 +// web-test cli/commands/start v1.1 // Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import http from 'http'; import { writeFileSync } from 'fs'; @@ -10,7 +10,15 @@ import { handleRequest } from '../server.mjs'; export async function cmdStart(url) { if (!url) die('Usage: node src/run.mjs start '); - const state = await browser.connect(url); + // A startup blocker (no free 1C licence, publication asking for credentials) is a diagnosis, + // not a crash — connect() already released the seance and closed the browser, so print the + // message and leave instead of dumping a stack trace that reads like an engine failure. + let state; + try { + state = await browser.connect(url); + } catch (e) { + die(e.message); + } const httpServer = http.createServer(handleRequest); httpServer.listen(0, '127.0.0.1', () => { diff --git a/.claude/skills/web-test/scripts/engine/core/session.mjs b/.claude/skills/web-test/scripts/engine/core/session.mjs index 0ed1d793..001e5535 100644 --- a/.claude/skills/web-test/scripts/engine/core/session.mjs +++ b/.claude/skills/web-test/scripts/engine/core/session.mjs @@ -1,4 +1,4 @@ -// web-test core/session v1.19 — Browser session lifecycle: connect/disconnect/attach/detach, multi-context registry. +// web-test core/session v1.20 — Browser session lifecycle: connect/disconnect/attach/detach, multi-context registry. // Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import { chromium } from 'playwright'; @@ -61,15 +61,23 @@ function findExtension(overridePath) { * session-less slot behind; the first engine call then produced a plainly wrong diagnosis * ("Section panel is in icon-only mode…"). Measured: 66s wasted, then a lie. * - * Contract: throw ONLY on positive evidence of a startup dialog. A missing client marker is NOT - * evidence — a login page legitimately never shows it (that is what the fallback below is for). + * Two blockers are recognised, both by id: + * #messageBoxText — the startup message box (no free licence, and any other startup error) + * #authWindow — the login dialog: the publication wants credentials, which the engine + * cannot supply. Landing here used to be treated as legitimate ("login + * page"), but that was fiction: closeModals() presses Escape 5x right after + * this wait, which dismisses the dialog and leaves a blank page reported as + * a healthy start. Nobody could ever log in by hand either. + * + * Contract: throw ONLY on positive evidence — a visible dialog. A missing client marker is NOT + * evidence (an unknown or merely slow start must keep its old behaviour), hence the fallback. * * Anchors are ids, never text: the platform's wording is locale-dependent and only gets quoted - * into the message. Verified on this stand — on a healthy start #messageBoxText does not exist at - * all, neither in the loaded client nor at any point while the shell boots (polled every 100ms). - * The offsetWidth/text conjuncts guard the case where a future build pre-renders it hidden, the - * way the shell already pre-renders its login form. offsetWidth (not offsetParent — that is null - * for position:fixed, and #ps0win is an overlay) matches the convention in errors.mjs. + * into the message. Verified on this stand — on a healthy start neither #messageBoxText nor + * #authWindow exists at all, in the loaded client or at any point while the shell boots (polled + * every 100ms, including bpdemo's 19.5s auto-login boot). The offsetWidth/text conjuncts guard a + * future build that pre-renders them hidden. offsetWidth (not offsetParent — that is null for + * position:fixed, and #ps0win is an overlay) matches the convention in errors.mjs. */ async function waitForClientOrStartupBlock(pg, url, timeout = INIT_TIMEOUT) { let outcome; @@ -78,6 +86,8 @@ async function waitForClientOrStartupBlock(pg, url, timeout = INIT_TIMEOUT) { if (document.querySelector('#themesCell_theme_0')) return 'client'; const box = document.querySelector('#messageBoxText'); if (box && box.offsetWidth > 0 && box.textContent.trim()) return 'blocked'; + const auth = document.querySelector('#authWindow'); + if (auth && auth.offsetWidth > 0) return 'auth'; return false; }, null, { timeout }).then(h => h.jsonValue()); } catch { @@ -96,11 +106,28 @@ async function waitForClientOrStartupBlock(pg, url, timeout = INIT_TIMEOUT) { const visibleText = (el) => (el && el.offsetWidth > 0) ? (el.innerText || '').trim() : ''; if (document.querySelector('#themesCell_theme_0')) return null; // the client won after all const text = visibleText(document.querySelector('#messageBoxText')); - return text ? { text, seances: visibleText(document.querySelector('#seancesToFinish')) } : null; + if (text) return { kind: 'blocked', text, seances: visibleText(document.querySelector('#seancesToFinish')) }; + const auth = document.querySelector('#authWindow'); + if (auth && auth.offsetWidth > 0) return { kind: 'auth', text: visibleText(auth) }; + return null; }).catch(() => null); if (!evidence) return; // flicker — carry on exactly as before const oneLine = (s) => s.replace(/\s+/g, ' ').trim().slice(0, 300); + + if (evidence.kind === 'auth') { + // The publication asks a human for credentials. The engine cannot answer, and until now it + // did something worse than fail: it waited 66s and then closeModals()'s Escape dismissed the + // dialog, leaving a blank page reported as a healthy start. Note a seance IS already created + // here (unlike the licence case) and holds a licence — callers release it before rethrowing. + throw new Error( + `1C requires interactive login before the web client loads: "${oneLine(evidence.text)}"` + + '\n Движок ввод учётных данных не поддерживает — опубликуйте базу с пользователем' + + ' (web-publish -UserName … → Usr=/Pwd= в vrd) либо укажите его в строке соединения.' + + `\n URL: ${url}` + ); + } + throw new Error( `1C startup blocked before the web client loaded: "${oneLine(evidence.text)}"` + (evidence.seances ? `\n Сеансы, предложенные платформой к завершению: ${oneLine(evidence.seances)}` : '') + @@ -160,8 +187,18 @@ export async function connect(url, { extensionPath } = {}) { await page.goto(url, { waitUntil: 'domcontentloaded', timeout: LOAD_TIMEOUT }); } - // Wait for 1C to initialize — or fail fast if the startup shell blocks the client - await waitForClientOrStartupBlock(page, url); + // Wait for 1C to initialize — or fail fast if the startup shell blocks the client. + // MUST run before closeModals(): that presses Escape 5x, which dismisses the auth dialog and + // destroys the evidence (measured — one Escape blanks the page). + try { + await waitForClientOrStartupBlock(page, url); + } catch (e) { + // On the auth dialog a 1C seance already exists and holds a licence, and killing the process + // does NOT release it. cmdStart has no catch and run.mjs does not wrap the command, so the + // error escapes and the process dies — release the seance here, while we still can. + await softDeadline(disconnect(), 20000, 'disconnect(startup-block)'); + throw e; + } // Try to close startup modals (Путеводитель etc.) await closeModals();