Skip to content

fix(repo): repair the vm runner and config subpath after 11.0.0 - #110

Merged
ryanleecode merged 14 commits into
mainfrom
bug-fixes2
Sep 25, 2026
Merged

ryanleecode merged 14 commits into
mainfrom
bug-fixes2

Conversation

@systemfsoftware-maker

@systemfsoftware-maker systemfsoftware-maker commented Sep 25, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Six regressions users hit after upgrading to 11.0.0 now work again: defineConfig/mergeConfig config files, strict-pnpm installs of the default vm runner, and four vm runner divergences from vitest run.

  • import { defineConfig, mergeConfig } from '@systemfsoftware/stryker-js/config' loads and typechecks again, and const config: StrykerConfig = defineConfig({...}) typechecks. The 11.0.0 StrykerConfig class statics are removed; the changeset is a major and gives the migration.
  • A pnpm project that installs only stryker-js and vitest completes a vm dry run. The harness is now located from its own install instead of from the project's cwd. effect is now a required peer rather than an optional one: optional peers are never installed, so the CLI failed to read its config without effect, even though it imports effect at runtime.
  • import.meta.vitest inside a string, template, comment or regex literal is no longer rewritten, so RuleTester-style fixtures pass.
  • A test file with no tests passes when that project sets passWithNoTests.
  • Setup files and helpers outside the project root get the harness vitest, so a module-scope beforeEach no longer fails with "Vitest failed to find the runner".
  • A failed dry run names each failing test and its message, both in the error and at the default log level.

CI repairs

main's CI was already red (typescript-checker#lint:tsgo, instrumenter and test-contribution timeouts), and this branch surfaced three more flakes. All are fixed at the root, with no suppressions and no raised timeouts:

  • TypeScriptCompiler is declared as a Context.Service class, which clears the lint:tsgo error.
  • The concurrency opt-in selection scenario instruments only the fixtures that carry each mutator (1.4 s to 0.3 s), and the test-contribution laws draw only the fields the judgement reads (125 s to 25 s under CI settings). Both had crossed the 30 s timeout under CI coverage.
  • The vm runner concurrency scenario proves overlap with a rendezvous instead of a 300 ms window.
  • The worker-options round-trip laws no longer draw object keys with JSON escapes. V8 13.6 corrupts those keys once a lone-backslash key has been parsed in the process (nodejs/node#63785), which falsified a correct codec.
  • The passWithNoTests harness test resolves its sandbox through realPath, because macOS links /tmp to /private/tmp.
  • The vm parity differential records the unmutated baseline once per fixture instead of once per mutant (565 s to about 140 s).

Not in this PR

#100's enterprise-fixture parity half. Its hand-written live-export and Vite-id patches were dropped in favour of the vm runner redesign, #111, which is layer 2 of stack #112 on top of this PR. The four enterprise e2e configs stay on the vitest runner until then.

Fixes #104
Fixes #105
Fixes #106
Fixes #107
Fixes #108
Related: #100

Validation

Local: pnpm format:check, pnpm typecheck, pnpm test, pnpm check:ci and the changeset check pass. CI is green on ubuntu and macOS, including the e2e lane. For #105 I packed stryker-js and installed it into a fresh pnpm project (default isolated linker) whose only direct dependencies were stryker-js and vitest. There, a vm dry run and a mutation run both exit 0. That strict-pnpm install has no in-repo test: the e2e bake installs every tarball with npm.


Compound Engineering

The vm session added a failed `(no test suite)` result for every discovered
file that registered no tests, ignoring Vitest's `passWithNoTests`. The flag
now travels from the resolved Vitest project config through the schema,
defaults and extraction, and the session consults the file's own project
before reporting the failure, mirroring Vitest's
`!file.tasks.length && !config.passWithNoTests` check

Closes #107
The vm runner handed its in-memory vitest only to importers inside the
sandbox prefix or to files under node_modules/<served name>. A setup file or
helper outside the project root therefore received the real, runner-less
vitest: a module-scope registration either failed with "Vitest failed to find
the runner" or silently registered nothing, stopping the dry run as
init-failed.

Track the modules a run loads — any importer inside an active sandbox, any
importer carrying a run salt, and any module reached from one of those — and
serve the harness vitest to all of them. The served shims now read the
current run state when their registration API is called, so an unsalted,
shared module registers against the current test file's runner rather than a
stale session's

Closes #108
The vm runner replaced every textual `import.meta.vitest` in a loaded
module with a global regex, so a string or template literal, a comment,
or a regex literal that mentioned the marker reached the program
corrupted. A suite whose fixtures contain the marker (a lint-rule
RuleTester, for example) failed on the vm runner while passing under
`vitest run`, because Vitest only rewrites modules it discovered through
`includeSource`.

Replace the regex with a scanner that rewrites only real member
expressions and copies string, template, comment and regex text byte for
byte. The scanner tracks template substitutions (an expression inside
`${…}` is still rewritten), escaped quotes and backticks, and picks
between a regex literal and division from the last significant token.

Both the host-transform path (`hostResult`) and the loaded-source path of
the transform plugin use the scanner, so in-source `if (import.meta.vitest)`
tests still register and run

Closes #106
… cwd

The vm test runner's Vitest host worker resolved @systemfsoftware/stryker-vm-harness/package.json with a createRequire rooted at process.cwd(). Under pnpm's default isolated node_modules layout the harness is installed beside @systemfsoftware/stryker-js and is not visible from the project root, so a project that installed only stryker-js and vitest failed its dry run with: Cannot find module '@systemfsoftware/stryker-vm-harness/package.json'.

The lookup now uses createRequire(import.meta.url), resolving the harness from where the harness itself is installed — the same self-resolution worker-client.ts already uses — while the project's own vitest keeps resolving from the project. The dist-less fallback also pointed at a stale src/shell/vitest-host directory; it now points at src/vitest-host, so a source checkout with no build still starts the host worker

Closes #105
The compound-pack refactor moved the config helpers onto a `StrykerConfig`
class, so `@systemfsoftware/stryker-js/config` stopped exporting
`defineConfig` and `mergeConfig` and `StrykerConfig` became a class-instance
type. A `stryker.config.ts` written against the 10.1.1 surface then failed at
load time, and `const config: StrykerConfig = defineConfig({ … })` failed to
typecheck, with nothing in the 11.0.0 changelog explaining why.

`defineConfig` returns the four 10.1.1 overloads (object, promise, and the
`(env: ConfigEnv) => …` factory forms) and `mergeConfig(defaults, overrides)`
composes presets again, both as named exports of `./config`. `StrykerConfig`
names the partial options a config file writes. The schema-consuming runtime
values moved out of `stryker-config.schema.ts` into `config/default-options.ts`
so the schema module only declares schemas and types

Closes #104
A failing dry run reported only counts, so the stage failure, its cause and the machine-mode error envelope named neither the failing tests nor their assertion messages.

The dry-run decision now carries every failing test's name and failure message (DryRunFailed.failedTests, fed from the runner's failed TestResults); the stage failure's reason renders them and the dry run logs them at error level, so the default-level log and the error event both name each failure

Closes #100
The real-vitest reference arm re-copied the fixture and spawned a cold 'vitest run --update' for every mutant, and both arms ran the full vm mutation engine to list the same mutants. Record the unmutated baseline once per fixture, copy it per mutant, and share one cached engine report between the arms; the mocking mutant case drops from 65s to 32s locally

Refs #110
effect-tsgo rejects a Context.Service assigned to a variable; a named shape interface keeps the declaration emit free of the handle's private slot type

Refs #110
The selection scenario instrumented the whole 528-mutant corpus eight times and crossed the 30s timeout under CI coverage; it now reads one placement site per mutator and keeps the same checks

Refs #110
…ment reads

Generating whole mutation reports per draw pushed three laws past the 30s timeout on CI

Refs #110
A fixed 300ms window flaked under CI load; each suite now waits for its sibling to start

Refs #110
…he bug

V8 13.6 corrupts escaped JSON object keys once a lone-backslash key is parsed (nodejs/node#63785); the law domain now draws only unescaped keys and the ineffective seed pin is removed

Refs #110
macOS links /tmp to /private/tmp, so results keyed by the real path never matched the sandbox path the test looked up

Refs #110
effect was an optional peer although the CLI imports it at runtime, so a pnpm project that installed only stryker-js and vitest failed to read its config. Required peers are installed by pnpm and npm, so the #105 install works and Effect users still share one copy

Refs #105
@systemfsoftware-maker
systemfsoftware-maker added this pull request to stack #112 September 25, 2026 14:15
@ryanleecode
ryanleecode merged commit e37907b into main Sep 25, 2026
10 checks passed
@ryanleecode
ryanleecode deleted the bug-fixes2 branch September 25, 2026 14:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment