@7n/test 0.16.0 → 0.16.2

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,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.16.2] - 2026-07-18
4
+
5
+ ### Fixed
6
+
7
+ - coverage: точний root cause провалу full-режиму Storybook mutation testing — дві незалежні причини замість 'невідомо'. (1) Stryker-інструментація ламає vue-docgen-api парсер @storybook/vue3-vite (framework-вбудований, не залежить від addon-docs) — виправлено через docgen:false у .storybook/main.js#framework.options, root cause підтверджено відтворенням поза Stryker-процесом. (2) окремий, підтверджений upstream Vitest browser mode баг (vitest#9509) — детерміновано (3/3), не флейкі, прогрів кешу не допомагає, не виправна на нашому боці. Full-режим лишається задокументованим напрямком до фіксу у Vitest
8
+ - coverage: подальше розслідування upstream Vitest browser-mode бага (vitest#9509) у full-режимі Storybook mutation testing — 11/11 детермінованих відтворень (було 3/3), простежено точний механізм резолву setup-file.js (bare-специфікатор через package.json#exports, версієзалежний .../internal/setup-file.browser.4), випробувано й відкинуто optimizeDeps.include з буквальним dist-шляхом (дає окрему exports-помилку) і з коректними bare-специфікаторами (синтаксично вірно, але не усуває fetch-провал). Висновок незмінний — --changed-executor лишається єдиним підтвердженим робочим шляхом
9
+ - coverage: vitest#9509 закритий upstream, але його фікс (optimizeDeps.include з повним runtime-стеком) не переноситься на наш кейс — ізоляційний тест з mutate:[] (0 інструментованих файлів) дає ту саму fetch-помилку на setup-file.js, виключаючи вміст файлів; штучна затримка перед стартом vitest теж не допомогла, виключаючи startup-race. Провал специфічний до факту спавну як дочірнього процесу Stryker — ймовірно окремий, ще не задокументований баг зі схожим симптомом. Висновок незмінний: --changed-executor лишається єдиним підтвердженим шляхом
10
+ - Full-режим Storybook mutation testing (Stryker command runner) тепер працює на реальних проєктах: root cause "Failed to fetch dynamically imported module" — symlinked node_modules у Stryker sandbox-копії, обхід через inPlace: true в stryker.storybook.config.mjs
11
+
12
+ ## [0.16.1] - 2026-07-18
13
+
14
+ ### Fixed
15
+
16
+ - 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 лишається єдиним підтвердженим робочим шляхом
17
+
3
18
  ## [0.16.0] - 2026-07-18
4
19
 
5
20
  ### 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.2",
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,36 @@ 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) виявив ДВІ незалежні
641
+ * причини провалу dry-run (`Failed to fetch dynamically imported module`),
642
+ * обидві тепер виправлені конфігом:
643
+ * (1) Stryker-інструментація ламає `vue-docgen-api`-парсер `@storybook/vue3-vite`
644
+ * root cause підтверджено (інструментований вміст, витягнутий з sandbox і
645
+ * підставлений напряму, відтворює ту саму помилку БЕЗ участі Stryker) і
646
+ * ВИПРАВЛЕНО через `docgen: false` у `.storybook/main.js#framework.options`;
647
+ * (2) Stryker sandbox symlink-ає `node_modules` у тимчасову `.stryker-tmp/sandbox-*`
648
+ * теку (дефолт), а `@storybook/addon-vitest` резолвить абсолютний шлях свого
649
+ * `setup-file.js` через РЕАЛЬНИЙ (symlink-target) шлях, а не sandbox-корінь
650
+ * Vite dev server відмовляється віддавати файл поза власним root. Підтверджено
651
+ * прямим експериментом: `vitest run` (БЕЗ Stryker) із `node_modules`, вручну
652
+ * зробленим symlink-ом з іншої директорії, відтворює ту саму помилку 1:1;
653
+ * той самий `node_modules`, СКОПІЙОВАНИЙ (не symlink) — працює без помилок.
654
+ * `resolve.preserveSymlinks` (Vite) і `NODE_OPTIONS=--preserve-symlinks` (Node)
655
+ * НЕ допомагають (шлях резолвиться всередині коду самого addon-vitest, не
656
+ * через Vite-резолвер). ВИПРАВЛЕНО через `inPlace: true` у
657
+ * `stryker.storybook.config.mjs` — Stryker мутує файли без sandbox-копії,
658
+ * symlink просто не задіяний.
659
+ * Висновок: з обома фіксами (`docgen: false` + `inPlace: true`) full-режим
660
+ * дає справжній mutation score на реальному Storybook-проєкті;
661
+ * `--changed`-executor (нижче) лишається окремим шляхом для звичайного
662
+ * PR-прогону по змінених файлах.
616
663
  *
617
664
  * Changed-режим: запускається тільки якщо серед змінених файлів root-а є хоча б
618
665
  * один `.vue`/`*.stories.*` (`scope.files` — вже звужений через `scopeToStorybookRoot`
@@ -669,8 +716,9 @@ async function collectStorybookForRoot(jsRoot, cwd, runner, scope = null) {
669
716
  if (code !== 0 && !existsSync(reportPath)) {
670
717
  throw new Error(
671
718
  `Storybook Stryker (command runner) exit ${code} — перевір ${STORYBOOK_STRYKER_CONFIG} ` +
672
- '(testRunner: "command", commandRunner.command, jsonReporter.fileName) ' +
673
- 'і define __STRYKER_ACTIVE_MUTANT__ у vite-конфізі browser-проєкту'
719
+ '(testRunner: "command", commandRunner.command, jsonReporter.fileName, inPlace: true), ' +
720
+ 'define __STRYKER_ACTIVE_MUTANT__ у vite-конфізі browser-проєкту ' +
721
+ 'і docgen: false у .storybook/main.js#framework.options (див. npm/docs/stryker-storybook-config.md)'
674
722
  )
675
723
  }
676
724
  if (existsSync(reportPath)) {