From 45af0e04aed83107822ed58c4f21d608eb825251 Mon Sep 17 00:00:00 2001 From: qnbs <155236708+qnbs@users.noreply.github.com> Date: Sat, 22 Aug 2026 10:41:58 +0200 Subject: [PATCH 1/3] test(mutation): bind copilot shard to direct tests --- .github/workflows/mutation.yml | 66 +++++++++++++++---- README.md | 8 +-- scripts/stryker-scope.d.mts | 5 ++ scripts/stryker-scope.mjs | 13 +++- stryker-scope.json | 6 ++ .../tooling/strykerWorkflowPolicy.test.ts | 38 +++++++++++ 6 files changed, 117 insertions(+), 19 deletions(-) diff --git a/.github/workflows/mutation.yml b/.github/workflows/mutation.yml index e4d304b51..49e5ae094 100644 --- a/.github/workflows/mutation.yml +++ b/.github/workflows/mutation.yml @@ -87,35 +87,76 @@ jobs: stryker-incremental-${{ matrix.name }}- - name: Prepare incremental file + env: + MATRIX_NAME: ${{ matrix.name }} + MODE_INPUT: ${{ github.event.inputs.mode || 'incremental' }} run: | + if [[ ! "$MATRIX_NAME" =~ ^[a-z0-9-]+$ ]]; then + echo "Invalid Stryker module name: $MATRIX_NAME" >&2 + exit 1 + fi + INCREMENTAL_FILE="reports/stryker-incremental-${MATRIX_NAME}.json" mkdir -p reports - MODE="${{ github.event.inputs.mode || 'incremental' }}" + MODE="$MODE_INPUT" + if [[ "$MODE" != "incremental" && "$MODE" != "force" ]]; then + echo "Invalid Stryker mode: $MODE" >&2 + exit 1 + fi if [ "$MODE" = "force" ]; then - echo "๐Ÿงน Force mode โ†’ clearing incremental cache for ${{ matrix.name }}" - rm -f reports/stryker-incremental-${{ matrix.name }}.json - elif [ -f "reports/stryker-incremental-${{ matrix.name }}.json" ]; then - echo "โ™ป๏ธ Using existing incremental cache for ${{ matrix.name }}" + echo "๐Ÿงน Force mode โ†’ clearing incremental cache for $MATRIX_NAME" + rm -f "$INCREMENTAL_FILE" + elif [ -f "$INCREMENTAL_FILE" ]; then + echo "โ™ป๏ธ Using existing incremental cache for $MATRIX_NAME" else echo "๐Ÿ†• No incremental cache found โ€” will create new one" fi - name: Run Stryker (${{ matrix.name }}) + env: + MATRIX_NAME: ${{ matrix.name }} + MATRIX_MUTATE: ${{ matrix.mutate }} + MATRIX_TEST_FILES: ${{ join(matrix.testFiles, ',') }} + MODE_INPUT: ${{ github.event.inputs.mode || 'incremental' }} + CONCURRENCY_INPUT: ${{ github.event.inputs.per_job_concurrency || '2' }} + STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }} run: | - MODE="${{ github.event.inputs.mode || 'incremental' }}" + if [[ ! "$MATRIX_NAME" =~ ^[a-z0-9-]+$ ]]; then + echo "Invalid Stryker module name: $MATRIX_NAME" >&2 + exit 1 + fi + MODE="$MODE_INPUT" + if [[ "$MODE" != "incremental" && "$MODE" != "force" ]]; then + echo "Invalid Stryker mode: $MODE" >&2 + exit 1 + fi FORCE_FLAG="" if [ "$MODE" = "force" ]; then FORCE_FLAG="--force" fi - CONCURRENCY="${{ github.event.inputs.per_job_concurrency || '2' }}" + CONCURRENCY="$CONCURRENCY_INPUT" + if [[ ! "$CONCURRENCY" =~ ^[0-9]+%?$ ]]; then + echo "Invalid Stryker concurrency: $CONCURRENCY" >&2 + exit 1 + fi - echo "โ–ถ๏ธ Module: ${{ matrix.name }}" - echo " Pattern: ${{ matrix.mutate }}" + echo "โ–ถ๏ธ Module: $MATRIX_NAME" + echo " Pattern: $MATRIX_MUTATE" + TEST_FILES="$MATRIX_TEST_FILES" + if [ -n "$TEST_FILES" ]; then + echo " Test files: $TEST_FILES" + else + echo " Test files: Vitest related selection" + fi echo " Concurrency: $CONCURRENCY" echo " Mode: $MODE" # QNBS-v3: pass the supported CLI option; arbitrary env vars are not Stryker config. - INCREMENTAL_FILE="reports/stryker-incremental-${{ matrix.name }}.json" + INCREMENTAL_FILE="reports/stryker-incremental-${MATRIX_NAME}.json" + TEST_FILE_ARGS=() + if [ -n "$TEST_FILES" ]; then + TEST_FILE_ARGS+=(--testFiles "$TEST_FILES") + fi pnpm exec stryker run \ stryker.config.mjs \ @@ -123,10 +164,9 @@ jobs: --incrementalFile "$INCREMENTAL_FILE" \ $FORCE_FLAG \ --concurrency "$CONCURRENCY" \ - --mutate "${{ matrix.mutate }}" \ + --mutate "$MATRIX_MUTATE" \ + "${TEST_FILE_ARGS[@]}" \ --reporters progress,json,html - env: - STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }} - name: Upload Stryker report if: always() diff --git a/README.md b/README.md index d0016fea4..e49201741 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ IndexedDB v8 PWA v3.0 i18n 19 locales โ€” 2924 keys - 6910+ tests / 568 files + 6911+ tests / 568 files Codecov Coverage License MIT CI Status @@ -511,7 +511,7 @@ The Settings โ†’ AI panel shows a live GPU status badge with adapter details and | **Document Export** | docx + jszip | Word-compatible `.docx` generation (lazy-loaded) | | **PWA** | Service Worker + Web App Manifest v3 | Offline support, installability, Workbox chunking | | **i18n** | Custom React Context (`I18nContext.tsx`) | 2924 keys ร— 19 locales (de/en/es/fr/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu/ru/ko Beta); EN fallback; `localStorage` persistence | -| **Testing** | Vitest 4.x (6910+ tests / 568 files) + Playwright E2E | Unit/integration + cross-browser E2E; Stryker mutation (manual workflow) | +| **Testing** | Vitest 4.x (6911+ tests / 568 files) + Playwright E2E | Unit/integration + cross-browser E2E; Stryker mutation (manual workflow) | | **Code Quality** | Biome (lint + format) + TypeScript 7 (tsgo) strict | `--error-on-warnings` in CI; zero `any` policy | | **Visualization** | Force-directed graph | Interactive character relationship network | | **Desktop** | Tauri v2 | Cross-platform installer; auto-updater via `latest.json` | @@ -549,7 +549,7 @@ WorldScript-Studio/ โ”‚ โ”œโ”€โ”€ sw.js # PWA Service Worker โ”‚ โ””โ”€โ”€ manifest.json # PWA Web App Manifest v3 โ”œโ”€โ”€ tests/ -โ”‚ โ”œโ”€โ”€ unit/ # Vitest unit tests (6910+ tests, 568 files) โ€” count spans tests/, components/, packages/*/tests/, not just this folder +โ”‚ โ”œโ”€โ”€ unit/ # Vitest unit tests (6911+ tests, 568 files) โ€” count spans tests/, components/, packages/*/tests/, not just this folder โ”‚ โ”‚ โ”œโ”€โ”€ ai/ # aiSmallModules, aiCoreFallbackPaths โ”‚ โ”‚ โ””โ”€โ”€ settings/ # WebLlmPanel, AiSections โ”‚ โ””โ”€โ”€ e2e/ # Playwright specs + helpers.ts @@ -711,7 +711,7 @@ The main pipeline is [`.github/workflows/ci.yml`](.github/workflows/ci.yml). Opt | `scorecard` | weekly + `main` push | OpenSSF Scorecard โ€” SARIF uploaded to GitHub Code Scanning | **Current test metrics (2026-08-21, source-synchronized; CI remains authoritative for pass/fail):** -- **6910+ unit tests** across **568 test files** โ€” CI is authoritative for pass/fail +- **6911+ unit tests** across **568 test files** โ€” CI is authoritative for pass/fail - Coverage thresholds: lines โ‰ฅ 80 ยท branches โ‰ฅ 66 ยท functions โ‰ฅ 72 ยท statements โ‰ฅ 78 โ€” enforced in CI (see Codecov badge for live metrics) - i18n: **2924 keys ร— 19 locales** (en/de/fr/es/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu/ru/ko Beta) diff --git a/scripts/stryker-scope.d.mts b/scripts/stryker-scope.d.mts index 46b51a895..47d8b097c 100644 --- a/scripts/stryker-scope.d.mts +++ b/scripts/stryker-scope.d.mts @@ -5,8 +5,13 @@ export interface StrykerMutationModule { name: string; riskTier: StrykerRiskTier; mutate: string; + testFiles: string[]; } export const mutationFiles: string[]; export const mutationModules: StrykerMutationModule[]; export function selectMutationModules(selector?: string): StrykerMutationModule[]; +export function validateScope(scopeDefinition: unknown): { + moduleNames: Set; + mutationFiles: Set; +}; diff --git a/scripts/stryker-scope.mjs b/scripts/stryker-scope.mjs index 553490504..8e211f2cc 100644 --- a/scripts/stryker-scope.mjs +++ b/scripts/stryker-scope.mjs @@ -7,7 +7,7 @@ const scopePath = path.join(repositoryRoot, 'stryker-scope.json'); export const scope = JSON.parse(readFileSync(scopePath, 'utf8')); -function validateScope(scopeDefinition) { +export function validateScope(scopeDefinition) { if (!Array.isArray(scopeDefinition.modules) || scopeDefinition.modules.length === 0) { throw new Error('Stryker scope must define at least one module.'); } @@ -27,6 +27,9 @@ function validateScope(scopeDefinition) { moduleNames.add(module.name); if (module.mutate.length === 0) throw new Error(`Stryker module has no targets: ${module.name}`); + if (module.testFiles !== undefined && !Array.isArray(module.testFiles)) { + throw new Error(`Stryker testFiles must be an array: ${module.name}`); + } for (const file of module.mutate) { if (typeof file !== 'string' || mutationFiles.has(file)) { throw new Error(`Duplicate or invalid Stryker target: ${file}`); @@ -36,16 +39,22 @@ function validateScope(scopeDefinition) { } mutationFiles.add(file); } + for (const file of module.testFiles ?? []) { + if (typeof file !== 'string' || !existsSync(path.join(repositoryRoot, file))) { + throw new Error(`Stryker test file does not exist: ${file}`); + } + } } return { moduleNames, mutationFiles }; } const validatedScope = validateScope(scope); export const mutationFiles = [...validatedScope.mutationFiles]; -export const mutationModules = scope.modules.map(({ name, riskTier, mutate }) => ({ +export const mutationModules = scope.modules.map(({ name, riskTier, mutate, testFiles = [] }) => ({ name, riskTier, mutate: mutate.join(','), + testFiles, })); export function selectMutationModules(selector = 'all') { diff --git a/stryker-scope.json b/stryker-scope.json index 27bd89228..3cad8c23b 100644 --- a/stryker-scope.json +++ b/stryker-scope.json @@ -63,6 +63,12 @@ "services/copilot/insightGenerator.ts", "services/copilot/actionApplier.ts", "services/copilot/copilotContextService.ts" + ], + "testFiles": [ + "tests/unit/copilot/heuristicEngine.test.ts", + "tests/unit/copilot/insightGenerator.test.ts", + "tests/unit/copilot/actionApplier.test.ts", + "tests/unit/copilot/copilotContextService.test.ts" ] }, { diff --git a/tests/unit/tooling/strykerWorkflowPolicy.test.ts b/tests/unit/tooling/strykerWorkflowPolicy.test.ts index b373b99dd..d5f351897 100644 --- a/tests/unit/tooling/strykerWorkflowPolicy.test.ts +++ b/tests/unit/tooling/strykerWorkflowPolicy.test.ts @@ -7,6 +7,7 @@ import { mutationFiles, mutationModules, selectMutationModules, + validateScope, } from '../../../scripts/stryker-scope.mjs'; import config from '../../../stryker.config.mjs'; @@ -17,6 +18,7 @@ const workflow = readFileSync(workflowPath, 'utf8'); const scopeScriptPath = fileURLToPath( new URL('../../../scripts/stryker-scope.mjs', import.meta.url), ); +const githubExpression = (expression: string) => '$' + '{{ ' + expression + ' }}'; // QNBS-v3: Lock workflow/config invariants so mutation plumbing cannot silently drift. describe('Stryker workflow policy', () => { @@ -28,6 +30,12 @@ describe('Stryker workflow policy', () => { expect(mutationModules.every(({ riskTier }) => ['A', 'B'].includes(riskTier))).toBe(true); expect(selectMutationModules('tier-a').every(({ riskTier }) => riskTier === 'A')).toBe(true); expect(selectMutationModules('services-commands')).toHaveLength(1); + expect(selectMutationModules('copilot')[0]?.testFiles).toEqual([ + 'tests/unit/copilot/heuristicEngine.test.ts', + 'tests/unit/copilot/insightGenerator.test.ts', + 'tests/unit/copilot/actionApplier.test.ts', + 'tests/unit/copilot/copilotContextService.test.ts', + ]); const matrix = JSON.parse( execFileSync(process.execPath, [scopeScriptPath, '--matrix'], { encoding: 'utf8' }), ); @@ -36,6 +44,17 @@ describe('Stryker workflow policy', () => { it('uses supported incremental plumbing and preserves shard identity', () => { expect(workflow).toContain('--incrementalFile "$INCREMENTAL_FILE"'); + expect(workflow).toContain('MATRIX_NAME: ' + githubExpression('matrix.name')); + expect(workflow).toContain('MATRIX_MUTATE: ' + githubExpression('matrix.mutate')); + expect(workflow).toContain( + 'MATRIX_TEST_FILES: ' + githubExpression("join(matrix.testFiles, ',')"), + ); + expect(workflow).toContain('TEST_FILE_ARGS+=(--testFiles "$TEST_FILES")'); + expect(workflow).toContain('"${' + 'TEST_FILE_ARGS[@]}"'); + expect(workflow).not.toContain( + 'rm -f reports/stryker-incremental-' + githubExpression('matrix.name') + '.json', + ); + expect(workflow).not.toContain('--mutate "' + githubExpression('matrix.mutate') + '"'); expect(workflow).not.toContain('STRYKER_INCREMENTAL_FILE='); expect(workflow).toContain('merge-multiple: false'); expect(workflow).toContain('if-no-files-found: error'); @@ -49,4 +68,23 @@ describe('Stryker workflow policy', () => { expect(workflow).toContain('SELECTOR: $' + "{{ github.event.inputs.module || 'all' }}"); expect(workflow).not.toContain('force-all-modules'); }); + + it('validates optional module test-file mappings fail closed', () => { + const validModule = { + name: 'test-module', + riskTier: 'A' as const, + mutate: ['services/commands/fuzzyScore.ts'], + }; + + expect(() => validateScope({ modules: [validModule] })).not.toThrow(); + expect(() => + validateScope({ modules: [{ ...validModule, testFiles: 'not-an-array' }] }), + ).toThrow('Stryker testFiles must be an array'); + expect(() => validateScope({ modules: [{ ...validModule, testFiles: [42] }] })).toThrow( + 'Stryker test file does not exist', + ); + expect(() => + validateScope({ modules: [{ ...validModule, testFiles: ['tests/unit/missing.test.ts'] }] }), + ).toThrow('Stryker test file does not exist'); + }); }); From df88d712e570ebfcedeed76e74a09896ff052d6f Mon Sep 17 00:00:00 2001 From: qnbs <155236708+qnbs@users.noreply.github.com> Date: Sat, 22 Aug 2026 11:20:01 +0200 Subject: [PATCH 2/3] fix(mutation): make explicit test files discoverable --- stryker.config.mjs | 2 -- tests/unit/tooling/strykerWorkflowPolicy.test.ts | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/stryker.config.mjs b/stryker.config.mjs index 58c6a73fe..84e817c4d 100644 --- a/stryker.config.mjs +++ b/stryker.config.mjs @@ -37,8 +37,6 @@ export default { ignorePatterns: [ '**/dist/**', '**/node_modules/**', - '**/*.test.ts', - '**/*.spec.ts', '**/playwright-report/**', '**/storybook-static/**', ], diff --git a/tests/unit/tooling/strykerWorkflowPolicy.test.ts b/tests/unit/tooling/strykerWorkflowPolicy.test.ts index d5f351897..f710c0b56 100644 --- a/tests/unit/tooling/strykerWorkflowPolicy.test.ts +++ b/tests/unit/tooling/strykerWorkflowPolicy.test.ts @@ -25,6 +25,8 @@ describe('Stryker workflow policy', () => { it('uses one explicit target source for config and the matrix', () => { expect(config.mutate).toEqual(mutationFiles); expect(config['vitest']).toEqual(expect.objectContaining({ related: true })); + expect(config.ignorePatterns).not.toContain('**/*.test.ts'); + expect(config.ignorePatterns).not.toContain('**/*.spec.ts'); expect(mutationModules).toHaveLength(8); expect(new Set(mutationFiles).size).toBe(25); expect(mutationModules.every(({ riskTier }) => ['A', 'B'].includes(riskTier))).toBe(true); From a37a170bf5c9366002d5ca3deeb75ec182c7965e Mon Sep 17 00:00:00 2001 From: qnbs <155236708+qnbs@users.noreply.github.com> Date: Sat, 22 Aug 2026 11:34:09 +0200 Subject: [PATCH 3/3] fix(mutation): accept downloaded report layouts --- README.md | 8 ++--- scripts/aggregate-stryker-reports.mjs | 30 ++++++++++++++-- tests/unit/tooling/strykerAggregation.test.ts | 35 +++++++++++++++++++ 3 files changed, 66 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index e49201741..898583b30 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ IndexedDB v8 PWA v3.0 i18n 19 locales โ€” 2924 keys - 6911+ tests / 568 files + 6912+ tests / 568 files Codecov Coverage License MIT CI Status @@ -511,7 +511,7 @@ The Settings โ†’ AI panel shows a live GPU status badge with adapter details and | **Document Export** | docx + jszip | Word-compatible `.docx` generation (lazy-loaded) | | **PWA** | Service Worker + Web App Manifest v3 | Offline support, installability, Workbox chunking | | **i18n** | Custom React Context (`I18nContext.tsx`) | 2924 keys ร— 19 locales (de/en/es/fr/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu/ru/ko Beta); EN fallback; `localStorage` persistence | -| **Testing** | Vitest 4.x (6911+ tests / 568 files) + Playwright E2E | Unit/integration + cross-browser E2E; Stryker mutation (manual workflow) | +| **Testing** | Vitest 4.x (6912+ tests / 568 files) + Playwright E2E | Unit/integration + cross-browser E2E; Stryker mutation (manual workflow) | | **Code Quality** | Biome (lint + format) + TypeScript 7 (tsgo) strict | `--error-on-warnings` in CI; zero `any` policy | | **Visualization** | Force-directed graph | Interactive character relationship network | | **Desktop** | Tauri v2 | Cross-platform installer; auto-updater via `latest.json` | @@ -549,7 +549,7 @@ WorldScript-Studio/ โ”‚ โ”œโ”€โ”€ sw.js # PWA Service Worker โ”‚ โ””โ”€โ”€ manifest.json # PWA Web App Manifest v3 โ”œโ”€โ”€ tests/ -โ”‚ โ”œโ”€โ”€ unit/ # Vitest unit tests (6911+ tests, 568 files) โ€” count spans tests/, components/, packages/*/tests/, not just this folder +โ”‚ โ”œโ”€โ”€ unit/ # Vitest unit tests (6912+ tests, 568 files) โ€” count spans tests/, components/, packages/*/tests/, not just this folder โ”‚ โ”‚ โ”œโ”€โ”€ ai/ # aiSmallModules, aiCoreFallbackPaths โ”‚ โ”‚ โ””โ”€โ”€ settings/ # WebLlmPanel, AiSections โ”‚ โ””โ”€โ”€ e2e/ # Playwright specs + helpers.ts @@ -711,7 +711,7 @@ The main pipeline is [`.github/workflows/ci.yml`](.github/workflows/ci.yml). Opt | `scorecard` | weekly + `main` push | OpenSSF Scorecard โ€” SARIF uploaded to GitHub Code Scanning | **Current test metrics (2026-08-21, source-synchronized; CI remains authoritative for pass/fail):** -- **6911+ unit tests** across **568 test files** โ€” CI is authoritative for pass/fail +- **6912+ unit tests** across **568 test files** โ€” CI is authoritative for pass/fail - Coverage thresholds: lines โ‰ฅ 80 ยท branches โ‰ฅ 66 ยท functions โ‰ฅ 72 ยท statements โ‰ฅ 78 โ€” enforced in CI (see Codecov badge for live metrics) - i18n: **2924 keys ร— 19 locales** (en/de/fr/es/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu/ru/ko Beta) diff --git a/scripts/aggregate-stryker-reports.mjs b/scripts/aggregate-stryker-reports.mjs index f3b49aa7f..08a9bf363 100644 --- a/scripts/aggregate-stryker-reports.mjs +++ b/scripts/aggregate-stryker-reports.mjs @@ -1,4 +1,4 @@ -import { existsSync, readFileSync } from 'node:fs'; +import { existsSync, readdirSync, readFileSync } from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { mutationModules, selectMutationModules } from './stryker-scope.mjs'; @@ -101,13 +101,37 @@ function validateMetricRelationships(metrics, reportPath) { } } +function findReportPath(rootDirectory, moduleName, selectedModuleCount) { + const preferredPath = path.join(rootDirectory, `stryker-report-${moduleName}`, 'mutation.json'); + if (existsSync(preferredPath)) return preferredPath; + if (!existsSync(rootDirectory)) return undefined; + + const candidates = []; + const visit = (directory, depth) => { + for (const entry of readdirSync(directory, { withFileTypes: true })) { + const entryPath = path.join(directory, entry.name); + if (entry.isDirectory() && depth < 5) { + visit(entryPath, depth + 1); + continue; + } + if (!entry.isFile() || entry.name !== 'mutation.json') continue; + const relativeSegments = path.relative(rootDirectory, entryPath).split(path.sep); + const preservesModuleIdentity = relativeSegments.includes(`stryker-report-${moduleName}`); + if (preservesModuleIdentity || selectedModuleCount === 1) candidates.push(entryPath); + } + }; + visit(rootDirectory, 0); + + return candidates.length === 1 ? candidates[0] : undefined; +} + export function readStrykerReports(rootDirectory, selectedModules = mutationModules) { const reports = []; const missing = []; for (const module of selectedModules) { - const reportPath = path.join(rootDirectory, `stryker-report-${module.name}`, 'mutation.json'); - if (!existsSync(reportPath)) { + const reportPath = findReportPath(rootDirectory, module.name, selectedModules.length); + if (!reportPath) { missing.push(`${module.name}/mutation.json`); continue; } diff --git a/tests/unit/tooling/strykerAggregation.test.ts b/tests/unit/tooling/strykerAggregation.test.ts index 74bfa79a1..7fbb772a1 100644 --- a/tests/unit/tooling/strykerAggregation.test.ts +++ b/tests/unit/tooling/strykerAggregation.test.ts @@ -48,6 +48,30 @@ function writeReport(root: string, moduleName: string, overrides: Record { it('requires every authoritative scope module', async () => { const root = createReportRoot(); @@ -94,6 +118,17 @@ describe('Stryker report aggregation', () => { } }); + it('accepts a flat report when exactly one module is selected', async () => { + const root = createReportRoot(); + writeFlatReport(root); + const { selectMutationModules } = await import('../../../scripts/stryker-scope.mjs'); + + const result = aggregateStrykerReports(root, selectMutationModules('services-commands')); + + expect(result.reports).toHaveLength(1); + expect(result.reports[0]?.name).toBe('services-commands'); + }); + it('rejects inconsistent Stryker metrics instead of trusting report scores', async () => { const root = createReportRoot(); const { mutationModules } = await import('../../../scripts/stryker-scope.mjs');