// Generated by dts-bundle-generator v9.5.1 import { StrykerOptions } from '@stryker-mutator/api/core'; import { Logger } from '@stryker-mutator/api/logging'; import { PluginKind } from '@stryker-mutator/api/plugin'; import { DryRunResult, MutantRunOptions, MutantRunResult, TestRunner, TestRunnerCapabilities } from '@stryker-mutator/api/test-runner'; /** * Bun test runner for Stryker mutation testing */ export declare class BunTestRunner implements TestRunner { private readonly logger; static readonly inject: [ "logger", "options" ]; private readonly bunPath; private readonly timeout; private readonly inspectorTimeout; private readonly env?; private readonly bunArgs?; private readonly testFilesOverride?; private readonly mutateGlobs; private readonly smol; private readonly maxChildRss?; private readonly rssCheckIntervalMs?; private readonly maxSpawnDepth?; private preloadScriptPath?; private coverageFilePath?; private sanitizedBunfigPath?; private sanitizedBunfigCwd?; private tempDir?; private cachedTestNames?; private baseNameIndex?; private testNameIndex?; private cachedTestFiles?; private cachedTestFilesCwd?; private cachedEagerModules?; private cachedEagerModulesCwd?; private lastRegistryTmpPath?; /** * AbortController for whichever `dryRun`/`mutantRun` child process is * currently in flight, if any. Lets {@link dispose} kill an orphaned child * if Stryker disposes this runner while a run hasn't finished — see README * "Orphan prevention". */ private currentAbortController?; constructor(logger: Logger, options: StrykerOptions); /** * Single source of truth for the registry file name. * * Lives in the OS temp directory, not cwd: under Stryker's --inPlace mode * cwd IS the user's real project root, and writing there would both * pollute the user's project and never get cleaned up (dispose() * deliberately never unlinks the registry itself — see its doc comment). * The temp directory is OS-managed instead. * * Keyed by sha256(cwd + ':' + ppid): every worker process Stryker spawns * for one run is a direct child of that run's single Stryker main * process, so all of them share BOTH process.cwd() (the sandbox * directory) AND process.ppid (the main process's pid) — and therefore * independently derive this SAME path with no coordination required. A * worker recycled mid-run is still a child of the same main process, so * it re-derives the same path too (README documents recycled instances * lazily loading the registry). This is the entire sharing contract: * writer (buildAndPersistTestRegistry) and reader (loadRegistryFile) both * go through this one getter rather than reimplementing the formula, so * they can never disagree by construction. * * process.ppid is what makes this safe for this repo's own dogfooding: * this plugin's own unit/integration tests run INSIDE a Stryker sandbox as * the system under test, as children of the sandbox WORKER process, not * of the Stryker main process — so those inner test processes hash a * different ppid and land on a different file, and can never clobber the * outer run's registry. A worker-unique key (e.g. the STRYKER_MUTATOR_WORKER * env var Stryker injects per forked worker) was considered and rejected * for the same reason it would break the cross-worker sharing above: it * uniquely identifies each worker, not the run. * * process.cwd() is read directly (not resolved via fs.realpathSync): * Stryker's child-process-proxy-worker computes every worker's chdir * target via a pure path.resolve() of an identical IPC string, so all * workers of a run already get a byte-identical process.cwd() with no * symlink divergence to guard against — realpath would only add a * syscall (and a new ENOENT failure mode if the sandbox dir is mid- * teardown) for no benefit. * * Using a getter that reads process.cwd()/process.ppid at call time * (rather than caching at construction) ensures the path resolves against * Stryker's sandbox directory — which is set by the time these are * invoked — rather than the orchestrator's cwd at module-load time. */ private get registryPath(); private get registryTmpPath(); /** * Narrow a targeted mutant run to just the test FILES its covering tests live * in. * * `--test-name-pattern` filters at the TEST level, not the file level: bun * still loads every test file it was given in order to discover which names * match. On a suite of 31 spec files that costs ~330ms per run of which only * ~54ms is process startup — and a run that ends up executing two tests pays * it in full, on every mutant. Handing bun only the files that can contain a * covering test drops the same measurement to ~66ms. * * Test ids are ` > ` (see buildProjectFileTestName), so * the file is already the id's prefix and no extra bookkeeping is needed. * * Returns undefined — meaning "use the full discovered list" — whenever the * narrowing cannot be proven safe: * - no filter at all (static mutants, full-suite runs), * - any id that carries no parsable file prefix (console-fallback ids), or * - any derived file that is not in the discovered list, which would mean the * prefix was something other than a real test file. * * A wrong narrowing would silently skip a covering test and turn a killed * mutant into a survivor, so every uncertain case takes the full list. */ private resolveCoveringTestFiles; /** * Get test runner capabilities */ capabilities(): TestRunnerCapabilities; /** * Return the test file list to use for this run. * When `bun.testFiles` was provided it is returned verbatim and * auto-discovery is skipped entirely. Otherwise the result is cached after * the first real discovery call so that subsequent callers (dryRun, mutantRun) * do not re-glob the filesystem. */ private getOrDiscoverTestFiles; /** * Test-file cache hit for a given cwd. * Returns the cached list synchronously when available, or undefined to * signal that async re-discovery is needed (cwd changed or first call). * Used by dryRun() to avoid introducing a microtask yield on the hot path. */ private testFilesCacheHit; /** * Initialize the test runner */ init(): Promise; /** * Regenerate the sanitized bunfig if the worker's cwd has changed (or if this * is the first spawn). Bun resolves relative paths in a bunfig against the * bunfig file's location, so keying on cwd ensures preload/root paths land in * the right sandbox. */ private ensureSanitizedBunfig; /** * Load the shared dryRun registry written by the one worker that ran dryRun. * Populates this.cachedTestNames, this.baseNameIndex, and this.testNameIndex * so that subsequent mutantRun calls on this worker can resolve killedBy names * and build exact --test-name-pattern alternatives, even for static-coverage * mutants where testFilter is empty. * * Loading is all-or-nothing: a wrong version or a malformed field rejects the * whole file (never a half-initialised registry). Failures are non-fatal — the * worker falls back to raw console names and the lossy pattern reconstruction, * and a warning is logged so the issue is visible. */ private loadRegistryFile; /** * Build the failureMessage for a single failed test in the Complete-path result. * * Base message prefers the parsed-console failure message, then the inspector's * error.message, then falls back to a generic string. Stryker core's * logFailedTestsInInitialRun prints exactly name+failureMessage for each failed * test in the ConfigError initial-run path, so the inspector's error.stack (when * present and not already part of the base message) is appended — that's what * makes that path actionable instead of a bare one-liner. * * NOTE this is a genuine, intentional change to the content of the returned * DryRunResult (a failed test's failureMessage is longer / carries a stack it * didn't before) — not a diagnostic-only, log-line-only change like the two * warn helpers below. Test statuses, ids, coverage, and mutantRun behavior * are all unaffected; only failureMessage content is enriched. */ private buildFailureMessage; /** * Build test results from inspector data. * * Also returns a testNameIndex mapping each FINAL test id (dedup ' [N]' * suffix included) to Bun's exact internal matching name (TestInfo.bunName) * for --test-name-pattern generation. Tests without a bunName — console- * fallback results and unknown-inspectorId placeholders — get no index entry * and stay on the lossy pattern reconstruction path (never worse than before). * * @param inspectorIdToProjectFile - Optional mapping from inspector ID to project file path. * When provided, the project file is used for TestResult.id, name, and fileName instead of * testInfo.url. This is important for tests defined via helpers (e.g. RuleTester.run()) where * Bun's inspector reports a url pointing to node_modules rather than the user's test file. */ private buildTestsFromInspector; /** * Run all tests (dry run) */ dryRun(): Promise; /** * Post-child-exit dry-run pipeline: collect/remap coverage, build tests * from inspector data, sort them for incremental-mode determinism, run the * completeness gate, and — only if the gate passes — persist the test * registry. Returns the gate's Error result when it fires, otherwise the * Complete result. * * Extracted from dryRun() purely to keep dryRun()'s own cyclomatic * complexity under the lint threshold; this is a pure code move — same call * order and same early-return-on-gate-failure semantics as before. */ private buildGatedDryRunResult; /** * Build the in-memory test name cache and base-name index, then atomically * persist them — together with the exact-name testNameIndex from * buildTestsFromInspector — to a well-known file so other worker processes * can lazy-load them when handling static-coverage mutants (testFilter is * empty for those) and build exact --test-name-pattern alternatives. * * Writing to a .tmp path then renaming is atomic on POSIX: readers always see * either the previous complete file or the new one — never a partial write. */ private buildAndPersistTestRegistry; /** * Lossy-visibility warns for a mutant run's --test-name-pattern. * * Bun exits 0 on a PARTIAL pattern miss (verified live, bun 1.3.14), * silently dropping the missed tests — so these warns are the only signal * that some covering-test alternatives were built via the lossy * ' > '-collapsing reconstruction instead of exact bun names. Must be called * AFTER the registry lazy-load so this.testNameIndex reflects reality. */ private warnLossyPatternAlternatives; /** * Run tests with an active mutant */ mutantRun(options: MutantRunOptions): Promise; /** * Spawn bun for a mutant run and interpret the result. Extracted from * mutantRun so that a --test-name-pattern which matched 0 tests can retry * once with the full suite (testNamePattern undefined) while reusing the * SAME localRegistry/localBaseIndex built from the original testFilter for * killedBy resolution — avoiding both a false Killed and redundant setup. * The retry always recurses with testNamePattern undefined, so the retry * gate's first conjunct is false on that call: recursion is bounded to * depth 1 by construction, never a retry-of-a-retry. */ private executeMutantRun; /** * Build the MutantRunResult for a killed mutant (non-zero exit code). * Handles runtime error detection and killedBy resolution. */ private buildMutantKilledResult; /** * Check if the process failed due to a runtime error (no tests ran). * Returns a MutantRunResult if this is a runtime error, or null to continue. */ private checkRuntimeError; /** * Check for dry run process failures (timeout or non-zero exit). * Returns a DryRunResult to short-circuit if the process failed, or null to proceed. */ private checkDryRunProcessResult; /** * Build a structured summary of failed tests observed via the inspector, for * inclusion in checkDryRunProcessResult's error message. Bun's own stdout/stderr * recap can be truncated or entirely empty on process failure, so this pulls * failure detail straight from the inspector's TestReporter data instead — * the same data buildTestsFromInspector would otherwise turn into per-test * results, had the process not short-circuited into the error branch first. * * Returns '' when there are no failed tests in the hierarchy so callers can * keep the plain exit-code+stderr message unchanged in that case. Only * type === 'test' entries are considered — describe blocks can also carry * status 'fail' (propagated from a failing child) and must not masquerade * as failed tests here. */ private formatInspectorFailureDetails; /** * Diagnostic-only check for the Complete dry-run path: bun reported a failure * (non-zero exit, or a failed count in its console recap) but nothing in the * built test results identifies which test failed. This is the observed * incident fingerprint — bun prints e.g. "1 tests failed:" with an empty * recap, the inspector shows no TestStatus.Failed entry, and nothing points * at a culprit. An unhandled error firing between tests (e.g. a rejected * fire-and-forget promise) rather than inside any single test body is a * likely cause. Logs a warning only; this check itself never alters test * statuses, ids, coverage, or the returned DryRunResult — contrast with * buildFailureMessage above, which (elsewhere in this same dry-run path) * DOES intentionally change a failed test's failureMessage content. */ private warnOnUnidentifiedDryRunFailure; /** * Dry-run data-completeness gate. * * Guards against the inspector event stream silently truncating mid-run * (observed under CI runner contention): bun's own child-side coverage file * can be complete while the inspector-derived `executionOrder` is cut off at * a file boundary, and everything downstream of that (test results, coverage * attribution) accepted the truncated data silently — a plausible-looking * but corrupted score, not a loud failure. * * ONLY evaluated when the run otherwise looks GREEN (zero Failed entries in * the already-built `tests`): an already-failing dry run is handled by * {@link checkDryRunProcessResult} / {@link warnOnUnidentifiedDryRunFailure}, * and this precondition structurally prevents a failing beforeAll (which * marks the rest of its describe/file Failed) from ever reaching Signal A — * the incident's own signature was a run that looked completely healthy. * * Fires (returns a DryRunStatus.Error result) iff EITHER signal is material: * * - Signal A (execution/console shortfall): `consoleTotal` (bun's SUMMARY * pass+fail counts — deliberately NOT the parser's max(per-line, summary) * fields, so per-ATTEMPT retry output can never inflate it — see * ParsedTestResults.summaryPassed/summaryFailed) compared against the * count of ids in `executionOrder` whose status is neither 'skip' nor * bun's not-yet-implemented placeholder-test status. Retries do not * create a shortfall in this direction: empirically (bun 1.3.14), a * retried test fires ONE TestReporter.start per ATTEMPT for the SAME * inspector id — so executionOrder, if anything, grows on retries, and * bun's summary line counts tests, not attempts, either way. A material * shortfall must clear both an absolute floor and a ratio floor. * * - Signal B (orphaned coverage keys): `orphanedKeyCount` (a whole file's * coverage keys unpaired with any inspector test — see coverage-mapper.ts) * exceeding a small absolute floor. Deliberately gate-blind for the legacy * ("test-N") coverage format, which never populates this count — Signal A * still covers that path since it does not depend on coverage format. * * `wasClosedUnexpectedly` is NEVER a standalone trigger — the WS-vs-child-exit * close race can plausibly be true on a nontrivial fraction of healthy runs — * it is folded into the Error message only as corroborating context once the * gate has already fired via Signal A or B. * * @returns A DryRunStatus.Error result if the gate fires, otherwise null. */ private checkCompletenessGate; /** * Collect coverage from the coverage file and remap counter-based IDs to * full test names using the inspector's execution order. * * Also collects any cross-test async coverage-bleed observations recorded * by the preload (see {@link emitCoverageBleedWarnings}) and warns about * them. This method's sole call site is dryRun, so bleed detection is * dry-run-only by construction — mutant runs never reach this code. */ private collectAndRemapCoverage; /** * Warn about cross-test async coverage bleed: mutant coverage that was * recorded in the gap between one test's afterEach and the next test's * beforeEach, most likely because a fire-and-forget promise chain from the * ended test kept running past the test boundary. * * Diagnostic only: never alters dryRun/mutantRun results or coverage * attribution (stabilizeCoverage's "static wins" rule already governs * attribution independently of this warning). * * A known benign trigger this deliberately does NOT try to suppress: a * same-file describe-level beforeAll running in the gap looks identical to * genuine bleed from this vantage point (both execute while * currentTestId is undefined, between two tests). Filtering it out would * require tracking beforeAll boundaries explicitly, which is out of scope * here — the warning text calls the false-positive out instead. * * Capped at {@link MAX_COVERAGE_BLEED_WARNINGS} individual warnings, plus a * final summary line for the rest, so a suite with many leaking tests * doesn't flood the log. */ private emitCoverageBleedWarnings; /** * Build local index structures from testFilter for killedBy resolution. * * testFilter carries the full registry IDs Stryker wants us to run, including * any " [N]" dedup suffixes. Building the index here means all workers behave * identically on the first shot, eliminating incremental drift caused by workers * that never ran dryRun falling through to raw names. */ private buildLocalTestFilterIndex; /** * Resolve raw failed test names from console output against the test registry. * * Console-parser output lacks the [N] dedup suffix that dryRun appends when * multiple tests share the same base name (e.g. it.each with %s). * * Fallback chain — stops at the FIRST successful resolution for each name: * 1. Exact match in localRegistry (built from testFilter) * 2. Base-name match in localBaseIndex (built from testFilter) * 3. Exact match in this.cachedTestNames (instance registry from dryRun) * 4. Base-name match in this.baseNameIndex (instance registry from dryRun) * * Names resolving through none of these are DROPPED with a single WARN — never * emitted raw. Every entry in a Killed result's killedBy must be exactly a * dry-run TestResult.id: Stryker core's remapTestId (`id => testIdMap.get(id) * ?? id`) writes anything else verbatim into the incremental report, where it * orphans against the test registry and the differ silently re-runs the mutant * on every incremental pass, forever (and the next accumulation pass launders * the orphan into an empty killedBy). Dropping instead degrades that one * mutant to correct-but-non-reusable — same re-run cost, but loudly visible * and never poisonous. No guessed recovery is attempted: bare console names * can collide across files, and --test-name-pattern leakage (see mutantRun) * means the killer may not even be a testFilter member, so any inference * risks crediting the wrong test and enabling stale cache reuse. */ private resolveKilledBy; /** * WARN about failed test names that could not be resolved to dry-run test * ids (and are therefore dropped from killedBy by resolveKilledBy). WARN, * not debug — the old debug-level log is why the resulting cache poisoning * was invisible in CI. No-op when everything resolved. */ private warnUnresolvedKilledBy; /** * Cleanup resources */ dispose(): Promise; } /** * Configuration options specific to the Bun test runner */ export interface BunTestRunnerOptions { /** * Custom path to the bun binary * @default 'bun' */ bunPath?: string; /** * Child-process timeout in milliseconds — the maximum wall-clock time that the * entire `bun test` subprocess is allowed to run before it is killed. * @default 10000 * * Note: this is distinct from Bun's per-test timeout configured via * `[test].timeout` in `bunfig.toml`. The two are independent: `bunfig.toml` * controls when Bun itself declares a single test timed out; this option * controls when the Stryker runner forcibly kills the whole child process. */ timeout?: number; /** * Timeout for inspector connection in ms * @default 5000 */ inspectorTimeout?: number; /** * Additional environment variables to pass to bun test */ env?: Record; /** * Additional bun test flags to pass. * * Bail flags (`--bail`, `--bail=`, or a space-separated `--bail `) are * stripped if present — bail is fully managed by the runner based on * Stryker's `disableBail` option, so a bail flag configured here would * otherwise silently override that decision. * * @example ['--only', '--verbose'] */ bunArgs?: string[]; /** * Explicit list of test file paths (must be non-empty when provided). * When provided, BunTestRunner skips auto-discovery (which globs * `**\/*.test.ts` from the current working directory) and uses exactly this * list. Useful for restricting mutation testing to a subset, or for callers * that run outside Stryker's sandboxed cwd and need to point at a specific * file set. Relative paths resolve against the bun subprocess's cwd. * * IMPORTANT: In a Stryker mutation-testing run, each worker's cwd is set to * a sandbox directory (`.stryker-tmp/sandbox-XYZ/`) containing sandbox copies * of the project files. Relative paths are resolved against that sandbox cwd * and therefore point at the mutated copies. Absolute paths bypass the sandbox * and always point at the ORIGINAL (unmutated) files — mutations will be * silently ignored. Always prefer relative paths in Stryker context. * * An empty array (`[]`) is invalid — use `undefined` or omit the option to * fall back to auto-discovery. */ testFiles?: string[]; /** * Pass Bun's `--smol` flag to every `bun test` child, trading some speed for * a significantly smaller JavaScriptCore heap footprint. Recommended on * memory-constrained machines, especially at higher Stryker `concurrency`, * since peak memory during a campaign is roughly * `concurrency × per-run suite footprint` (each run is an isolated process * that exits when it completes — see README "Memory model"). * @default false */ smol?: boolean; /** * Soft memory ceiling, in bytes, for each `bun test` child's resident set * size (RSS). When set, the child's RSS is polled periodically; a run that * exceeds this ceiling is killed and reported as a clean timeout/error for * that one mutant, rather than being left to grow toward system-wide swap * exhaustion. This is a polled userspace check, not a kernel-enforced * limit — see README "Memory containment" for why a true hard ceiling * (rlimit/cgroup) isn't used. Omit to disable. */ maxChildRss?: number; /** * Poll interval in milliseconds for the {@link maxChildRss} check. * @default 1000 */ rssCheckIntervalMs?: number; /** * Maximum `bun test` spawn nesting depth before the runner refuses to spawn. * * The runner can spawn `bun test` from inside a `bun test` it already * spawned. If a nested run falls back to auto-discovery while its cwd is the * project root, it picks up the project's entire suite — including the test * that spawned it — and the nesting never terminates. This ceiling makes that * finite: a run at or beyond the limit fails with a non-zero exit code * instead of spawning. See the README's "Recursion containment" section. * * The default of 1 allows the runner's own `bun test` children and nothing * deeper, which is right for every project whose tests do not themselves * drive this runner. Raise it to 2 only if yours do — note that the value * must be set on the runner instance making the *nested* call, since that is * where the ceiling is enforced. * * @default 1 */ maxSpawnDepth?: number; } /** * Extended Stryker options with Bun-specific configuration */ export interface StrykerBunOptions extends StrykerOptions { /** * Bun test runner specific configuration */ bun?: BunTestRunnerOptions; } /** * Stryker plugin declarations */ export declare const strykerPlugins: import("@stryker-mutator/api/plugin").ClassPlugin[]; /** * JSON Schema validation for plugin options */ export declare const strykerValidationSchema: { $schema: string; properties: { bun: { title: string; description: string; type: string; properties: { bunPath: { type: string; description: string; default: string; }; timeout: { type: string; minimum: number; description: string; default: number; }; inspectorTimeout: { type: string; minimum: number; description: string; default: number; }; env: { type: string; description: string; additionalProperties: { type: string; }; }; bunArgs: { type: string; description: string; items: { type: string; }; }; testFiles: { type: string; description: string; minItems: number; items: { type: string; }; }; smol: { type: string; description: string; default: boolean; }; maxChildRss: { type: string; minimum: number; description: string; }; rssCheckIntervalMs: { type: string; minimum: number; description: string; default: number; }; maxSpawnDepth: { type: string; minimum: number; description: string; default: number; }; }; additionalProperties: boolean; }; }; }; export {};