@7n/test 0.15.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 +12 -0
- package/package.json +1 -1
- package/src/coverage/js-collector.mjs +135 -37
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
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
|
+
|
|
9
|
+
## [0.16.0] - 2026-07-18
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- 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
|
|
14
|
+
|
|
3
15
|
## [0.15.0] - 2026-07-18
|
|
4
16
|
|
|
5
17
|
### Added
|
package/package.json
CHANGED
|
@@ -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`,
|
|
@@ -77,6 +78,13 @@ export function scopeToStorybookRoot(changedFiles, cwd, jsRoot) {
|
|
|
77
78
|
const VITEST_HINT =
|
|
78
79
|
'js coverage: vitest відсутній у package.json — додай `vitest`, `@vitest/coverage-v8` та `@stryker-mutator/vitest-runner` у devDependencies (див. test.mdc)'
|
|
79
80
|
|
|
81
|
+
/** Canonical config-файл для full-режиму Storybook-мутації через Stryker command runner. */
|
|
82
|
+
const STORYBOOK_STRYKER_CONFIG = 'stryker.storybook.config.mjs'
|
|
83
|
+
/** Шлях mutation.json того ж прогону (окремий від `reports/stryker/` JS-виміру). */
|
|
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/
|
|
87
|
+
|
|
80
88
|
/**
|
|
81
89
|
* Чи у пакеті встановлено vitest (через dependencies або devDependencies).
|
|
82
90
|
* @param {{dependencies?: Record<string,string>, devDependencies?: Record<string,string>}} pkg package.json
|
|
@@ -306,20 +314,36 @@ const defaultRunner = {
|
|
|
306
314
|
// зайве дублювання з collectStorybookForRoot і ризик втягнути Playwright-залежність
|
|
307
315
|
// у звичайний coverage-прогін.
|
|
308
316
|
const projectArgs = excludeStorybookProject ? ['--project=!storybook'] : []
|
|
309
|
-
const
|
|
310
|
-
'
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
{ cwd, stdio: 'inherit', env: process.env }
|
|
322
|
-
|
|
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
|
|
323
347
|
return r.status ?? 1
|
|
324
348
|
},
|
|
325
349
|
runBunCoverage({ cwd, lcovDir }) {
|
|
@@ -406,6 +430,24 @@ const defaultRunner = {
|
|
|
406
430
|
? spawnSync(strykerBin, ['run', ...mutateArgs], { cwd, stdio: 'inherit', env: process.env })
|
|
407
431
|
: spawnSync('npx', ['@stryker-mutator/core', 'run', ...mutateArgs], { cwd, stdio: 'inherit', env: process.env })
|
|
408
432
|
return r.status ?? 1
|
|
433
|
+
},
|
|
434
|
+
runStorybookStrykerFull({ cwd }) {
|
|
435
|
+
// Full-режим Storybook-мутація через canonical `stryker.storybook.config.mjs`
|
|
436
|
+
// (окремий Stryker-прогін, testRunner:'command' — vitest-runner browser mode не
|
|
437
|
+
// підтримує, див. коментар над collectStorybookForRoot). Спайк 2026-07 підтвердив:
|
|
438
|
+
// command runner + vitest browser mode ПРАЦЮЄ з `define` для
|
|
439
|
+
// __STRYKER_ACTIVE_MUTANT__ у vite-конфізі browser-проєкту — канонічний контракт
|
|
440
|
+
// конфіга задокументовано в npm/docs/stryker-storybook-config.md. Той самий
|
|
441
|
+
// резолвінг local-bin, що й runStryker (plugin-discovery відносно install-каталогу).
|
|
442
|
+
const strykerBin = resolveLocalStrykerBin()
|
|
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]
|
|
447
|
+
const r = strykerBin
|
|
448
|
+
? spawnSync(strykerBin, args, { cwd, stdio: 'inherit', env: process.env })
|
|
449
|
+
: spawnSync('npx', ['@stryker-mutator/core', ...args], { cwd, stdio: 'inherit', env: process.env })
|
|
450
|
+
return r.status ?? 1
|
|
409
451
|
}
|
|
410
452
|
}
|
|
411
453
|
|
|
@@ -443,18 +485,28 @@ const defaultRunner = {
|
|
|
443
485
|
*/
|
|
444
486
|
async function collectOneRoot(jsRoot, cwd, runner, scope = null) {
|
|
445
487
|
const wsRel = relative(cwd, jsRoot)
|
|
446
|
-
// У changed-режимі production-файли для мутації = змінені JS/Vue цього root без
|
|
447
|
-
// тест-файлів і без *.stories.* (сторі — не production-код, окремий Storybook-вимір).
|
|
448
|
-
// `.vue` тут ЗАЛИШАЄТЬСЯ — Stryker core мутує <script>/<script setup> SFC (з v7+),
|
|
449
|
-
// допоки покриття дають НЕ-browser-mode тести (@vue/test-utils тощо, не Storybook-сторі).
|
|
450
|
-
const mutateSrc = scope ? scope.files.filter(f => !TEST_FILE.test(f) && !STORIES_FILE_RE.test(f)) : null
|
|
451
488
|
|
|
452
489
|
// Bun-native workspace: coverage через `bun test`, mutation пропускається
|
|
453
490
|
// (Stryker vitest-runner не виконає код з `import ... from 'bun'`).
|
|
454
491
|
const bunNative = await isBunNativeRoot(jsRoot)
|
|
455
492
|
// Storybook root: named vitest-проєкт "storybook" (browser mode) типово ділить один
|
|
456
493
|
// vitest.config.mjs зі звичайним suite — виключаємо його з JS-прогону (див. runJsCoverage).
|
|
457
|
-
const
|
|
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
|
|
458
510
|
if (bunNative && !(await hasRunnableTests(jsRoot))) {
|
|
459
511
|
// `bun test` без тестів завершується помилкою — graceful skip як vitest --passWithNoTests.
|
|
460
512
|
return scope
|
|
@@ -558,8 +610,10 @@ async function collectOneRoot(jsRoot, cwd, runner, scope = null) {
|
|
|
558
610
|
* Mutation testing: у **changed-режимі** виконується власним mutate→run→restore
|
|
559
611
|
* executor-ом (`storybook-mutation.mjs`) — детерміновані AST-мутанти по змінених
|
|
560
612
|
* production-файлах, вбиває/милує реальний browser-mode прогін. У **full-режимі**
|
|
561
|
-
*
|
|
562
|
-
*
|
|
613
|
+
* власний executor надто дорогий (усі production-файли, не лише змінені) — тут шлях
|
|
614
|
+
* лише через canonical Stryker command-runner (`STORYBOOK_STRYKER_CONFIG`, див. нижче
|
|
615
|
+
* і `npm/docs/stryker-storybook-config.md`), якщо target-проєкт його налаштував;
|
|
616
|
+
* інакше чесний skip з попередженням (той самий принцип, що й bun-native).
|
|
563
617
|
*
|
|
564
618
|
* **Чому не Stryker (перевіряй перед покладанням на це в майбутньому — площина
|
|
565
619
|
* активно змінюється з обох боків):** issue stryker-js#4557 ("[vitest] support browser
|
|
@@ -576,24 +630,29 @@ async function collectOneRoot(jsRoot, cwd, runner, scope = null) {
|
|
|
576
630
|
* Meta ACH): детермінована генерація мутантів + реальний прогін як суддя — без LLM
|
|
577
631
|
* у контурі виконання.
|
|
578
632
|
*
|
|
579
|
-
*
|
|
580
|
-
*
|
|
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 ПРАЦЮЄ з обов'язковим
|
|
581
638
|
* `define: { 'process.env.__STRYKER_ACTIVE_MUTANT__': JSON.stringify(process.env.__STRYKER_ACTIVE_MUTANT__ ?? '') }`
|
|
582
|
-
* у vite-конфізі
|
|
583
|
-
*
|
|
584
|
-
*
|
|
585
|
-
*
|
|
586
|
-
*
|
|
587
|
-
*
|
|
588
|
-
*
|
|
589
|
-
*
|
|
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-коду.
|
|
590
649
|
*
|
|
591
650
|
* Changed-режим: запускається тільки якщо серед змінених файлів root-а є хоча б
|
|
592
651
|
* один `.vue`/`*.stories.*` (`scope.files` — вже звужений через `scopeToStorybookRoot`
|
|
593
652
|
* на боці виклику); інакше `null` (root пропускається повністю для цього виміру).
|
|
594
653
|
* @param {string} jsRoot абсолютний шлях workspace-кореня
|
|
595
654
|
* @param {string} cwd корінь проєкту (для рібейзингу `survived[].file`)
|
|
596
|
-
* @param {{runStorybookCoverage:Function, runStorybookMutantTest?:Function, proposeStorybookLlmMutants?:Function}} runner spawn-ін'єкція
|
|
655
|
+
* @param {{runStorybookCoverage:Function, runStorybookMutantTest?:Function, proposeStorybookLlmMutants?:Function, runStorybookStrykerFull?:Function}} runner spawn-ін'єкція
|
|
597
656
|
* @param {{files:string[], base:string|null}|null} [scope] changed-scope (null = full-режим)
|
|
598
657
|
* @returns {Promise<{coverage:object, mutation:{caught:number,total:number}, survived:Array<object>} | null>} результат або null коли root не Storybook/без сторі/без relevant-змін
|
|
599
658
|
*/
|
|
@@ -630,12 +689,51 @@ async function collectStorybookForRoot(jsRoot, cwd, runner, scope = null) {
|
|
|
630
689
|
await rm(lcovDir, { recursive: true, force: true })
|
|
631
690
|
}
|
|
632
691
|
|
|
633
|
-
// Full-режим:
|
|
634
|
-
//
|
|
635
|
-
|
|
692
|
+
// Full-режим: власний executor надто дорогий (повний suite × кожен мутант ×
|
|
693
|
+
// Chromium на ВСІХ production-файлах, не лише змінених) — тут шлях лише через
|
|
694
|
+
// canonical Stryker command-runner (STORYBOOK_STRYKER_CONFIG), якщо target-проєкт
|
|
695
|
+
// його налаштував (див. npm/docs/stryker-storybook-config.md). Без canonical
|
|
696
|
+
// конфіга або без runStorybookStrykerFull у runner-і — чесний skip, як і раніше.
|
|
697
|
+
if (!scope) {
|
|
698
|
+
const hasCanonicalConfig = existsSync(join(jsRoot, STORYBOOK_STRYKER_CONFIG))
|
|
699
|
+
if (hasCanonicalConfig && typeof runner.runStorybookStrykerFull === 'function') {
|
|
700
|
+
const code = await runner.runStorybookStrykerFull({ cwd: jsRoot })
|
|
701
|
+
const reportPath = join(jsRoot, STORYBOOK_STRYKER_REPORT)
|
|
702
|
+
if (code !== 0 && !existsSync(reportPath)) {
|
|
703
|
+
throw new Error(
|
|
704
|
+
`Storybook Stryker (command runner) exit ${code} — перевір ${STORYBOOK_STRYKER_CONFIG} ` +
|
|
705
|
+
'(testRunner: "command", commandRunner.command, jsonReporter.fileName) ' +
|
|
706
|
+
'і define __STRYKER_ACTIVE_MUTANT__ у vite-конфізі browser-проєкту'
|
|
707
|
+
)
|
|
708
|
+
}
|
|
709
|
+
if (existsSync(reportPath)) {
|
|
710
|
+
const report = JSON.parse(await readFile(reportPath, 'utf8'))
|
|
711
|
+
const parsed = parseStrykerReport(report, jsRoot)
|
|
712
|
+
console.log(
|
|
713
|
+
`✓ ${wsRel || '.'}: Storybook mutation (Stryker command runner) — ${parsed.caught}/${parsed.total} вбито`
|
|
714
|
+
)
|
|
715
|
+
return {
|
|
716
|
+
coverage,
|
|
717
|
+
mutation: { caught: parsed.caught, total: parsed.total },
|
|
718
|
+
survived: parsed.survived.map(group => ({
|
|
719
|
+
...group,
|
|
720
|
+
file: wsRel === '' ? group.file : join(wsRel, group.file)
|
|
721
|
+
}))
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
636
725
|
console.error(
|
|
637
726
|
`⚠ ${wsRel || '.'}: Storybook (Vue) — mutation testing пропущено ` +
|
|
638
|
-
|
|
727
|
+
`(${hasCanonicalConfig ? 'runner без runStorybookStrykerFull' : `нема ${STORYBOOK_STRYKER_CONFIG}`}), лише line coverage`
|
|
728
|
+
)
|
|
729
|
+
return { coverage, mutation: { caught: 0, total: 0 }, survived: [] }
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Changed-режим: guard на runStorybookMutantTest тримає сумісність із
|
|
733
|
+
// інжектованими runner-ами без mutation-підтримки (лише line coverage).
|
|
734
|
+
if (typeof runner.runStorybookMutantTest !== 'function') {
|
|
735
|
+
console.error(
|
|
736
|
+
`⚠ ${wsRel || '.'}: Storybook (Vue) — mutation testing пропущено (runner без runStorybookMutantTest), лише line coverage`
|
|
639
737
|
)
|
|
640
738
|
return { coverage, mutation: { caught: 0, total: 0 }, survived: [] }
|
|
641
739
|
}
|