fix(web-test): причина сбоя запуска Chromium в тексте ошибки, установка браузера в Setup (#113)

На Windows Chromium, который не может стартовать, сообщает только «spawn UNKNOWN».
После сбоя запуска движок теперь проверяет side-by-side манифест рядом с chrome.exe
и дописывает в первую строку ошибки установленный факт: манифеста нет (установка
неполная) или он нечитаем для текущего пользователя (ограничение песочницы).
Проверка только после сбоя: Windows кэширует контекст активации запускавшегося
chrome.exe, и нечитаемый манифест сам по себе не означает, что запуск упадёт.

Setup: `npm install` браузер не скачивает — добавлен `npx playwright install chromium`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W4Lg8jEvTAoYYEbgx2cc7i
This commit is contained in:
Nick Shirokov
2026-09-29 21:33:28 +03:00
co-authored by Claude Opus 5.5
parent 292b6c97de
commit 618874972b
4 changed files with 55 additions and 14 deletions
+2 -2
View File
@@ -35,10 +35,10 @@ SCRIPT
## Setup (first time)
```bash
cd "${CLAUDE_SKILL_DIR}/scripts" && npm install
cd "${CLAUDE_SKILL_DIR}/scripts" && npm install && npx playwright install chromium
```
Requires Node.js 18+. `npm install` downloads Playwright and Chromium.
Requires Node.js 18+. `npm install` fetches Playwright; `npx playwright install chromium` downloads the browser.
## URL resolution
@@ -1,12 +1,12 @@
// web-test core/session v1.21 — Browser session lifecycle: connect/disconnect/attach/detach, multi-context registry.
// web-test core/session v1.22 — Browser session lifecycle: connect/disconnect/attach/detach, multi-context registry.
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import { chromium } from 'playwright';
import { softDeadline } from './deadline.mjs';
import { statSync, mkdirSync, readdirSync } from 'fs';
import { join as pathJoin } from 'path';
import { statSync, mkdirSync, readdirSync, openSync, closeSync } from 'fs';
import { join as pathJoin, dirname } from 'path';
import { removePathSync } from './fsutil.mjs';
import { tmpdir } from 'os';
import { tmpdir, userInfo } from 'os';
import {
browser, page, sessionPrefix, seanceId, recorder, highlightMode,
contexts, activeContextName, activeMode, persistentUserDataDir,
@@ -52,6 +52,47 @@ function findExtension(overridePath) {
return null;
}
/**
* On Windows a Chromium that cannot start reports only "spawn UNKNOWN". The usual cause is the
* side-by-side manifest next to chrome.exe (<version>.manifest): missing from the install, or not
* readable by the current user (agent sandboxes). Append what we can establish to the first line
* of the error — some callers print only that line. Silent when nothing is established.
* Only after a failure: Windows caches the activation context of a chrome.exe that already ran,
* so an unreadable manifest does not by itself mean a launch will fail.
*/
function explainLaunchFailure(e) {
try {
if (process.platform !== 'win32' || !/spawn UNKNOWN/.test(e?.message || '')) return e;
const dir = dirname(chromium.executablePath());
const manifests = readdirSync(dir).filter(f => f.toLowerCase().endsWith('.manifest'));
let hint = null;
if (manifests.length === 0) {
hint = `Chromium side-by-side manifest (<version>.manifest) is missing next to chrome.exe in ${dir} — ` +
'the browser install is incomplete. Reinstall: npx playwright install --force chromium';
} else {
for (const m of manifests) {
try { closeSync(openSync(pathJoin(dir, m), 'r')); }
catch (err) {
if (err.code !== 'EPERM' && err.code !== 'EACCES') continue;
hint = `${pathJoin(dir, m)} exists but is not readable by user ${userInfo().username} (${err.code}) — ` +
'likely a sandbox/permissions restriction, not a broken install. Check the launch outside the sandbox';
break;
}
}
}
if (hint) {
const nl = e.message.indexOf('\n');
e.message = nl < 0 ? `${e.message} — ${hint}` : `${e.message.slice(0, nl)} — ${hint}${e.message.slice(nl)}`;
}
} catch {}
return e;
}
async function launchWithDiagnosis(fn) {
try { return await fn(); }
catch (e) { throw explainLaunchFailure(e); }
}
/* isConnected moved to core/state.mjs */
/**
@@ -151,7 +192,7 @@ export async function connect(url, { extensionPath } = {}) {
// Launch with 1C browser extension via persistent context
setPersistentUserDataDir(pathJoin(tmpdir(), 'pw-1c-ext-' + Date.now()));
mkdirSync(persistentUserDataDir, { recursive: true });
const context = await chromium.launchPersistentContext(persistentUserDataDir, {
const context = await launchWithDiagnosis(() => chromium.launchPersistentContext(persistentUserDataDir, {
headless: false,
args: [
'--start-maximized',
@@ -160,12 +201,12 @@ export async function connect(url, { extensionPath } = {}) {
],
viewport: null,
permissions: ['clipboard-read', 'clipboard-write'],
});
}));
setBrowser(context); // persistent context IS the browser
setPage(context.pages()[0] || await context.newPage());
} else {
// Fallback: launch without extension
setBrowser(await chromium.launch({ headless: false, args: ['--start-maximized'] }));
setBrowser(await launchWithDiagnosis(() => chromium.launch({ headless: false, args: ['--start-maximized'] })));
const context = await browser.newContext({
viewport: null,
permissions: ['clipboard-read', 'clipboard-write'],
@@ -429,15 +470,15 @@ export async function createContext(name, url, { extensionPath, isolation = 'tab
// Persistent context: extension loads reliably, one window with tabs per context
setPersistentUserDataDir(pathJoin(tmpdir(), 'pw-1c-test-' + Date.now()));
mkdirSync(persistentUserDataDir, { recursive: true });
setBrowser(await chromium.launchPersistentContext(persistentUserDataDir, {
setBrowser(await launchWithDiagnosis(() => chromium.launchPersistentContext(persistentUserDataDir, {
headless: false,
args: launchArgs,
viewport: null,
permissions: ['clipboard-read', 'clipboard-write'],
}));
})));
} else {
// Window mode: separate BrowserContext per slot, full cookie isolation
setBrowser(await chromium.launch({ headless: false, args: launchArgs }));
setBrowser(await launchWithDiagnosis(() => chromium.launch({ headless: false, args: launchArgs })));
}
setActiveMode(isolation);
}
+1 -1
View File
@@ -12,7 +12,7 @@
- База опубликована через Apache (`/web-publish`)
- Node.js 18+ установлен
- Зависимости установлены: `cd .claude/skills/web-test/scripts && npm install`
- Зависимости и браузер установлены: `cd .claude/skills/web-test/scripts && npm install && npx playwright install chromium`
## Рабочий цикл
+1 -1
View File
@@ -11,7 +11,7 @@
## Предусловия
- База опубликована через Apache (`/web-publish`).
- Установлен Node.js 18+, зависимости подняты: `cd .claude/skills/web-test/scripts && npm install`.
- Установлен Node.js 18+, зависимости и браузер подняты: `cd .claude/skills/web-test/scripts && npm install && npx playwright install chromium`.
- ffmpeg — нужен только если хотите видеозапись прогона как доказательство падения. Без него падения фиксируются скриншотами. Установка описана в [web-test-recording-guide.md](web-test-recording-guide.md).
## Как это устроено