@7n/test 0.15.0 → 0.16.0

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.0] - 2026-07-18
4
+
5
+ ### Added
6
+
7
+ - coverage: full-режим mutation testing для Storybook через Stryker command runner — canonical stryker.storybook.config.mjs (testRunner:'command', jsonReporter → reports/stryker-storybook/mutation.json), спайк-валідований define-трюк для __STRYKER_ACTIVE_MUTANT__ у browser-mode vite-конфізі; без canonical конфіга — чесний skip, як і раніше. Задокументовано контракт у npm/docs/stryker-storybook-config.md
8
+
3
9
  ## [0.15.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.15.0",
3
+ "version": "0.16.0",
4
4
  "description": "CLI-утиліта @7n/test",
5
5
  "keywords": [
6
6
  "7n",
@@ -77,6 +77,11 @@ export function scopeToStorybookRoot(changedFiles, cwd, jsRoot) {
77
77
  const VITEST_HINT =
78
78
  'js coverage: vitest відсутній у package.json — додай `vitest`, `@vitest/coverage-v8` та `@stryker-mutator/vitest-runner` у devDependencies (див. test.mdc)'
79
79
 
80
+ /** Canonical config-файл для full-режиму Storybook-мутації через Stryker command runner. */
81
+ const STORYBOOK_STRYKER_CONFIG = 'stryker.storybook.config.mjs'
82
+ /** Шлях mutation.json того ж прогону (окремий від `reports/stryker/` JS-виміру). */
83
+ const STORYBOOK_STRYKER_REPORT = join('reports', 'stryker-storybook', 'mutation.json')
84
+
80
85
  /**
81
86
  * Чи у пакеті встановлено vitest (через dependencies або devDependencies).
82
87
  * @param {{dependencies?: Record<string,string>, devDependencies?: Record<string,string>}} pkg package.json
@@ -406,6 +411,21 @@ const defaultRunner = {
406
411
  ? spawnSync(strykerBin, ['run', ...mutateArgs], { cwd, stdio: 'inherit', env: process.env })
407
412
  : spawnSync('npx', ['@stryker-mutator/core', 'run', ...mutateArgs], { cwd, stdio: 'inherit', env: process.env })
408
413
  return r.status ?? 1
414
+ },
415
+ runStorybookStrykerFull({ cwd }) {
416
+ // Full-режим Storybook-мутація через canonical `stryker.storybook.config.mjs`
417
+ // (окремий Stryker-прогін, testRunner:'command' — vitest-runner browser mode не
418
+ // підтримує, див. коментар над collectStorybookForRoot). Спайк 2026-07 підтвердив:
419
+ // command runner + vitest browser mode ПРАЦЮЄ з `define` для
420
+ // __STRYKER_ACTIVE_MUTANT__ у vite-конфізі browser-проєкту — канонічний контракт
421
+ // конфіга задокументовано в npm/docs/stryker-storybook-config.md. Той самий
422
+ // резолвінг local-bin, що й runStryker (plugin-discovery відносно install-каталогу).
423
+ const strykerBin = resolveLocalStrykerBin()
424
+ const args = ['run', '--configFile', STORYBOOK_STRYKER_CONFIG]
425
+ const r = strykerBin
426
+ ? spawnSync(strykerBin, args, { cwd, stdio: 'inherit', env: process.env })
427
+ : spawnSync('npx', ['@stryker-mutator/core', ...args], { cwd, stdio: 'inherit', env: process.env })
428
+ return r.status ?? 1
409
429
  }
410
430
  }
411
431
 
@@ -558,8 +578,10 @@ async function collectOneRoot(jsRoot, cwd, runner, scope = null) {
558
578
  * Mutation testing: у **changed-режимі** виконується власним mutate→run→restore
559
579
  * executor-ом (`storybook-mutation.mjs`) — детерміновані AST-мутанти по змінених
560
580
  * production-файлах, вбиває/милує реальний browser-mode прогін. У **full-режимі**
561
- * пропускається з попередженням (повний suite × кожен мутант × Chromium — надто
562
- * дорого; той самий принцип чесного skip, що й bun-native).
581
+ * власний executor надто дорогий (усі production-файли, не лише змінені) тут шлях
582
+ * лише через canonical Stryker command-runner (`STORYBOOK_STRYKER_CONFIG`, див. нижче
583
+ * і `npm/docs/stryker-storybook-config.md`), якщо target-проєкт його налаштував;
584
+ * інакше чесний skip з попередженням (той самий принцип, що й bun-native).
563
585
  *
564
586
  * **Чому не Stryker (перевіряй перед покладанням на це в майбутньому — площина
565
587
  * активно змінюється з обох боків):** issue stryker-js#4557 ("[vitest] support browser
@@ -586,14 +608,18 @@ async function collectOneRoot(jsRoot, cwd, runner, scope = null) {
586
608
  * потребує змін конфігів target-проєкту (define + stryker command-config) і має
587
609
  * silent-0% failure mode, якщо Storybook-ів config merge загубить define; власний
588
610
  * executor мутує файл на диску — активація не залежить від env-пропагації взагалі.
589
- * Кандидат на майбутнє для full-режиму (richer operator set + incremental mode).
611
+ * Тому в **full-режимі** прийнято саме цей шлях (`runStorybookStrykerFull`, опційний
612
+ * у runner-і) — там власний executor вибуває через вартість, а не через технічні
613
+ * обмеження, тож ризик command-runner-а прийнятний. У **changed-режимі** лишається
614
+ * власний executor: дешевший на кількох файлах, і активація не залежить від
615
+ * env-пропагації взагалі.
590
616
  *
591
617
  * Changed-режим: запускається тільки якщо серед змінених файлів root-а є хоча б
592
618
  * один `.vue`/`*.stories.*` (`scope.files` — вже звужений через `scopeToStorybookRoot`
593
619
  * на боці виклику); інакше `null` (root пропускається повністю для цього виміру).
594
620
  * @param {string} jsRoot абсолютний шлях workspace-кореня
595
621
  * @param {string} cwd корінь проєкту (для рібейзингу `survived[].file`)
596
- * @param {{runStorybookCoverage:Function, runStorybookMutantTest?:Function, proposeStorybookLlmMutants?:Function}} runner spawn-ін'єкція
622
+ * @param {{runStorybookCoverage:Function, runStorybookMutantTest?:Function, proposeStorybookLlmMutants?:Function, runStorybookStrykerFull?:Function}} runner spawn-ін'єкція
597
623
  * @param {{files:string[], base:string|null}|null} [scope] changed-scope (null = full-режим)
598
624
  * @returns {Promise<{coverage:object, mutation:{caught:number,total:number}, survived:Array<object>} | null>} результат або null коли root не Storybook/без сторі/без relevant-змін
599
625
  */
@@ -630,12 +656,51 @@ async function collectStorybookForRoot(jsRoot, cwd, runner, scope = null) {
630
656
  await rm(lcovDir, { recursive: true, force: true })
631
657
  }
632
658
 
633
- // Full-режим: mutation чесно пропускається (див. JSDoc). Guard на runStorybookMutantTest
634
- // тримає сумісність із інжектованими runner-ами без mutation-підтримки.
635
- if (!scope || typeof runner.runStorybookMutantTest !== 'function') {
659
+ // Full-режим: власний executor надто дорогий (повний suite × кожен мутант ×
660
+ // Chromium на ВСІХ production-файлах, не лише змінених) — тут шлях лише через
661
+ // canonical Stryker command-runner (STORYBOOK_STRYKER_CONFIG), якщо target-проєкт
662
+ // його налаштував (див. npm/docs/stryker-storybook-config.md). Без canonical
663
+ // конфіга або без runStorybookStrykerFull у runner-і — чесний skip, як і раніше.
664
+ if (!scope) {
665
+ const hasCanonicalConfig = existsSync(join(jsRoot, STORYBOOK_STRYKER_CONFIG))
666
+ if (hasCanonicalConfig && typeof runner.runStorybookStrykerFull === 'function') {
667
+ const code = await runner.runStorybookStrykerFull({ cwd: jsRoot })
668
+ const reportPath = join(jsRoot, STORYBOOK_STRYKER_REPORT)
669
+ if (code !== 0 && !existsSync(reportPath)) {
670
+ throw new Error(
671
+ `Storybook Stryker (command runner) exit ${code} — перевір ${STORYBOOK_STRYKER_CONFIG} ` +
672
+ '(testRunner: "command", commandRunner.command, jsonReporter.fileName) ' +
673
+ 'і define __STRYKER_ACTIVE_MUTANT__ у vite-конфізі browser-проєкту'
674
+ )
675
+ }
676
+ if (existsSync(reportPath)) {
677
+ const report = JSON.parse(await readFile(reportPath, 'utf8'))
678
+ const parsed = parseStrykerReport(report, jsRoot)
679
+ console.log(
680
+ `✓ ${wsRel || '.'}: Storybook mutation (Stryker command runner) — ${parsed.caught}/${parsed.total} вбито`
681
+ )
682
+ return {
683
+ coverage,
684
+ mutation: { caught: parsed.caught, total: parsed.total },
685
+ survived: parsed.survived.map(group => ({
686
+ ...group,
687
+ file: wsRel === '' ? group.file : join(wsRel, group.file)
688
+ }))
689
+ }
690
+ }
691
+ }
636
692
  console.error(
637
693
  `⚠ ${wsRel || '.'}: Storybook (Vue) — mutation testing пропущено ` +
638
- '(у full-режимі надто дорого: повний browser-mode suite на кожен мутант), лише line coverage'
694
+ `(${hasCanonicalConfig ? 'runner без runStorybookStrykerFull' : `нема ${STORYBOOK_STRYKER_CONFIG}`}), лише line coverage`
695
+ )
696
+ return { coverage, mutation: { caught: 0, total: 0 }, survived: [] }
697
+ }
698
+
699
+ // Changed-режим: guard на runStorybookMutantTest тримає сумісність із
700
+ // інжектованими runner-ами без mutation-підтримки (лише line coverage).
701
+ if (typeof runner.runStorybookMutantTest !== 'function') {
702
+ console.error(
703
+ `⚠ ${wsRel || '.'}: Storybook (Vue) — mutation testing пропущено (runner без runStorybookMutantTest), лише line coverage`
639
704
  )
640
705
  return { coverage, mutation: { caught: 0, total: 0 }, survived: [] }
641
706
  }