From 3889c7279f0bcb4191653c4b66be9882a5dd3702 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Wed, 15 Jul 2026 15:36:48 +0300 Subject: [PATCH] =?UTF-8?q?fix(web-test):=20=D1=81=D0=BA=D1=80=D0=B8=D0=BD?= =?UTF-8?q?=D1=88=D0=BE=D1=82=D1=8B=20=D0=BF=D0=B0=D0=B4=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B9=20=D0=B4=D0=BE=D0=B5=D0=B7=D0=B6=D0=B0=D1=8E=D1=82=20?= =?UTF-8?q?=D0=B4=D0=BE=20Allure-=D0=BE=D1=82=D1=87=D1=91=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Вложение «Screenshot on failure» весило 0 B у всех красных тестов — две независимых причины, маскирующие друг друга. 1. slugify сохранял кириллицу в именах артефактов. Allure CLI молча не находит вложение с не-ASCII именем: пишет "size": 0 без ссылки на файл (JAVA_OPTS с file.encoding/sun.jnu.encoding не помогает). Теперь slugify транслитерирует кириллицу и схлопывает остальное не-ASCII в дефис. Чинит и видео — оно использовало то же имя. 2. Скриншот 1С-ошибки писался в фиксированный <навык>/error-shot.png: вне reportDir (репортер аттачит по basename → мёртвая ссылка) и одним именем на весь прогон (каждый следующий тест перетирал предыдущий). exec-context получил setErrorShotDir + уникальные имена; раннер направляет их в reportDir. Дефолт для интерактивных exec/run не менялся. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../web-test/scripts/cli/commands/test.mjs | 25 +++++++++++++++--- .../web-test/scripts/cli/exec-context.mjs | 26 +++++++++++++++---- .claude/skills/web-test/scripts/cli/util.mjs | 25 +++++++++++++++--- 3 files changed, 64 insertions(+), 12 deletions(-) diff --git a/.claude/skills/web-test/scripts/cli/commands/test.mjs b/.claude/skills/web-test/scripts/cli/commands/test.mjs index e9214dd0..49a63f39 100644 --- a/.claude/skills/web-test/scripts/cli/commands/test.mjs +++ b/.claude/skills/web-test/scripts/cli/commands/test.mjs @@ -1,10 +1,10 @@ -// web-test cli/commands/test v1.4 — regression test runner +// web-test cli/commands/test v1.5 — regression test runner // Source: https://github.com/Nikolay-Shirokov/cc-1c-skills -import { existsSync, writeFileSync, mkdirSync } from 'fs'; +import { existsSync, writeFileSync, mkdirSync, renameSync, copyFileSync, unlinkSync } from 'fs'; import { resolve, dirname, basename, relative } from 'path'; import * as browser from '../../browser.mjs'; import { out, die, elapsed, slugify, formatDuration, interpolate, printSteps } from '../util.mjs'; -import { buildContext, buildScopedContext } from '../exec-context.mjs'; +import { buildContext, buildScopedContext, setErrorShotDir } from '../exec-context.mjs'; import { createAssertions } from '../test-runner/assertions.mjs'; import { buildSeverityIndex } from '../test-runner/severity.mjs'; import { writeAllure, buildJUnit, syncAllureExtras } from '../test-runner/reporters.mjs'; @@ -143,6 +143,10 @@ export async function cmdTest(rawArgs) { : (opts.report && !reportToStdout ? dirname(resolve(opts.report)) : testDir); if (opts.screenshot !== 'off') { try { mkdirSync(reportDir, { recursive: true }); } catch {} + // 1C-error screenshots (taken inside the action wrapper) default to a single + // fixed file at the skill root — outside reportDir and shared by every test. + // Point them at reportDir so each failure keeps its own attachable file. + setErrorShotDir(reportDir); } // Discover test files @@ -432,6 +436,21 @@ export async function cmdTest(rawArgs) { shotFile = resolve(reportDir, `error-${testIdx}-${slugify(t.file.replace(/\.test\.mjs$/, ''))}.png`); writeFileSync(shotFile, png); } catch {} + } else if (shotFile && dirname(resolve(shotFile)) !== reportDir) { + // Shot came from a context built before setErrorShotDir (e.g. a server + // session started earlier): reporters attach by basename, so anything + // outside reportDir is a dead link. Move it in under a unique name. + const dest = resolve(reportDir, `error-${testIdx}-${slugify(t.file.replace(/\.test\.mjs$/, ''))}.png`); + try { + renameSync(resolve(shotFile), dest); + shotFile = dest; + } catch { + try { + copyFileSync(resolve(shotFile), dest); + try { unlinkSync(resolve(shotFile)); } catch {} + shotFile = dest; + } catch {} + } } if (t.teardown) try { await t.teardown(ctx); } catch {} diff --git a/.claude/skills/web-test/scripts/cli/exec-context.mjs b/.claude/skills/web-test/scripts/cli/exec-context.mjs index a6c0964f..cd802565 100644 --- a/.claude/skills/web-test/scripts/cli/exec-context.mjs +++ b/.claude/skills/web-test/scripts/cli/exec-context.mjs @@ -1,13 +1,29 @@ -// web-test cli/exec-context v1.0 — buildContext + executeScript для run/exec/test +// web-test cli/exec-context v1.1 — buildContext + executeScript для run/exec/test // Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import { readFileSync, writeFileSync } from 'fs'; import { resolve, dirname } from 'path'; import { fileURLToPath } from 'url'; import * as browser from '../browser.mjs'; -import { elapsed } from './util.mjs'; +import { elapsed, slugify } from './util.mjs'; const __dirname = dirname(fileURLToPath(import.meta.url)); -const ERROR_SHOT_PATH = resolve(__dirname, '..', '..', 'error-shot.png'); +const DEFAULT_ERROR_SHOT_PATH = resolve(__dirname, '..', '..', 'error-shot.png'); + +// Where 1C-error screenshots go. The default is a single fixed file: fine for +// interactive `exec`/`run` (last error wins, easy to find), wrong for a test run +// where every test needs its own file inside reportDir. cmdTest overrides this. +let errorShotDir = null; +let errorShotSeq = 0; + +export function setErrorShotDir(dir) { + errorShotDir = dir; + errorShotSeq = 0; +} + +function nextErrorShotPath(label) { + if (!errorShotDir) return DEFAULT_ERROR_SHOT_PATH; + return resolve(errorShotDir, `onec-error-${++errorShotSeq}-${slugify(label || 'shot')}.png`); +} /** * Build a per-context wrapper: same shape as buildContext output, but every call @@ -69,7 +85,7 @@ export function buildContext({ noRecord = false } = {}) { let errorShot; try { const png = await ctx.screenshot(); - errorShot = ERROR_SHOT_PATH; + errorShot = nextErrorShotPath(name); writeFileSync(errorShot, png); } catch {} // Try to fetch call stack for modal errors before throwing @@ -127,7 +143,7 @@ export async function executeScript(code, { noRecord } = {}) { if (!shotFile) { try { const png = await browser.screenshot(); - shotFile = ERROR_SHOT_PATH; + shotFile = nextErrorShotPath('script'); writeFileSync(shotFile, png); } catch {} } diff --git a/.claude/skills/web-test/scripts/cli/util.mjs b/.claude/skills/web-test/scripts/cli/util.mjs index c497d4b9..36bfc8a8 100644 --- a/.claude/skills/web-test/scripts/cli/util.mjs +++ b/.claude/skills/web-test/scripts/cli/util.mjs @@ -1,4 +1,4 @@ -// web-test cli/util v1.2 — generic helpers for CLI commands +// web-test cli/util v1.3 — generic helpers for CLI commands // Source: https://github.com/Nikolay-Shirokov/cc-1c-skills export function out(obj) { @@ -35,12 +35,29 @@ export function elapsed2(start, stop) { return Math.round(((stop || Date.now()) - start) / 100) / 10; } +const TRANSLIT = { + а: 'a', б: 'b', в: 'v', г: 'g', д: 'd', е: 'e', ё: 'e', ж: 'zh', з: 'z', и: 'i', + й: 'y', к: 'k', л: 'l', м: 'm', н: 'n', о: 'o', п: 'p', р: 'r', с: 's', т: 't', + у: 'u', ф: 'f', х: 'h', ц: 'ts', ч: 'ch', ш: 'sh', щ: 'sch', ъ: '', ы: 'y', ь: '', + э: 'e', ю: 'yu', я: 'ya', +}; + +/** + * ASCII-only slug for artifact file names (screenshots, videos). + * Non-ASCII names are unusable as Allure attachments: the Allure CLI silently + * fails to resolve them and emits `"size": 0` with no link to the file + * (JAVA_OPTS encoding flags do not help). Cyrillic is transliterated so the + * name stays readable; anything else non-ASCII collapses to `-`. + */ export function slugify(s) { - return String(s).trim() - .replace(/[\s/\\:*?"<>|]+/g, '-') + const ascii = String(s).trim().toLowerCase() + .replace(/[а-яё]/g, ch => TRANSLIT[ch] ?? '-'); + return ascii + .replace(/[^a-z0-9._-]+/g, '-') .replace(/-+/g, '-') .replace(/^-|-$/g, '') - .slice(0, 60) || 'step'; + .slice(0, 60) + .replace(/^-|-$/g, '') || 'step'; } export function formatDuration(seconds) {