@7n/test 0.16.0 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.16.1] - 2026-07-18
4
+
5
+ ### Fixed
6
+
7
+ - coverage: три реальні баги, знайдені живою перевіркою на справжньому Storybook 10 + Vue 3 проєкті (storybook init-скаффолд) — (1) --project=!storybook падав з 'No projects matched the filter' для проєктів без окремого JS unit-test vitest-проєкту (типовий default-скаффолд), тепер трактується як passWithNoTests; (2) .vue на Storybook-root виключено з JS-виміру Stryker-мутації — vitest-runner структурно не може прогнати dry-run, коли єдиний vitest-проєкт це browser-mode 'storybook', раніше це валило ВЕСЬ прогін; (3) Stryker CLI configFile — позиційний аргумент, не --configFile. Задокументовано нове невирішене обмеження full-режиму command runner на реальних проєктах (npm/docs/stryker-storybook-config.md) — --changed-executor лишається єдиним підтвердженим робочим шляхом
8
+
3
9
  ## [0.16.0] - 2026-07-18
4
10
 
5
11
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@7n/test",
3
- "version": "0.16.0",
3
+ "version": "0.16.1",
4
4
  "description": "CLI-утиліта @7n/test",
5
5
  "keywords": [
6
6
  "7n",
@@ -33,6 +33,7 @@ const JS_FILE = /\.(c|m)?[jt]sx?$|\.vue$/
33
33
  const TEST_FILE = /\.(test|spec)\.[^.]+$/
34
34
  /** `.vue`-компоненти + `*.stories.*` — сигнал для Storybook-змінного scope (line coverage). */
35
35
  const VUE_OR_STORIES_FILE = /\.vue$|\.stories\.[^.]+$/
36
+ const VUE_FILE_RE = /\.vue$/
36
37
 
37
38
  /**
38
39
  * Звужує список змінених файлів (relative до cwd) до тих, що лежать під `jsRoot`,
@@ -81,6 +82,8 @@ const VITEST_HINT =
81
82
  const STORYBOOK_STRYKER_CONFIG = 'stryker.storybook.config.mjs'
82
83
  /** Шлях mutation.json того ж прогону (окремий від `reports/stryker/` JS-виміру). */
83
84
  const STORYBOOK_STRYKER_REPORT = join('reports', 'stryker-storybook', 'mutation.json')
85
+ /** vitest hard-fail коли `--project=!storybook` не залишає жодного проєкту (не test-порожньо). */
86
+ const NO_PROJECTS_MATCHED_RE = /No projects matched the filter/
84
87
 
85
88
  /**
86
89
  * Чи у пакеті встановлено vitest (через dependencies або devDependencies).
@@ -311,20 +314,36 @@ const defaultRunner = {
311
314
  // зайве дублювання з collectStorybookForRoot і ризик втягнути Playwright-залежність
312
315
  // у звичайний coverage-прогін.
313
316
  const projectArgs = excludeStorybookProject ? ['--project=!storybook'] : []
314
- const r = spawnSync(
315
- 'bunx',
316
- [
317
- 'vitest',
318
- 'run',
319
- '--passWithNoTests',
320
- '--coverage',
321
- '--coverage.reporter=lcov',
322
- `--coverage.reportsDirectory=${lcovDir}`,
323
- ...projectArgs,
324
- ...changedArgs
325
- ],
326
- { cwd, stdio: 'inherit', env: process.env }
327
- )
317
+ const args = [
318
+ 'vitest',
319
+ 'run',
320
+ '--passWithNoTests',
321
+ '--coverage',
322
+ '--coverage.reporter=lcov',
323
+ `--coverage.reportsDirectory=${lcovDir}`,
324
+ ...projectArgs,
325
+ ...changedArgs
326
+ ]
327
+
328
+ if (!excludeStorybookProject) {
329
+ const r = spawnSync('bunx', args, { cwd, stdio: 'inherit', env: process.env })
330
+ return r.status ?? 1
331
+ }
332
+
333
+ // excludeStorybookProject-гілка захоплює stdout/stderr (не stdio:'inherit'), бо треба
334
+ // програмно розрізнити "--project=!storybook не залишив ЖОДНОГО проєкту" (типовий
335
+ // canonical @storybook/addon-vitest setup БЕЗ окремого unit-test проєкту — Storybook-root
336
+ // з єдиним named-проєктом "storybook") від реальної помилки. vitest у цьому кейсі кидає
337
+ // hard "No projects matched the filter" і завершується exit≠0 ДО будь-якого запуску тестів
338
+ // (`--passWithNoTests` цей кейс не покриває — той стосується тестів усередині вже
339
+ // знайдених проєктів, не відсутності самих проєктів). Підтверджено емпірично на
340
+ // реальному `storybook init` (Storybook 10 + Vue3, 2026-07): весь `test.projects` — лише
341
+ // "storybook", без окремого JS unit-test проєкту.
342
+ const r = spawnSync('bunx', args, { cwd, encoding: 'utf8', env: process.env })
343
+ const out = (r.stdout ?? '') + (r.stderr ?? '')
344
+ process.stdout.write(r.stdout ?? '')
345
+ process.stderr.write(r.stderr ?? '')
346
+ if (r.status !== 0 && NO_PROJECTS_MATCHED_RE.test(out)) return 0
328
347
  return r.status ?? 1
329
348
  },
330
349
  runBunCoverage({ cwd, lcovDir }) {
@@ -421,7 +440,10 @@ const defaultRunner = {
421
440
  // конфіга задокументовано в npm/docs/stryker-storybook-config.md. Той самий
422
441
  // резолвінг local-bin, що й runStryker (plugin-discovery відносно install-каталогу).
423
442
  const strykerBin = resolveLocalStrykerBin()
424
- const args = ['run', '--configFile', STORYBOOK_STRYKER_CONFIG]
443
+ // `configFile` позиційний аргумент Stryker CLI (`stryker run [options] [configFile]`),
444
+ // НЕ `--configFile` — емпірично підтверджено (реальний прогін, 2026-07): `--configFile`
445
+ // дає `error: unknown option '--configFile'`.
446
+ const args = ['run', STORYBOOK_STRYKER_CONFIG]
425
447
  const r = strykerBin
426
448
  ? spawnSync(strykerBin, args, { cwd, stdio: 'inherit', env: process.env })
427
449
  : spawnSync('npx', ['@stryker-mutator/core', ...args], { cwd, stdio: 'inherit', env: process.env })
@@ -463,18 +485,28 @@ const defaultRunner = {
463
485
  */
464
486
  async function collectOneRoot(jsRoot, cwd, runner, scope = null) {
465
487
  const wsRel = relative(cwd, jsRoot)
466
- // У changed-режимі production-файли для мутації = змінені JS/Vue цього root без
467
- // тест-файлів і без *.stories.* (сторі — не production-код, окремий Storybook-вимір).
468
- // `.vue` тут ЗАЛИШАЄТЬСЯ — Stryker core мутує <script>/<script setup> SFC (з v7+),
469
- // допоки покриття дають НЕ-browser-mode тести (@vue/test-utils тощо, не Storybook-сторі).
470
- const mutateSrc = scope ? scope.files.filter(f => !TEST_FILE.test(f) && !STORIES_FILE_RE.test(f)) : null
471
488
 
472
489
  // Bun-native workspace: coverage через `bun test`, mutation пропускається
473
490
  // (Stryker vitest-runner не виконає код з `import ... from 'bun'`).
474
491
  const bunNative = await isBunNativeRoot(jsRoot)
475
492
  // Storybook root: named vitest-проєкт "storybook" (browser mode) типово ділить один
476
493
  // vitest.config.mjs зі звичайним suite — виключаємо його з JS-прогону (див. runJsCoverage).
477
- const excludeStorybookProject = !bunNative && (await isStorybookRoot(jsRoot))
494
+ const isStorybook = !bunNative && (await isStorybookRoot(jsRoot))
495
+ const excludeStorybookProject = isStorybook
496
+
497
+ // У changed-режимі production-файли для мутації = змінені JS/Vue цього root без
498
+ // тест-файлів і без *.stories.* (сторі — не production-код, окремий Storybook-вимір).
499
+ // `.vue` на Storybook-root ТЕЖ виключено: якщо root — Storybook-only (типовий
500
+ // `storybook init`-скаффолд, ЄДИНИЙ vitest-проєкт — "storybook"), Stryker vitest-runner
501
+ // структурно не може прогнати dry-run (немає non-browser проєкту для виконання) —
502
+ // емпірично підтверджено (2026-07, реальний Storybook 10 + Vue3 скаффолд): dry-run
503
+ // валиться, `collectOneRoot` кидає, і це ламає ВЕСЬ прогін (обидва виміри), не лише
504
+ // JS-рядок. `.vue`-мутація на Storybook-root — виключно відповідальність
505
+ // `collectStorybookForRoot` (own executor / command-runner), яка вже покриває SFC
506
+ // повністю; дублювання через JS-вимір там і зайве, і структурно ламке.
507
+ const mutateSrc = scope
508
+ ? scope.files.filter(f => !TEST_FILE.test(f) && !STORIES_FILE_RE.test(f) && !(isStorybook && VUE_FILE_RE.test(f)))
509
+ : null
478
510
  if (bunNative && !(await hasRunnableTests(jsRoot))) {
479
511
  // `bun test` без тестів завершується помилкою — graceful skip як vitest --passWithNoTests.
480
512
  return scope
@@ -598,21 +630,22 @@ async function collectOneRoot(jsRoot, cwd, runner, scope = null) {
598
630
  * Meta ACH): детермінована генерація мутантів + реальний прогін як суддя — без LLM
599
631
  * у контурі виконання.
600
632
  *
601
- * **Валідована альтернатива (спайк 2026-07):** Stryker **command runner** (не
602
- * vitest-runner) + browser mode ПРАЦЮЄ але лише з обов'язковим
633
+ * **Full-режим Stryker command runner (`runStorybookStrykerFull`, опційний у
634
+ * runner), АЛЕ з відомим невирішеним обмеженням (перевіряй перед покладанням
635
+ * секція «Обмеження» в `npm/docs/stryker-storybook-config.md`):**
636
+ * Spike на синтетичному JS-репо (2026-07-17) підтвердив саму МЕХАНІКУ — command
637
+ * runner + browser mode ПРАЦЮЄ з обов'язковим
603
638
  * `define: { 'process.env.__STRYKER_ACTIVE_MUTANT__': JSON.stringify(process.env.__STRYKER_ACTIVE_MUTANT__ ?? '') }`
604
- * у vite-конфізі browser-проєкту (vitest browser mode ставить define як runtime-глобал
605
- * у Chromium саме той шлях, що читає інструментація Stryker). Без define тихий
606
- * провал: 0% killed, усі мутанти "виживають" (контрольний експеримент підтвердив).
607
- * ~1.2s/мутант, sandbox без проблем (symlinked node_modules). Не прийнято зараз:
608
- * потребує змін конфігів target-проєкту (define + stryker command-config) і має
609
- * silent-0% failure mode, якщо Storybook-ів config merge загубить define; власний
610
- * executor мутує файл на диску активація не залежить від env-пропагації взагалі.
611
- * Тому в **full-режимі** прийнято саме цей шлях (`runStorybookStrykerFull`, опційний
612
- * у runner-і) — там власний executor вибуває через вартість, а не через технічні
613
- * обмеження, тож ризик command-runner-а прийнятний. У **changed-режимі** лишається
614
- * власний executor: дешевший на кількох файлах, і активація не залежить від
615
- * env-пропагації взагалі.
639
+ * у vite-конфізі (без define тихий провал, 0% killed). Але живий прогін на
640
+ * РЕАЛЬНОМУ `storybook init`-скаффолді (2026-07-18) показав: dry-run падає з
641
+ * `Failed to fetch dynamically imported module` для `*.stories.js`, і ЖОДНА з
642
+ * перевірених гіпотез (symlink sandbox, `inPlace: true`, vue-docgen-plugin,
643
+ * concurrency) не root-cause. Той самий `vitest run --project=storybook`
644
+ * бездоганно працює через ЗВИЧАЙНИЙ `spawnSync` (наш `--changed`-executor)
645
+ * проблема специфічна саме до того, як Stryker spawn-ить/оточує процес.
646
+ * Висновок: full-режим лишається задокументованим напрямком, не підтвердженим
647
+ * робочим рішенням; `--changed`-executor (нижче) єдиний перевірений на
648
+ * реальному проєкті шлях отримати справжній mutation score для Storybook-коду.
616
649
  *
617
650
  * Changed-режим: запускається тільки якщо серед змінених файлів root-а є хоча б
618
651
  * один `.vue`/`*.stories.*` (`scope.files` — вже звужений через `scopeToStorybookRoot`