# Stryker Vitest 5 runner — probe findings and upstream deltas Versions probed: Vitest 5.0.5, Stryker 10.0.0 (`@stryker-mutator/core`, `@stryker-mutator/api`), tsx 4.23.15, Node v22.23.3. Probed on 2026-10-23 in a scratch directory outside the repo. ## Probe findings (U11) 1. Name filter separator: **yes.** `vitest run test/unit/shared-type-extractors.test.ts -t 'extractElementTypeFromString returns undefined for empty string'` gives "Tests 108 skipped (108)" with exit 0. The same name joined with `' > '` gives "1 passed | 107 skipped". `-t` is a regular expression, so titles that contain `|`, `(`, `[` or `?` also need escaping. 2. Per-spec task ids: **yes.** `project.createSpecification(file, { testIds: ['-1575855114_0_5', '-1575855114_1_15'] })`, passed to `vitest.runTestSpecifications([spec])`, gives `{"skipped":106,"passed":2}`, and the 2 tests that ran are exactly the selected ids. Task ids were the same across runs. A spec with only unknown ids silently skips every test and reports no error. 3. Mutant active from module load, unfiltered run: **yes.** A copy of `shared.ts` was instrumented with Stryker 10's `Instrumenter` (1232 mutants, 245 hit at load). Static mutant 4 (`new Set(['Keys'])` to `[]`) was activated by `__STRYKER_ACTIVE_MUTANT__=4` (CLI, and the Node API with the env set between runs) or by `provide` plus a setup file that sets `globalThis.__stryker__.activeMutant`. Each run executed all 108 tests and gave "1 failed | 107 passed" (AssertionError). With no mutant active, all 108 tests passed. 4. TypeScript plugin in child processes: **yes.** A `.ts` plugin exports `strykerPlugins` built with `declareClassPlugin(PluginKind.TestRunner, ...)` and `strykerValidationSchema`, and imports a `.ts` enum through a `.js` specifier. It is loaded through `appendPlugins` with `testRunnerNodeArgs: ["--import", "tsx"]`, and the main process is started as `node --import tsx stryker.js run`. The dry run and mutant runs passed in a child with `execArgv ["--import","tsx"]` and `NODE_OPTIONS` null. Controls: with no tsx in the main process, the plugin fails to load in the main process. With `testRunnerNodeArgs: []`, the child fails with "Cannot find TestRunner plugin". 5. Setup-file task meta and `afterAll` suite: **yes, with a constraint.** Meta written in a setup file reaches the main process through `testCase.meta()` and `testModule.meta()`: from `beforeEach` and `afterEach`, on finished tests from `afterAll`, and on the file task. `afterAll` receives the file task as its second argument. Vitest 5 parses the first argument as a fixture pattern, so the hook must be written `afterAll(({}, suite) => ...)`. Writing `(ctx, suite) =>` throws a FixtureParseError at registration, which fails the file and skips every test. 6. Suite `beforeAll` has no current test, and the setup-file `beforeAll` runs first: **yes.** The observed order is setup `beforeAll` (file task), then `Outer.beforeAll`. In both, `TestRunner.getCurrentTest()` is undefined and `expect.getState().currentTestName` is undefined. In a later describe's `beforeAll` and in `afterAll`, `expect.getState().currentTestName` still holds the previous test's name ("Outer > t2"). Only `TestRunner.getCurrentTest()` can therefore tell that no test is running. ## Residual deltas against upstream What an upstream `@stryker-mutator/vitest-runner` release needs before this plugin can be dropped (KTD1). Status on 2026-10-24: the latest release is 10.0.0, and nothing has been filed upstream. A release that covers every item must still pass gates 1-4 before the plugin goes. - **Per-test failure classes.** Stryker's mutant run result has no field for them, only killed, survived or timeout plus `killedBy`. Gates 2-4 and the assertion-only kill rule need the class of every failing test (assertion, runtime, hook, timeout) and of every failure outside a test (a failed `beforeAll`, a collection error). The plugin writes them to sidecar files (KTD6, KTD17). Upstream needs a channel for them. - **Name filter fixes in a release.** The equivalent of #6214 (`' > '` separator, merged, unreleased) and #6246 (end anchoring). Even with both, titles hold regex metacharacters and duplicate full names, so name patterns stay fragile: see task ids below. - **Keep the project's pool (#6236).** Upstream forces the threads pool. The repo runs `forks`, and the plugin refuses to start unless the pool is `forks` with isolation on. - **NoCoverage, not Survived (#6256).** A mutant no test covers must be reported as NoCoverage. Gate 3 treats the two as one class but keeps the raw status in its report. - **Selection by Vitest task id.** Select the tests of a mutant run with `project.createSpecification(file, { testIds })` and `runTestSpecifications`, never related mode or name patterns, and check that the executed set equals the requested set: a specification with only unknown ids silently skips every test. Static mutants run exactly the dry run's test set. - **A dry-run manifest shared across processes.** Only one pool process runs the dry run. The others, and replacements after a crash, a timeout or `maxTestRunnerReuse`, only call `init` and `mutantRun`, so test ids, task ids and the expected test set per file must reach them from outside that process. The plugin writes a run manifest atomically to the run directory, and every process loads it before its first mutant run (KTD16). - **Hook-time hit attribution.** A mutant hit after collection with no current test (a suite's `beforeAll`) is coverage for every test of the suite whose hook ran. Only load- and collection-time hits make a mutant static. The current test must come from `TestRunner.getCurrentTest()`, because `expect.getState().currentTestName` is stale in later hooks (finding 6). - **Single-test selection in the dry run (Phase 1).** The runner's `vitest5.testIds` option (from a driver scope's `testIds`) restricts the whole run to chosen tests: the dry run collects the scope's files with `vitest.collectTests`, assigns KTD3 ids, maps the requested ids to task ids, runs one `createSpecification(file, { testIds })` per file and checks that exactly the requested tests ran. An id the collection did not find fails the dry run, naming it, before any mutant run. The run manifest then holds only the selected tests, so static mutants run them alone too. - **A hook marker per mutant (Phase 1).** Hook-attributed coverage cannot be told apart from a test's own hits once it is merged. The setup file keeps the attributed hook bucket apart in the file metadata, the dry-run sidecar record (schema 2) carries, per mutant, the tests a suite hook attributed it to, and `results.json` (schema 2) exposes it as `hookAttributedTests`, next to every attempt's per-test classes (`perTest`) and `attemptsDisagree`. Upstream needs a channel for both. - **The `({}, suite)` hook signature.** Vitest 5 parses the first hook parameter as a fixture pattern, so a setup-file hook that needs the suite must be written `afterAll(({}, suite) => ...)`. A plain identifier throws a FixtureParseError and fails the file (finding 5).