@systemfsoftware/stryker-js-vitest-runner 1.0.0 → 2.0.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/dist/index.mjs CHANGED
@@ -1,20 +1,31 @@
1
- import { PluginKind, RunConfiguration, SandboxDirectory, declarePlugin } from "@systemfsoftware/stryker-js-plugin-api/plugin";
1
+ import { RunConfiguration, SandboxDirectory, declarePlugin } from "@systemfsoftware/stryker-js/Plugin";
2
2
  import * as Effect$1 from "effect/Effect";
3
3
  import * as Layer from "effect/Layer";
4
4
  import * as S from "effect/Schema";
5
- import { Effect, Option } from "effect";
6
- import { INSTRUMENTER_CONSTANTS, errorToString, normalizeFileName } from "@systemfsoftware/stryker-js-plugin-api/core";
7
- import { DryRunStatus, TestRunner, TestRunnerFailed, TestStatus, determineHitLimitReached, testFilesProvided, toMutantRunResult } from "@systemfsoftware/stryker-js-plugin-api/test-runner";
8
- import * as FileSystem from "effect/FileSystem";
9
- import * as Option$1 from "effect/Option";
10
- import * as Path from "effect/Path";
11
- import * as Ref from "effect/Ref";
12
- import { fileURLToPath } from "url";
13
5
  import { createRequire } from "module";
14
6
  import { pathToFileURL } from "node:url";
15
7
  import path from "path";
8
+ import { fileURLToPath } from "url";
16
9
  import { createVitest } from "vitest/node";
17
- //#region src/vitest-runner-options.schema.ts
10
+ import { Cell, Workflow } from "@systemfsoftware/effect-cell-types";
11
+ import { INSTRUMENTER_CONSTANTS, errorToString, normalizeFileName } from "@systemfsoftware/stryker-js/Mutant";
12
+ import { TestRunner, TestRunnerFailed, testFilesProvided } from "@systemfsoftware/stryker-js/TestRunner";
13
+ import * as FileSystem from "effect/FileSystem";
14
+ import { pipe } from "effect/Function";
15
+ import * as Option from "effect/Option";
16
+ import * as Path from "effect/Path";
17
+ import * as Ref from "effect/Ref";
18
+ import * as Result from "effect/Result";
19
+ import { Effect } from "effect";
20
+ import * as Match from "effect/Match";
21
+ //#region src/Runner.schema.ts
22
+ /**
23
+ * Runner schemas — declarations for the vitest runner capability.
24
+ *
25
+ * Houses every Schema/Wire type this capability publishes or decodes
26
+ * internally: option validation, coverage/task metadata, sandbox
27
+ * manifest, and the dynamic vitest module shape.
28
+ */
18
29
  /**
19
30
  * The `vitest` option section of the Stryker options document. `related`
20
31
  * defaults to `true` at decode, the other members are optional.
@@ -29,24 +40,527 @@ const VitestRunnerOptionsSchema = S.Struct({
29
40
  * document, or present as a partial, and decoded into the section defaults.
30
41
  */
31
42
  const VitestSectionSchema = S.optional(VitestRunnerOptionsSchema).pipe(S.withDecodingDefault(Effect.succeed({ related: true })));
32
- //#endregion
33
- //#region src/sandbox-self-aliases.schema.ts
43
+ const HitCountMetaSchema = S.Struct({ hitCount: S.optional(S.Finite) });
44
+ const MutantCoverageMetaSchema = S.Struct({ mutantCoverage: S.optional(S.Struct({
45
+ static: S.Record(S.String, S.Finite),
46
+ perTest: S.Record(S.String, S.Record(S.String, S.Finite))
47
+ })) });
48
+ const MutantCoverageShapeSchema = S.Struct({
49
+ static: S.Record(S.String, S.Finite),
50
+ perTest: S.Record(S.String, S.Record(S.String, S.Finite))
51
+ });
52
+ var CoverageDecodeFailed = class extends S.TaggedError()("CoverageDecodeFailed", { cause: S.Unknown }) {};
34
53
  const ExportEntry = S.Union([S.String, S.Record(S.String, S.Unknown)]);
35
54
  const PackageManifest = S.StructWithRest(S.Struct({
36
55
  name: S.optional(S.String),
37
56
  exports: S.optional(S.Record(S.String, ExportEntry))
38
57
  }), [S.Record(S.String, S.Unknown)]);
58
+ /**
59
+ * The dynamically imported project-local `vitest/node` module. The runtime
60
+ * check only asserts object-likeness: the module namespace is whatever the
61
+ * resolved package exports, and the consumers tolerate a missing
62
+ * `createVitest` via their own fallbacks.
63
+ */
64
+ const VitestNodeModuleSchema = S.declare((input) => input !== null && typeof input === "object" && !Array.isArray(input), { description: "The project-local vitest/node module" });
65
+ /** The `package.json` document of a resolved vitest package. */
66
+ const VitestPackageSchema = S.Struct({ version: S.String });
67
+ //#endregion
68
+ //#region src/VitestDryRun.workflow.ts
69
+ /**
70
+ * VitestDryRun workflow — pure result-mapping for the dry-run phase.
71
+ *
72
+ * Every function here is pure: it maps raw vitest task payloads to
73
+ * Stryker run results without touching the filesystem or spawning
74
+ * processes. Impure orchestration lives in `Runner.ts`.
75
+ */
76
+ var VitestDryRunCommand$1 = class extends S.TaggedClass()("VitestDryRunCommand", {
77
+ rawTests: S.Array(S.Unknown),
78
+ projectRoot: S.String,
79
+ hasExternalError: S.Boolean,
80
+ externalErrorText: S.String
81
+ }) {};
82
+ var VitestDryRunOutput$1 = class extends S.TaggedClass()("VitestDryRunOutput", {
83
+ status: S.Literals(["Complete", "Error"]),
84
+ testsJson: S.String,
85
+ errorMessage: S.optional(S.String)
86
+ }) {};
87
+ S.TaggedError()("VitestDryRunError", { message: S.String });
88
+ const recordOption$1 = (value) => S.decodeUnknownOption(S.Record(S.String, S.Unknown))(value);
89
+ const getStringField$1 = (record, key) => Option.fromNullishOr(record[key]).pipe(Option.filter((v) => typeof v === "string"));
90
+ const getNumberField$1 = (record, key) => Option.fromNullishOr(record[key]).pipe(Option.filter((v) => typeof v === "number"));
91
+ const getSuite$1 = (value) => recordOption$1(value).pipe(Option.flatMap((rec) => Option.fromNullishOr(rec["suite"])));
92
+ const getFile$1 = (value) => recordOption$1(value).pipe(Option.flatMap((rec) => Option.fromNullishOr(rec["file"])));
93
+ const getResult$1 = (value) => recordOption$1(value).pipe(Option.flatMap((rec) => Option.fromNullishOr(rec["result"])));
94
+ const getErrors$1 = (value) => recordOption$1(value).pipe(Option.flatMap((rec) => Option.fromNullishOr(rec["errors"])), Option.filter((v) => Array.isArray(v)));
95
+ const getMessage$1 = (value) => recordOption$1(value).pipe(Option.flatMap((rec) => Option.fromNullishOr(rec["message"])), Option.filter((v) => typeof v === "string"));
96
+ const getName$1 = (value) => Option.match(recordOption$1(value), {
97
+ onNone: () => "",
98
+ onSome: (rec) => Option.getOrElse(getStringField$1(rec, "name"), () => "")
99
+ });
100
+ const getMode$1 = (value) => Option.match(recordOption$1(value), {
101
+ onNone: () => "run",
102
+ onSome: (rec) => Option.getOrElse(getStringField$1(rec, "mode"), () => "run")
103
+ });
104
+ const getState$1 = (value) => Match.value(value).pipe(Match.when("pass", () => "pass"), Match.when("fail", () => "fail"), Match.when("skip", () => "skip"), Match.when("todo", () => "todo"), Match.when("run", () => "run"), Match.when("queued", () => "queued"), Match.when("only", () => "only"), Match.when(void 0, () => void 0), Match.orElse(() => void 0));
105
+ const getDuration$1 = (value) => Option.match(recordOption$1(value), {
106
+ onNone: () => 0,
107
+ onSome: (rec) => Option.getOrElse(getNumberField$1(rec, "duration"), () => 0)
108
+ });
109
+ const getFilepath$1 = (value) => Option.match(recordOption$1(value), {
110
+ onNone: () => void 0,
111
+ onSome: (rec) => Option.getOrUndefined(Option.fromNullishOr(rec["filepath"]).pipe(Option.filter((v) => typeof v === "string")))
112
+ });
113
+ const collectSuiteNames$1 = (suite) => Option.match(Option.fromNullishOr(suite), {
114
+ onNone: () => [],
115
+ onSome: (current) => Option.match(recordOption$1(current), {
116
+ onNone: () => [],
117
+ onSome: (rec) => {
118
+ const name = Option.getOrElse(getStringField$1(rec, "name"), () => "");
119
+ const hasName = name.length > 0;
120
+ const parentNames = collectSuiteNames$1(rec["suite"]);
121
+ return Match.value(hasName).pipe(Match.when(true, () => [...parentNames, name]), Match.when(false, () => parentNames), Match.exhaustive);
122
+ }
123
+ })
124
+ });
125
+ const collectTestNameRaw$1 = (test) => {
126
+ const name = getName$1(test);
127
+ const suite = Option.getOrUndefined(getSuite$1(test));
128
+ return [...collectSuiteNames$1(suite), name].join(" ").trim();
129
+ };
130
+ const toRawTestIdRaw$1 = (test) => {
131
+ return `${Option.match(getFile$1(test), {
132
+ onNone: () => "unknown.js",
133
+ onSome: (file) => Option.getOrElse(Option.fromNullishOr(getFilepath$1(file)), () => "unknown.js")
134
+ })}#${collectTestNameRaw$1(test)}`;
135
+ };
136
+ /**
137
+ * A test id is `<file>#<test name>`, and the file is reported relative to the
138
+ * project root so an id is stable across machines and sandbox directories.
139
+ * Vitest reports an absolute path, so the root prefix is stripped here rather
140
+ * than resolved — a decision body has no path service and needs none.
141
+ */
142
+ const normalizeTestIdRaw$1 = (id, projectRoot) => {
143
+ const hash = id.indexOf("#");
144
+ if (hash === -1) return id;
145
+ const file = id.slice(0, hash);
146
+ const rest = id.slice(hash + 1);
147
+ return `${(() => {
148
+ if (file.startsWith(projectRoot)) return file.slice(projectRoot.length);
149
+ return file;
150
+ })().replace(/^[/\\]+/, "").replaceAll("\\", "/")}#${rest}`;
151
+ };
152
+ const toTestStatus$1 = (taskState, mode) => Match.value(mode === "skip").pipe(Match.when(true, () => "skipped"), Match.when(false, () => Match.value(taskState).pipe(Match.when("pass", () => "success"), Match.when("fail", () => "failed"), Match.when("skip", () => "skipped"), Match.when("todo", () => "skipped"), Match.when(void 0, () => "failed"), Match.when("queued", () => "failed"), Match.when("run", () => "failed"), Match.when("only", () => "failed"), Match.orElse(() => "failed"))), Match.exhaustive);
153
+ const findSuiteErrorRaw$1 = (suite) => Option.match(Option.fromNullishOr(suite), {
154
+ onNone: () => void 0,
155
+ onSome: (current) => Option.match(recordOption$1(current), {
156
+ onNone: () => void 0,
157
+ onSome: (rec) => {
158
+ const maybeError = Option.flatMap(getResult$1(rec), (result) => Option.flatMap(getErrors$1(result), (errs) => Match.value(errs.length > 0).pipe(Match.when(true, () => Option.flatMap(Option.fromNullishOr(errs[0]), (first) => getMessage$1(first))), Match.when(false, () => Option.none()), Match.exhaustive)));
159
+ return Option.match(maybeError, {
160
+ onNone: () => findSuiteErrorRaw$1(rec["suite"]),
161
+ onSome: (msg) => msg
162
+ });
163
+ }
164
+ })
165
+ });
166
+ const extractStatus$1 = (test) => {
167
+ const result = Option.getOrUndefined(getResult$1(test));
168
+ const mode = getMode$1(test);
169
+ const state = Option.match(Option.fromNullishOr(result), {
170
+ onNone: () => void 0,
171
+ onSome: (r) => Option.match(recordOption$1(r), {
172
+ onNone: () => void 0,
173
+ onSome: (rec) => getState$1(rec["state"])
174
+ })
175
+ });
176
+ return toTestStatus$1(state, mode);
177
+ };
178
+ const extractDuration$1 = (test) => Option.match(getResult$1(test), {
179
+ onNone: () => 0,
180
+ onSome: (result) => Option.match(recordOption$1(result), {
181
+ onNone: () => 0,
182
+ onSome: (rec) => getDuration$1(rec)
183
+ })
184
+ });
185
+ const extractFileName$1 = (test) => Option.match(getFile$1(test), {
186
+ onNone: () => void 0,
187
+ onSome: (file) => getFilepath$1(file)
188
+ });
189
+ const extractRawId$1 = (test, projectRoot) => normalizeTestIdRaw$1(toRawTestIdRaw$1(test), projectRoot);
190
+ const extractName$1 = (test) => collectTestNameRaw$1(test);
191
+ const extractFailureMessage$1 = (test) => Option.match(getResult$1(test), {
192
+ onNone: () => "StrykerJS: Unknown test failure",
193
+ onSome: (result) => Option.match(getErrors$1(result), {
194
+ onNone: () => "StrykerJS: Unknown test failure",
195
+ onSome: (errs) => Match.value(errs.length > 0).pipe(Match.when(true, () => Option.match(Option.fromNullishOr(errs[0]), {
196
+ onNone: () => "StrykerJS: Unknown test failure",
197
+ onSome: (first) => Option.getOrElse(getMessage$1(first), () => "StrykerJS: Unknown test failure")
198
+ })), Match.when(false, () => "StrykerJS: Unknown test failure"), Match.exhaustive)
199
+ })
200
+ });
201
+ const convertTestRaw$1 = (test, projectRoot) => {
202
+ const status = extractStatus$1(test);
203
+ const base = {
204
+ id: extractRawId$1(test, projectRoot),
205
+ name: extractName$1(test),
206
+ timeSpentMs: extractDuration$1(test),
207
+ fileName: extractFileName$1(test),
208
+ status
209
+ };
210
+ return Match.value(status).pipe(Match.when("failed", () => ({
211
+ ...base,
212
+ status,
213
+ failureMessage: extractFailureMessage$1(test)
214
+ })), Match.when("skipped", () => Match.value(findSuiteErrorRaw$1(Option.getOrUndefined(getSuite$1(test)))).pipe(Match.when(Match.defined, (suiteError) => ({
215
+ ...base,
216
+ status: "failed",
217
+ failureMessage: suiteError
218
+ })), Match.orElse(() => ({
219
+ ...base,
220
+ status
221
+ })))), Match.orElse(() => ({
222
+ ...base,
223
+ status
224
+ })));
225
+ };
226
+ const decideVitestDryRun$1 = (command) => {
227
+ const tests = command.rawTests.map((t) => convertTestRaw$1(t, command.projectRoot));
228
+ const hasFailure = tests.some((t) => t.status === "failed");
229
+ return Match.value({
230
+ hasFailure,
231
+ hasExternalError: command.hasExternalError
232
+ }).pipe(Match.when({
233
+ hasFailure: false,
234
+ hasExternalError: true
235
+ }, () => Result.succeed(VitestDryRunOutput$1.make({
236
+ status: "Error",
237
+ testsJson: JSON.stringify(tests),
238
+ errorMessage: `An error occurred outside of a test run: ${command.externalErrorText}`
239
+ }))), Match.orElse(() => Result.succeed(VitestDryRunOutput$1.make({
240
+ status: "Complete",
241
+ testsJson: JSON.stringify(tests),
242
+ errorMessage: void 0
243
+ }))));
244
+ };
245
+ const vitestDryRunWorkflow = Workflow.make(VitestDryRunCommand$1, decideVitestDryRun$1);
39
246
  //#endregion
40
- //#region src/sandbox-self-aliases.ts
247
+ //#region src/VitestMutantRun.workflow.ts
248
+ /**
249
+ * VitestMutantRun workflow — pure result-mapping for the mutant-run phase.
250
+ */
251
+ var VitestMutantRunCommand = class extends S.TaggedClass()("VitestMutantRunCommand", {
252
+ rawTests: S.Array(S.Unknown),
253
+ projectRoot: S.String,
254
+ hasExternalError: S.Boolean,
255
+ externalErrorText: S.String,
256
+ hitCount: S.optional(S.Finite),
257
+ hitLimit: S.optional(S.Finite),
258
+ reportAllKillers: S.Boolean
259
+ }) {};
260
+ var VitestMutantRunOutput = class extends S.TaggedClass()("VitestMutantRunOutput", {
261
+ status: S.Literals([
262
+ "Killed",
263
+ "Survived",
264
+ "Timeout",
265
+ "Error"
266
+ ]),
267
+ testsJson: S.String,
268
+ errorMessage: S.optional(S.String),
269
+ killerIds: S.optional(S.Array(S.String)),
270
+ failureMessage: S.optional(S.String),
271
+ reason: S.optional(S.String)
272
+ }) {};
273
+ var VitestMutantRunError = class extends S.TaggedError()("VitestMutantRunError", { message: S.String }) {};
274
+ var VitestDryRunCommand = class extends S.TaggedClass()("VitestDryRunCommand", {
275
+ rawTests: S.Array(S.Unknown),
276
+ projectRoot: S.String,
277
+ hasExternalError: S.Boolean,
278
+ externalErrorText: S.String
279
+ }) {};
280
+ var VitestDryRunOutput = class extends S.TaggedClass()("VitestDryRunOutput", {
281
+ status: S.Literals(["Complete", "Error"]),
282
+ testsJson: S.String,
283
+ errorMessage: S.optional(S.String)
284
+ }) {};
285
+ const recordOption = (value) => S.decodeUnknownOption(S.Record(S.String, S.Unknown))(value);
286
+ const getStringField = (record, key) => Option.fromNullishOr(record[key]).pipe(Option.filter((v) => typeof v === "string"));
287
+ const getNumberField = (record, key) => Option.fromNullishOr(record[key]).pipe(Option.filter((v) => typeof v === "number"));
288
+ const getSuite = (value) => recordOption(value).pipe(Option.flatMap((rec) => Option.fromNullishOr(rec["suite"])));
289
+ const getFile = (value) => recordOption(value).pipe(Option.flatMap((rec) => Option.fromNullishOr(rec["file"])));
290
+ const getResult = (value) => recordOption(value).pipe(Option.flatMap((rec) => Option.fromNullishOr(rec["result"])));
291
+ const getErrors = (value) => recordOption(value).pipe(Option.flatMap((rec) => Option.fromNullishOr(rec["errors"])), Option.filter((v) => Array.isArray(v)));
292
+ const getMessage = (value) => recordOption(value).pipe(Option.flatMap((rec) => Option.fromNullishOr(rec["message"])), Option.filter((v) => typeof v === "string"));
293
+ const getName = (value) => Option.match(recordOption(value), {
294
+ onNone: () => "",
295
+ onSome: (rec) => Option.getOrElse(getStringField(rec, "name"), () => "")
296
+ });
297
+ const getMode = (value) => Option.match(recordOption(value), {
298
+ onNone: () => "run",
299
+ onSome: (rec) => Option.getOrElse(getStringField(rec, "mode"), () => "run")
300
+ });
301
+ const getState = (value) => Match.value(value).pipe(Match.when("pass", () => "pass"), Match.when("fail", () => "fail"), Match.when("skip", () => "skip"), Match.when("todo", () => "todo"), Match.when("run", () => "run"), Match.when("queued", () => "queued"), Match.when("only", () => "only"), Match.when(void 0, () => void 0), Match.orElse(() => void 0));
302
+ const getDuration = (value) => Option.match(recordOption(value), {
303
+ onNone: () => 0,
304
+ onSome: (rec) => Option.getOrElse(getNumberField(rec, "duration"), () => 0)
305
+ });
306
+ const getFilepath = (value) => Option.match(recordOption(value), {
307
+ onNone: () => void 0,
308
+ onSome: (rec) => Option.getOrUndefined(Option.fromNullishOr(rec["filepath"]).pipe(Option.filter((v) => typeof v === "string")))
309
+ });
310
+ const collectSuiteNames = (suite) => Option.match(Option.fromNullishOr(suite), {
311
+ onNone: () => [],
312
+ onSome: (current) => Option.match(recordOption(current), {
313
+ onNone: () => [],
314
+ onSome: (rec) => {
315
+ const name = Option.getOrElse(getStringField(rec, "name"), () => "");
316
+ const hasName = name.length > 0;
317
+ const parentNames = collectSuiteNames(rec["suite"]);
318
+ return Match.value(hasName).pipe(Match.when(true, () => [...parentNames, name]), Match.when(false, () => parentNames), Match.exhaustive);
319
+ }
320
+ })
321
+ });
322
+ const collectTestNameRaw = (test) => {
323
+ const name = getName(test);
324
+ const suite = Option.getOrUndefined(getSuite(test));
325
+ return [...collectSuiteNames(suite), name].join(" ").trim();
326
+ };
327
+ const toRawTestIdRaw = (test) => {
328
+ return `${Option.match(getFile(test), {
329
+ onNone: () => "unknown.js",
330
+ onSome: (file) => Option.getOrElse(Option.fromNullishOr(getFilepath(file)), () => "unknown.js")
331
+ })}#${collectTestNameRaw(test)}`;
332
+ };
333
+ /**
334
+ * A test id is `<file>#<test name>`, and the file is reported relative to the
335
+ * project root so an id is stable across machines and sandbox directories.
336
+ * Vitest reports an absolute path, so the root prefix is stripped here rather
337
+ * than resolved — a decision body has no path service and needs none.
338
+ */
339
+ const normalizeTestIdRaw = (id, projectRoot) => {
340
+ const hash = id.indexOf("#");
341
+ if (hash === -1) return id;
342
+ const file = id.slice(0, hash);
343
+ const rest = id.slice(hash + 1);
344
+ return `${(() => {
345
+ if (file.startsWith(projectRoot)) return file.slice(projectRoot.length);
346
+ return file;
347
+ })().replace(/^[/\\]+/, "").replaceAll("\\", "/")}#${rest}`;
348
+ };
349
+ const toTestStatus = (taskState, mode) => Match.value(mode === "skip").pipe(Match.when(true, () => "skipped"), Match.when(false, () => Match.value(taskState).pipe(Match.when("pass", () => "success"), Match.when("fail", () => "failed"), Match.when("skip", () => "skipped"), Match.when("todo", () => "skipped"), Match.when(void 0, () => "failed"), Match.when("queued", () => "failed"), Match.when("run", () => "failed"), Match.when("only", () => "failed"), Match.orElse(() => "failed"))), Match.exhaustive);
350
+ const findSuiteErrorRaw = (suite) => Option.match(Option.fromNullishOr(suite), {
351
+ onNone: () => void 0,
352
+ onSome: (current) => Option.match(recordOption(current), {
353
+ onNone: () => void 0,
354
+ onSome: (rec) => {
355
+ const maybeError = Option.flatMap(getResult(rec), (result) => Option.flatMap(getErrors(result), (errs) => Match.value(errs.length > 0).pipe(Match.when(true, () => Option.flatMap(Option.fromNullishOr(errs[0]), (first) => getMessage(first))), Match.when(false, () => Option.none()), Match.exhaustive)));
356
+ return Option.match(maybeError, {
357
+ onNone: () => findSuiteErrorRaw(rec["suite"]),
358
+ onSome: (msg) => msg
359
+ });
360
+ }
361
+ })
362
+ });
363
+ const extractStatus = (test) => {
364
+ const result = Option.getOrUndefined(getResult(test));
365
+ const mode = getMode(test);
366
+ const state = Option.match(Option.fromNullishOr(result), {
367
+ onNone: () => void 0,
368
+ onSome: (r) => Option.match(recordOption(r), {
369
+ onNone: () => void 0,
370
+ onSome: (rec) => getState(rec["state"])
371
+ })
372
+ });
373
+ return toTestStatus(state, mode);
374
+ };
375
+ const extractDuration = (test) => Option.match(getResult(test), {
376
+ onNone: () => 0,
377
+ onSome: (result) => Option.match(recordOption(result), {
378
+ onNone: () => 0,
379
+ onSome: (rec) => getDuration(rec)
380
+ })
381
+ });
382
+ const extractFileName = (test) => Option.match(getFile(test), {
383
+ onNone: () => void 0,
384
+ onSome: (file) => getFilepath(file)
385
+ });
386
+ const extractRawId = (test, projectRoot) => normalizeTestIdRaw(toRawTestIdRaw(test), projectRoot);
387
+ const extractName = (test) => collectTestNameRaw(test);
388
+ const extractFailureMessage = (test) => Option.match(getResult(test), {
389
+ onNone: () => "StrykerJS: Unknown test failure",
390
+ onSome: (result) => Option.match(getErrors(result), {
391
+ onNone: () => "StrykerJS: Unknown test failure",
392
+ onSome: (errs) => Match.value(errs.length > 0).pipe(Match.when(true, () => Option.match(Option.fromNullishOr(errs[0]), {
393
+ onNone: () => "StrykerJS: Unknown test failure",
394
+ onSome: (first) => Option.getOrElse(getMessage(first), () => "StrykerJS: Unknown test failure")
395
+ })), Match.when(false, () => "StrykerJS: Unknown test failure"), Match.exhaustive)
396
+ })
397
+ });
398
+ const convertTestRaw = (test, projectRoot) => {
399
+ const status = extractStatus(test);
400
+ const base = {
401
+ id: extractRawId(test, projectRoot),
402
+ name: extractName(test),
403
+ timeSpentMs: extractDuration(test),
404
+ fileName: extractFileName(test),
405
+ status
406
+ };
407
+ return Match.value(status).pipe(Match.when("failed", () => ({
408
+ ...base,
409
+ status,
410
+ failureMessage: extractFailureMessage(test)
411
+ })), Match.when("skipped", () => Match.value(findSuiteErrorRaw(Option.getOrUndefined(getSuite(test)))).pipe(Match.when(Match.defined, (suiteError) => ({
412
+ ...base,
413
+ status: "failed",
414
+ failureMessage: suiteError
415
+ })), Match.orElse(() => ({
416
+ ...base,
417
+ status
418
+ })))), Match.orElse(() => ({
419
+ ...base,
420
+ status
421
+ })));
422
+ };
423
+ const decideVitestDryRun = (command) => Match.value(command.rawTests.map((t) => convertTestRaw(t, command.projectRoot))).pipe(Match.when((tests) => tests.some((t) => t.status === "failed") === false && command.hasExternalError === true, (tests) => Result.succeed(VitestDryRunOutput.make({
424
+ status: "Error",
425
+ testsJson: JSON.stringify(tests),
426
+ errorMessage: `An error occurred outside of a test run: ${command.externalErrorText}`
427
+ }))), Match.orElse((tests) => Result.succeed(VitestDryRunOutput.make({
428
+ status: "Complete",
429
+ testsJson: JSON.stringify(tests),
430
+ errorMessage: void 0
431
+ }))));
432
+ /**
433
+ * The hit-limit cutoff: `Some` iff both numbers are present AND the count
434
+ * strictly exceeds the limit. The boundary is `>` and not `>=` on purpose —
435
+ * `hitCount === hitLimit` is the last permitted hit, not one too many.
436
+ */
437
+ const hitLimitReason = (hitCount, hitLimit) => {
438
+ if (hitCount === void 0 || hitLimit === void 0) return Option.none();
439
+ if (hitCount > hitLimit) return Option.some(`Hit limit reached (${hitCount}/${hitLimit})`);
440
+ return Option.none();
441
+ };
442
+ const decideVitestMutantRun = (command) => Match.value(hitLimitReason(command.hitCount, command.hitLimit)).pipe(Match.when(Option.isSome, (hit) => Result.succeed(VitestMutantRunOutput.make({
443
+ status: "Timeout",
444
+ testsJson: "[]",
445
+ errorMessage: void 0,
446
+ killerIds: void 0,
447
+ failureMessage: void 0,
448
+ reason: hit.value
449
+ }))), Match.when(Option.isNone, () => {
450
+ const dryOut = decideVitestDryRun(VitestDryRunCommand.make({
451
+ rawTests: command.rawTests,
452
+ projectRoot: command.projectRoot,
453
+ hasExternalError: command.hasExternalError,
454
+ externalErrorText: command.externalErrorText
455
+ }));
456
+ return Match.value(Result.isFailure(dryOut)).pipe(Match.when(true, () => Result.fail(new VitestMutantRunError({ message: "dry run mapping failed" }))), Match.when(false, () => {
457
+ const dry = Result.getOrElse(dryOut, () => VitestDryRunOutput.make({
458
+ status: "Complete",
459
+ testsJson: "[]",
460
+ errorMessage: void 0
461
+ }));
462
+ return Match.value(dry.status === "Error").pipe(Match.when(true, () => Result.succeed(VitestMutantRunOutput.make({
463
+ status: "Error",
464
+ testsJson: "[]",
465
+ errorMessage: dry.errorMessage,
466
+ killerIds: void 0,
467
+ failureMessage: void 0
468
+ }))), Match.when(false, () => {
469
+ const testsOption = Option.liftThrowable((input) => JSON.parse(input))(dry.testsJson).pipe(Option.filter((v) => Array.isArray(v)));
470
+ const killed = Option.getOrElse(testsOption, () => []).filter((t) => t.status === "failed");
471
+ return Match.value(killed.length > 0).pipe(Match.when(true, () => Match.value(command.reportAllKillers).pipe(Match.when(true, () => {
472
+ const firstKiller = Option.fromNullishOr(killed[0]);
473
+ const failureMessage = Option.match(firstKiller, {
474
+ onNone: () => void 0,
475
+ onSome: (k) => k.failureMessage
476
+ });
477
+ return Result.succeed(VitestMutantRunOutput.make({
478
+ status: "Killed",
479
+ testsJson: dry.testsJson,
480
+ errorMessage: void 0,
481
+ killerIds: killed.map((t) => t.id),
482
+ failureMessage
483
+ }));
484
+ }), Match.when(false, () => {
485
+ const first = Option.fromNullishOr(killed[0]);
486
+ const failureMessage = Option.match(first, {
487
+ onNone: () => void 0,
488
+ onSome: (k) => k.failureMessage
489
+ });
490
+ return Match.value(Option.isSome(first)).pipe(Match.when(true, () => Result.succeed(VitestMutantRunOutput.make({
491
+ status: "Killed",
492
+ testsJson: dry.testsJson,
493
+ errorMessage: void 0,
494
+ killerIds: Option.match(first, {
495
+ onNone: () => void 0,
496
+ onSome: (k) => [k.id]
497
+ }),
498
+ failureMessage
499
+ }))), Match.when(false, () => Result.succeed(VitestMutantRunOutput.make({
500
+ status: "Killed",
501
+ testsJson: dry.testsJson,
502
+ errorMessage: void 0,
503
+ killerIds: Option.getOrUndefined(Option.match(first, {
504
+ onNone: () => Option.none(),
505
+ onSome: (k) => Option.some([k.id])
506
+ })),
507
+ failureMessage
508
+ }))), Match.exhaustive);
509
+ }), Match.exhaustive)), Match.when(false, () => Result.succeed(VitestMutantRunOutput.make({
510
+ status: "Survived",
511
+ testsJson: dry.testsJson,
512
+ errorMessage: void 0,
513
+ killerIds: void 0,
514
+ failureMessage: void 0
515
+ }))), Match.exhaustive);
516
+ }), Match.exhaustive);
517
+ }), Match.exhaustive);
518
+ }), Match.exhaustive);
519
+ const vitestMutantRunWorkflow = Workflow.make(VitestMutantRunCommand, decideVitestMutantRun);
520
+ //#endregion
521
+ //#region src/Runner.ts
522
+ function fromTestId(id) {
523
+ const [file, ...name] = id.split("#");
524
+ return {
525
+ file,
526
+ test: name.join("#")
527
+ };
528
+ }
529
+ function normalizeTestId(id, projectRoot, pathService) {
530
+ const { file, test } = fromTestId(id);
531
+ return `${normalizeFileName(pathService.relative(projectRoot, file))}#${test}`;
532
+ }
533
+ function normalizeCoverage(rawCoverage, projectRoot, pathService) {
534
+ return {
535
+ perTest: Object.fromEntries(Object.entries(rawCoverage.perTest).map(([rawTestId, coverageData]) => [normalizeTestId(rawTestId, projectRoot, pathService), coverageData])),
536
+ static: rawCoverage.static
537
+ };
538
+ }
539
+ function collectTestsFromSuite(suite) {
540
+ return suite.tasks.flatMap((task) => {
541
+ if (task.type === "suite") return collectTestsFromSuite(task);
542
+ return task;
543
+ });
544
+ }
545
+ function isErrorCodeError(error) {
546
+ if (error instanceof Error && "code" in error) return typeof Reflect.get(error, "code") === "string";
547
+ return false;
548
+ }
549
+ /** @see https://github.com/vitest-dev/vitest/blob/main/packages/vitest/src/node/errors.ts */
550
+ const VITEST_ERROR_CODES = Object.freeze({ FILES_NOT_FOUND: "VITEST_FILES_NOT_FOUND" });
41
551
  const SOURCE_CONDITION = "@systemfsoftware/source";
42
552
  const sourceTargetOf = (entry) => {
43
- if (typeof entry === "string") return entry.endsWith(".ts") || entry.endsWith(".tsx") || entry.endsWith(".mts") ? entry : void 0;
553
+ if (typeof entry === "string") return (() => {
554
+ if (entry.endsWith(".ts") || entry.endsWith(".tsx") || entry.endsWith(".mts")) return entry;
555
+ })();
44
556
  const source = entry[SOURCE_CONDITION];
45
- return typeof source === "string" ? source : void 0;
557
+ return (() => {
558
+ if (typeof source === "string") return source;
559
+ })();
46
560
  };
47
561
  const specifierForExport = (packageName, exportKey) => {
48
562
  if (exportKey === ".") return packageName;
49
- if (exportKey === "./package.json" || !exportKey.startsWith("./")) return;
563
+ if (exportKey === "./package.json" || !exportKey.startsWith("./")) return void 0;
50
564
  return `${packageName}/${exportKey.slice(2)}`;
51
565
  };
52
566
  const sandboxSelfAliases = (manifest, projectRoot, pathService) => {
@@ -87,117 +601,13 @@ const readSandboxSelfAliases = (projectRoot) => Effect$1.gen(function* () {
87
601
  * zero tests. Returning the sandbox source path from `resolveId` makes the
88
602
  * dep a real filesystem path related-mode can walk.
89
603
  */
90
- const sandboxSelfPlugin = (aliases) => {
91
- return {
92
- name: "stryker-sandbox-self-exports",
93
- enforce: "pre",
94
- resolveId(source) {
95
- for (const alias of aliases) if (alias.find.test(source)) return alias.replacement;
96
- }
97
- };
98
- };
99
- //#endregion
100
- //#region src/test-identity.ts
101
- function collectTestName({ name, suite }) {
102
- const nameParts = [name];
103
- let currentSuite = suite;
104
- while (currentSuite) {
105
- nameParts.unshift(currentSuite.name);
106
- currentSuite = currentSuite.suite;
107
- }
108
- return nameParts.join(" ").trim();
109
- }
110
- function toRawTestId(test) {
111
- return `${test.file?.filepath ?? "unknown.js"}#${collectTestName(test)}`;
112
- }
113
- //#endregion
114
- //#region src/vitest-task-mapping.ts
115
- function convertTaskStateToTestStatus(taskState, testMode) {
116
- if (testMode === "skip") return TestStatus.Skipped;
117
- switch (taskState) {
118
- case "pass": return TestStatus.Success;
119
- case "fail": return TestStatus.Failed;
120
- case "skip":
121
- case "todo": return TestStatus.Skipped;
122
- case void 0:
123
- case "queued":
124
- case "run":
125
- case "only": return TestStatus.Failed;
604
+ const sandboxSelfPlugin = (aliases) => ({
605
+ name: "stryker-sandbox-self-exports",
606
+ enforce: "pre",
607
+ resolveId(source) {
608
+ for (const alias of aliases) if (alias.find.test(source)) return alias.replacement;
126
609
  }
127
- }
128
- function convertTestToTestResult(test, projectRoot, pathService) {
129
- const status = convertTaskStateToTestStatus(test.result?.state, test.mode);
130
- const baseTestResult = {
131
- id: normalizeTestId(toRawTestId(test), projectRoot, pathService),
132
- name: collectTestName(test),
133
- timeSpentMs: test.result?.duration ?? 0,
134
- fileName: test.file?.filepath && pathService.resolve(test.file.filepath)
135
- };
136
- if (status === TestStatus.Failed) return {
137
- ...baseTestResult,
138
- status,
139
- failureMessage: test.result?.errors?.[0]?.message ?? "StrykerJS: Unknown test failure"
140
- };
141
- else if (status === TestStatus.Skipped) {
142
- const suiteError = findSuiteError(test.suite);
143
- if (suiteError) return {
144
- ...baseTestResult,
145
- status: TestStatus.Failed,
146
- failureMessage: suiteError
147
- };
148
- }
149
- return {
150
- ...baseTestResult,
151
- status
152
- };
153
- }
154
- function findSuiteError(suite) {
155
- if (!suite) return;
156
- if (suite.result?.state === "fail") return suite.result?.errors?.[0]?.message ?? "StrykerJS: Suite execution failed";
157
- return findSuiteError(suite.suite);
158
- }
159
- function fromTestId(id) {
160
- const [file, ...name] = id.split("#");
161
- return {
162
- file,
163
- test: name.join("#")
164
- };
165
- }
166
- function normalizeTestId(id, projectRoot, pathService) {
167
- const { file, test } = fromTestId(id);
168
- return `${normalizeFileName(pathService.relative(projectRoot, file))}#${test}`;
169
- }
170
- function normalizeCoverage(rawCoverage, projectRoot, pathService) {
171
- return {
172
- perTest: Object.fromEntries(Object.entries(rawCoverage.perTest).map(([rawTestId, coverageData]) => [normalizeTestId(rawTestId, projectRoot, pathService), coverageData])),
173
- static: rawCoverage.static
174
- };
175
- }
176
- function collectTestsFromSuite(suite) {
177
- return suite.tasks.flatMap((task) => {
178
- if (task.type === "suite") return collectTestsFromSuite(task);
179
- else if (task.type === "test") return task;
180
- else return [];
181
- });
182
- }
183
- function isErrorCodeError(error) {
184
- return error instanceof Error && "code" in error && typeof error.code === "string";
185
- }
186
- /** @see https://github.com/vitest-dev/vitest/blob/main/packages/vitest/src/node/errors.ts */
187
- const VITEST_ERROR_CODES = Object.freeze({ FILES_NOT_FOUND: "VITEST_FILES_NOT_FOUND" });
188
- //#endregion
189
- //#region src/vitest-wrapper.schema.ts
190
- /**
191
- * The dynamically imported project-local `vitest/node` module. The runtime
192
- * check only asserts object-likeness: the module namespace is whatever the
193
- * resolved package exports, and the consumers tolerate a missing
194
- * `createVitest` via their own fallbacks.
195
- */
196
- const VitestNodeModuleSchema = S.declare((input) => input !== null && typeof input === "object" && !Array.isArray(input), { description: "The project-local vitest/node module" });
197
- /** The `package.json` document of a resolved vitest package. */
198
- const VitestPackageSchema = S.Struct({ version: S.String });
199
- //#endregion
200
- //#region src/vitest-wrapper.ts
610
+ });
201
611
  const readVitestVersion = (readPackageJson) => S.decodeUnknownSync(VitestPackageSchema)(readPackageJson()).version;
202
612
  /** Falls back to the Vitest bundled with this package when `dir` has none. */
203
613
  const resolveVitest = async (dir) => {
@@ -206,7 +616,7 @@ const resolveVitest = async (dir) => {
206
616
  const vitestNodePath = projectRequire.resolve("vitest/node");
207
617
  const vitestNodeUrl = pathToFileURL(vitestNodePath).href;
208
618
  return {
209
- createVitest: S.decodeUnknownSync(VitestNodeModuleSchema)(await import(vitestNodeUrl)).createVitest ?? createVitest,
619
+ createVitest: S.decodeUnknownSync(VitestNodeModuleSchema)(await import(vitestNodeUrl)).createVitest,
210
620
  version: readVitestVersion(() => projectRequire(projectRequire.resolve("vitest/package.json")))
211
621
  };
212
622
  } catch {
@@ -217,20 +627,6 @@ const resolveVitest = async (dir) => {
217
627
  };
218
628
  }
219
629
  };
220
- //#endregion
221
- //#region src/vitest-runner-coverage.schema.ts
222
- const HitCountMetaSchema = S.Struct({ hitCount: S.optional(S.Finite) });
223
- const MutantCoverageMetaSchema = S.Struct({ mutantCoverage: S.optional(S.Struct({
224
- static: S.Record(S.String, S.Finite),
225
- perTest: S.Record(S.String, S.Record(S.String, S.Finite))
226
- })) });
227
- const MutantCoverageShapeSchema = S.Struct({
228
- static: S.Record(S.String, S.Finite),
229
- perTest: S.Record(S.String, S.Record(S.String, S.Finite))
230
- });
231
- var CoverageDecodeFailed = class extends S.TaggedError()("CoverageDecodeFailed", { cause: S.Unknown }) {};
232
- //#endregion
233
- //#region src/vitest-test-runner.ts
234
630
  const isRunnerTestSuite = (value) => typeof value === "object" && value !== null && "tasks" in value && Array.isArray(Reflect.get(value, "tasks"));
235
631
  const STRYKER_SETUP = fileURLToPath(new URL("./stryker-setup.mjs", import.meta.url));
236
632
  const shouldUseSuiteMetaSecondArg = (version) => {
@@ -262,7 +658,10 @@ const experimentalStateHasExternalErrors = (vitest) => {
262
658
  if (errorsSet instanceof Set) return errorsSet.size > 0;
263
659
  if (typeof errorsSet === "object" && errorsSet !== null && "size" in errorsSet) {
264
660
  const size = Reflect.get(errorsSet, "size");
265
- return typeof size === "number" ? size > 0 : false;
661
+ return (() => {
662
+ if (typeof size === "number") return size > 0;
663
+ return false;
664
+ })();
266
665
  }
267
666
  }
268
667
  }
@@ -289,7 +688,10 @@ const applySetupFilesToProjects = (vitest, localSetupFile) => {
289
688
  if (typeof browser === "object" && browser !== null) Reflect.set(browser, "screenshotFailures", false);
290
689
  for (const project of vitest.projects) {
291
690
  const setupFilesRaw = Reflect.get(project.config, "setupFiles");
292
- const files = Array.isArray(setupFilesRaw) ? setupFilesRaw.filter((x) => typeof x === "string") : [];
691
+ const files = (() => {
692
+ if (Array.isArray(setupFilesRaw)) return setupFilesRaw.filter((x) => typeof x === "string");
693
+ return [];
694
+ })();
293
695
  Reflect.set(project.config, "setupFiles", [localSetupFile, ...files]);
294
696
  const pBrowser = Reflect.get(project.config, "browser");
295
697
  if (typeof pBrowser === "object" && pBrowser !== null) Reflect.set(pBrowser, "screenshotFailures", false);
@@ -308,14 +710,17 @@ const makeVitestRunnerLayer = (input) => Layer.effect(TestRunner, Effect$1.gen(f
308
710
  if (state.ctx === void 0) return yield* new TestRunnerFailed({
309
711
  runnerName: "vitest",
310
712
  phase: "dryRun",
311
- cause: /* @__PURE__ */ new Error("Vitest runner is not initialized; call init() before running tests")
713
+ cause: errorToString(/* @__PURE__ */ new Error("Vitest runner is not initialized; call init() before running tests"))
312
714
  });
313
715
  return state.ctx;
314
716
  });
315
- const decodedOptionsEffect = (raw) => S.decodeUnknownEffect(VitestSectionSchema)(raw).pipe(Effect$1.map((decoded) => decoded === void 0 ? { related: true } : decoded), Effect$1.mapError((cause) => new TestRunnerFailed({
717
+ const decodedOptionsEffect = (raw) => S.decodeUnknownEffect(VitestSectionSchema)(raw).pipe(Effect$1.map((decoded) => (() => {
718
+ if (decoded === void 0) return { related: true };
719
+ return decoded;
720
+ })()), Effect$1.mapError((cause) => new TestRunnerFailed({
316
721
  runnerName: "vitest",
317
722
  phase: "init",
318
- cause
723
+ cause: errorToString(cause)
319
724
  })));
320
725
  const optionsEffect = decodedOptionsEffect(Reflect.get(input.options, "vitest")).pipe(Effect$1.map((vitestOptions) => ({
321
726
  ...input.options,
@@ -337,7 +742,7 @@ const makeVitestRunnerLayer = (input) => Layer.effect(TestRunner, Effect$1.gen(f
337
742
  yield* fsService.copyFile(input.setupFilePath ?? STRYKER_SETUP, localSetupFile).pipe(Effect$1.mapError((cause) => new TestRunnerFailed({
338
743
  runnerName: "vitest",
339
744
  phase: "init",
340
- cause
745
+ cause: errorToString(cause)
341
746
  })));
342
747
  const resolver = input.resolveVitestFor ?? resolveVitest;
343
748
  const { createVitest, version } = yield* Effect$1.tryPromise({
@@ -345,11 +750,13 @@ const makeVitestRunnerLayer = (input) => Layer.effect(TestRunner, Effect$1.gen(f
345
750
  catch: (cause) => new TestRunnerFailed({
346
751
  runnerName: "vitest",
347
752
  phase: "init",
348
- cause
753
+ cause: errorToString(cause)
349
754
  })
350
755
  });
351
756
  const namespace = input.globalNamespace ?? INSTRUMENTER_CONSTANTS.NAMESPACE;
352
- const scanDir = typeof options.vitest.dir === "string" ? pathService.resolve(projectRoot, options.vitest.dir) : void 0;
757
+ const scanDir = (() => {
758
+ if (typeof options.vitest.dir === "string") return pathService.resolve(projectRoot, options.vitest.dir);
759
+ })();
353
760
  const aliases = yield* readSandboxSelfAliases(projectRoot).pipe(Effect$1.provideService(FileSystem.FileSystem, fsService), Effect$1.provideService(Path.Path, pathService));
354
761
  const plugin = sandboxSelfPlugin(aliases);
355
762
  const ctx = yield* Effect$1.tryPromise({
@@ -360,9 +767,17 @@ const makeVitestRunnerLayer = (input) => Layer.effect(TestRunner, Effect$1.gen(f
360
767
  maxConcurrency: 1,
361
768
  watch: false,
362
769
  root: projectRoot,
363
- ...scanDir === void 0 ? {} : { dir: scanDir },
364
- bail: options.disableBail ? 0 : 1,
365
- onConsoleLog: () => false
770
+ ...(() => {
771
+ if (scanDir === void 0) return {};
772
+ return { dir: scanDir };
773
+ })(),
774
+ bail: (() => {
775
+ if (options.disableBail) return 0;
776
+ return 1;
777
+ })(),
778
+ onConsoleLog: () => false,
779
+ silent: true,
780
+ reporters: [{ onInit(_vitest) {} }]
366
781
  }, {
367
782
  resolve: {
368
783
  alias: [...aliases],
@@ -373,29 +788,31 @@ const makeVitestRunnerLayer = (input) => Layer.effect(TestRunner, Effect$1.gen(f
373
788
  catch: (cause) => new TestRunnerFailed({
374
789
  runnerName: "vitest",
375
790
  phase: "init",
376
- cause
791
+ cause: errorToString(cause)
377
792
  })
378
793
  });
379
794
  ctx.provide("globalNamespace", namespace);
380
795
  ctx.provide("isGreaterThanVitest4Point1", shouldUseSuiteMetaSecondArg(version));
381
796
  applySetupFilesToProjects(ctx, localSetupFile);
382
- yield* Effect$1.logDebug(`vitest final config: ${JSON.stringify(ctx.config, null, 2)}`);
383
797
  yield* Ref.update(stateRef, (s) => ({
384
798
  ...s,
385
799
  ctx
386
800
  }));
387
- }).pipe(Effect$1.mapError((cause) => cause instanceof TestRunnerFailed ? cause : new TestRunnerFailed({
388
- runnerName: "vitest",
389
- phase: "init",
390
- cause
391
- })));
801
+ }).pipe(Effect$1.mapError((cause) => (() => {
802
+ if (cause instanceof TestRunnerFailed) return cause;
803
+ return new TestRunnerFailed({
804
+ runnerName: "vitest",
805
+ phase: "init",
806
+ cause
807
+ });
808
+ })()));
392
809
  const resetContext = Effect$1.gen(function* () {
393
810
  const ctx = yield* requireCtx;
394
811
  experimentalStateClearFiles(ctx);
395
812
  });
396
- const getFileMeta = (file) => {
813
+ const getFileMeta = (file) => (() => {
397
814
  if (file !== null && typeof file === "object" && "meta" in file) return Reflect.get(file, "meta");
398
- };
815
+ })();
399
816
  const readHitCount = Effect$1.gen(function* () {
400
817
  const ctx = yield* requireCtx.pipe(Effect$1.mapError((cause) => new CoverageDecodeFailed({ cause })));
401
818
  const files = experimentalStateGetFiles(ctx);
@@ -412,11 +829,21 @@ const makeVitestRunnerLayer = (input) => Layer.effect(TestRunner, Effect$1.gen(f
412
829
  const files = experimentalStateGetFiles(ctx);
413
830
  const deduped = {};
414
831
  for (const file of files) {
415
- const projectNameValue = typeof file === "object" && file !== null && "projectName" in file ? Reflect.get(file, "projectName") : void 0;
416
- const projectName = typeof projectNameValue === "string" ? projectNameValue : "";
417
- const nameValue = typeof file === "object" && file !== null && "name" in file ? Reflect.get(file, "name") : void 0;
418
- const key = `${projectName}-${typeof nameValue === "string" ? nameValue : ""}`;
419
- deduped[key] = file;
832
+ const projectNameValue = (() => {
833
+ if (typeof file === "object" && file !== null && "projectName" in file) return Reflect.get(file, "projectName");
834
+ })();
835
+ const projectName = (() => {
836
+ if (typeof projectNameValue === "string") return projectNameValue;
837
+ return "";
838
+ })();
839
+ const nameValue = (() => {
840
+ if (typeof file === "object" && file !== null && "name" in file) return Reflect.get(file, "name");
841
+ })();
842
+ const name = (() => {
843
+ if (typeof nameValue === "string") return nameValue;
844
+ return "";
845
+ })();
846
+ deduped[`${projectName}-${name}`] = file;
420
847
  }
421
848
  const coverages = [];
422
849
  for (const file of Object.values(deduped)) {
@@ -428,36 +855,36 @@ const makeVitestRunnerLayer = (input) => Layer.effect(TestRunner, Effect$1.gen(f
428
855
  coverages.push(validated);
429
856
  }
430
857
  }
431
- if (coverages.length === 0) return;
858
+ if (coverages.length === 0) return void 0;
432
859
  if (coverages.length === 1) return coverages[0];
433
860
  const first = coverages[0];
434
- if (first === void 0) return;
435
861
  return coverages.slice(1).reduce((acc, projectCoverage) => {
436
- for (const [testId, testCoverage] of Object.entries(projectCoverage.perTest)) {
437
- const existing = acc.perTest[testId];
438
- if (existing !== void 0) mergeCoverage(existing, testCoverage);
439
- else acc.perTest[testId] = testCoverage;
440
- }
862
+ for (const [testId, testCoverage] of Object.entries(projectCoverage.perTest)) if (testId in acc.perTest) mergeCoverage(acc.perTest[testId], testCoverage);
863
+ else acc.perTest[testId] = testCoverage;
441
864
  mergeCoverage(acc.static, projectCoverage.static);
442
865
  return acc;
443
866
  }, first);
444
867
  });
445
- const run = (filter) => Effect$1.gen(function* () {
868
+ const collectRaw = (filter) => Effect$1.gen(function* () {
446
869
  const ctx = yield* requireCtx;
447
870
  const options = yield* optionsEffect;
448
871
  yield* resetContext.pipe(Effect$1.mapError((cause) => new TestRunnerFailed({
449
872
  runnerName: "vitest",
450
873
  phase: "dryRun",
451
- cause
874
+ cause: errorToString(cause)
452
875
  })));
453
876
  const vitestInRun = Reflect.get(options, "vitest");
454
- const related = Reflect.get(vitestInRun, "related") !== false && filter.relatedFiles !== void 0 ? filter.relatedFiles.map(normalizeFileName) : void 0;
455
- let testFilesToRun = filter.testFiles;
877
+ const relatedValue = Reflect.get(vitestInRun, "related");
878
+ const related = (() => {
879
+ if (relatedValue !== false && filter.relatedFiles !== void 0) return filter.relatedFiles.map(normalizeFileName);
880
+ })();
881
+ let testFilesToRun = (() => {
882
+ if (filter.testFiles !== void 0) return [...filter.testFiles];
883
+ })();
456
884
  let pattern;
457
885
  if ((filter.testIds ?? []).length > 0) {
458
886
  const parsedTests = (filter.testIds ?? []).map(fromTestId);
459
- const regexTestNameFilter = parsedTests.map(({ test: name }) => RegExp.escape(name)).join("|");
460
- pattern = new RegExp(regexTestNameFilter);
887
+ pattern = new RegExp(parsedTests.map(({ test: name }) => RegExp.escape(name)).join("|"));
461
888
  testFilesToRun = parsedTests.map(({ file }) => pathService.resolve(input.sandboxDirectory, file));
462
889
  }
463
890
  applyRunFilterToConfig(ctx, {
@@ -469,77 +896,201 @@ const makeVitestRunnerLayer = (input) => Layer.effect(TestRunner, Effect$1.gen(f
469
896
  catch: (cause) => new TestRunnerFailed({
470
897
  runnerName: "vitest",
471
898
  phase: "dryRun",
472
- cause
899
+ cause: errorToString(cause)
473
900
  })
474
- }).pipe(Effect$1.catchIf((error) => isErrorCodeError(error.cause) && error.cause.code === VITEST_ERROR_CODES.FILES_NOT_FOUND, () => Effect$1.void));
475
- const tests = experimentalStateGetFiles(ctx).flatMap((file) => isRunnerTestSuite(file) ? collectTestsFromSuite(file) : []).filter((test) => test.result !== void 0);
476
- let failure = false;
477
- const testResults = tests.map((test) => {
478
- const testResult = convertTestToTestResult(test, input.sandboxDirectory, pathService);
479
- failure ||= testResult.status === TestStatus.Failed;
480
- return testResult;
481
- });
482
- if (!failure && experimentalStateHasExternalErrors(ctx)) {
483
- const errorText = experimentalStateGetExternalErrorText(ctx);
484
- return {
485
- status: DryRunStatus.Error,
486
- errorMessage: `An error occurred outside of a test run: ${errorText}`
487
- };
488
- }
901
+ }).pipe(Effect$1.catchIf((error) => isErrorCodeError(error.cause) && typeof error.cause === "string" && error.cause.includes(VITEST_ERROR_CODES.FILES_NOT_FOUND), () => Effect$1.void));
902
+ const rawTests = experimentalStateGetFiles(ctx).flatMap((file) => (() => {
903
+ if (isRunnerTestSuite(file)) return collectTestsFromSuite(file);
904
+ return [];
905
+ })()).filter((test) => test.result !== void 0);
906
+ const hasExternalError = experimentalStateHasExternalErrors(ctx);
489
907
  return {
490
- tests: testResults,
491
- status: DryRunStatus.Complete
908
+ rawTests,
909
+ hasExternalError,
910
+ externalErrorText: (() => {
911
+ if (hasExternalError) return experimentalStateGetExternalErrorText(ctx);
912
+ return "";
913
+ })()
492
914
  };
493
915
  });
494
- const dryRun = (options) => Effect$1.gen(function* () {
916
+ const dryRunDescription = pipe(Cell.read((command) => Effect$1.gen(function* () {
495
917
  (yield* requireCtx).provide("mode", "dry-run");
496
- const testResult = testFilesProvided(options) ? yield* run({
497
- testFiles: options.testFiles,
498
- relatedFiles: options.files
499
- }) : yield* run({ relatedFiles: options.files });
500
- if (testResult.status === DryRunStatus.Complete && testResult.tests.length === 0 && (yield* optionsEffect).vitest.related !== false && !options.testFiles) yield* Effect$1.logWarning("Vitest failed to find test files related to mutated files. Either disable `vitest.related` or import your source files directly from your test files. See https://stryker-mutator.io/docs/stryker-js/troubleshooting/#vitest-failed-to-find-test-files-related-to-mutated-files");
501
- if (testResult.status === DryRunStatus.Complete) {
918
+ const hasTestFiles = testFilesProvided(command);
919
+ const filter = (() => {
920
+ if (hasTestFiles) return {
921
+ testFiles: [...command.testFiles ?? []],
922
+ relatedFiles: (() => {
923
+ if (command.files !== void 0) return [...command.files];
924
+ })()
925
+ };
926
+ return { relatedFiles: (() => {
927
+ if (command.files !== void 0) return [...command.files];
928
+ })() };
929
+ })();
930
+ const { rawTests, hasExternalError, externalErrorText } = yield* collectRaw(filter);
931
+ return {
932
+ rawTests,
933
+ projectRoot: input.sandboxDirectory,
934
+ hasExternalError,
935
+ externalErrorText
936
+ };
937
+ })), Cell.decode((raw) => Result.succeed(new VitestDryRunCommand$1({
938
+ rawTests: raw.rawTests,
939
+ projectRoot: raw.projectRoot,
940
+ hasExternalError: raw.hasExternalError,
941
+ externalErrorText: raw.externalErrorText
942
+ }))), Cell.decide(vitestDryRunWorkflow), Cell.encode((outcome) => Result.match(outcome, {
943
+ onFailure: (e) => ({
944
+ status: "error",
945
+ errorMessage: e.message
946
+ }),
947
+ onSuccess: (out) => {
948
+ const raw = JSON.parse(out.testsJson);
949
+ let tests;
950
+ if (Array.isArray(raw)) tests = raw.filter(isTestResultLike);
951
+ else tests = [];
952
+ if (out.status === "Error") return {
953
+ status: "error",
954
+ errorMessage: out.errorMessage ?? "unknown"
955
+ };
956
+ return {
957
+ status: "complete",
958
+ tests
959
+ };
960
+ }
961
+ })), Cell.write((output) => Effect$1.gen(function* () {
962
+ if (output.status === "complete") {
502
963
  const mutantCoverage = yield* readMutantCoverage.pipe(Effect$1.mapError((cause) => new TestRunnerFailed({
503
964
  runnerName: "vitest",
504
965
  phase: "dryRun",
505
- cause
966
+ cause: errorToString(cause)
506
967
  })));
507
- if (mutantCoverage === void 0) return testResult;
508
- return {
509
- ...testResult,
968
+ if (mutantCoverage !== void 0) return {
969
+ ...output,
510
970
  mutantCoverage
511
971
  };
512
972
  }
513
- return testResult;
514
- }).pipe(Effect$1.mapError((cause) => cause instanceof TestRunnerFailed ? cause : new TestRunnerFailed({
515
- runnerName: "vitest",
516
- phase: "dryRun",
517
- cause
973
+ return output;
518
974
  })));
519
- const mutantRun = (options) => Effect$1.gen(function* () {
975
+ const mutantRunDescription = pipe(Cell.read((command) => Effect$1.gen(function* () {
520
976
  const ctx = yield* requireCtx;
521
977
  ctx.provide("mode", "mutant");
522
- ctx.provide("hitLimit", options.hitLimit);
523
- ctx.provide("mutantActivation", options.mutantActivation);
524
- ctx.provide("activeMutant", options.activeMutant.id);
525
- const dryRunResult = yield* run({
526
- testIds: options.testFilter,
527
- relatedFiles: [options.sandboxFileName]
978
+ ctx.provide("hitLimit", command.hitLimit);
979
+ ctx.provide("mutantActivation", command.mutantActivation);
980
+ ctx.provide("activeMutant", command.activeMutant.id);
981
+ const { rawTests, hasExternalError, externalErrorText } = yield* collectRaw({
982
+ testIds: (() => {
983
+ if (command.testFilter !== void 0) return [...command.testFilter];
984
+ })(),
985
+ relatedFiles: [command.sandboxFileName]
528
986
  });
529
987
  const hitCount = yield* readHitCount.pipe(Effect$1.mapError((cause) => new TestRunnerFailed({
530
988
  runnerName: "vitest",
531
989
  phase: "mutantRun",
532
- cause
533
- })));
534
- const timeOut = determineHitLimitReached(hitCount, options.hitLimit);
535
- const effectiveResult = Option$1.isSome(timeOut) ? timeOut.value : dryRunResult;
536
- const reportAllKillers = typeof input.options.disableBail === "boolean" ? input.options.disableBail : false;
537
- return toMutantRunResult(effectiveResult, reportAllKillers);
538
- }).pipe(Effect$1.mapError((cause) => cause instanceof TestRunnerFailed ? cause : new TestRunnerFailed({
539
- runnerName: "vitest",
540
- phase: "mutantRun",
541
- cause
542
- })));
990
+ cause: errorToString(cause)
991
+ })), Effect$1.option, Effect$1.map(Option.getOrUndefined));
992
+ const reportAllKillers = (() => {
993
+ if (typeof input.options.disableBail === "boolean") return input.options.disableBail;
994
+ return false;
995
+ })();
996
+ if (hitCount === void 0) return {
997
+ rawTests,
998
+ projectRoot: input.sandboxDirectory,
999
+ hasExternalError,
1000
+ externalErrorText,
1001
+ hitLimit: command.hitLimit,
1002
+ reportAllKillers
1003
+ };
1004
+ return {
1005
+ rawTests,
1006
+ projectRoot: input.sandboxDirectory,
1007
+ hasExternalError,
1008
+ externalErrorText,
1009
+ hitCount,
1010
+ hitLimit: command.hitLimit,
1011
+ reportAllKillers
1012
+ };
1013
+ })), Cell.decode((raw) => {
1014
+ const base = {
1015
+ rawTests: raw.rawTests,
1016
+ projectRoot: raw.projectRoot,
1017
+ hasExternalError: raw.hasExternalError,
1018
+ externalErrorText: raw.externalErrorText,
1019
+ reportAllKillers: raw.reportAllKillers
1020
+ };
1021
+ if (raw.hitCount !== void 0) {
1022
+ if (raw.hitLimit !== void 0) return Result.succeed(new VitestMutantRunCommand({
1023
+ ...base,
1024
+ hitCount: raw.hitCount,
1025
+ hitLimit: raw.hitLimit
1026
+ }));
1027
+ return Result.succeed(new VitestMutantRunCommand({
1028
+ ...base,
1029
+ hitCount: raw.hitCount
1030
+ }));
1031
+ }
1032
+ if (raw.hitLimit !== void 0) return Result.succeed(new VitestMutantRunCommand({
1033
+ ...base,
1034
+ hitLimit: raw.hitLimit
1035
+ }));
1036
+ return Result.succeed(new VitestMutantRunCommand(base));
1037
+ }), Cell.decide(vitestMutantRunWorkflow), Cell.encode((outcome) => Result.match(outcome, {
1038
+ onFailure: (e) => ({
1039
+ status: "error",
1040
+ errorMessage: e.message
1041
+ }),
1042
+ onSuccess: (out) => {
1043
+ let parsed;
1044
+ try {
1045
+ const raw = JSON.parse(out.testsJson);
1046
+ if (Array.isArray(raw)) parsed = raw.filter(isIdRecord);
1047
+ else parsed = [];
1048
+ } catch {
1049
+ parsed = [];
1050
+ }
1051
+ const nrOfTests = parsed.length;
1052
+ if (out.status === "Error") return {
1053
+ status: "error",
1054
+ errorMessage: out.errorMessage ?? "unknown"
1055
+ };
1056
+ if (out.status === "Timeout") {
1057
+ if (out.reason === void 0) return { status: "timeout" };
1058
+ return {
1059
+ status: "timeout",
1060
+ reason: out.reason
1061
+ };
1062
+ }
1063
+ if (out.status === "Killed") return {
1064
+ status: "killed",
1065
+ failureMessage: out.failureMessage ?? "",
1066
+ killedBy: (() => {
1067
+ if (out.killerIds !== void 0) return [...out.killerIds];
1068
+ return [];
1069
+ })(),
1070
+ nrOfTests
1071
+ };
1072
+ return {
1073
+ status: "survived",
1074
+ nrOfTests
1075
+ };
1076
+ }
1077
+ })), Cell.write((output) => Effect$1.succeed(output)));
1078
+ const dryRun = (options) => Cell.apply(dryRunDescription, options).pipe(Effect$1.mapError((cause) => (() => {
1079
+ if (cause instanceof TestRunnerFailed) return cause;
1080
+ return new TestRunnerFailed({
1081
+ runnerName: "vitest",
1082
+ phase: "dryRun",
1083
+ cause: errorToString(cause)
1084
+ });
1085
+ })()));
1086
+ const mutantRun = (options) => Cell.apply(mutantRunDescription, options).pipe(Effect$1.mapError((cause) => (() => {
1087
+ if (cause instanceof TestRunnerFailed) return cause;
1088
+ return new TestRunnerFailed({
1089
+ runnerName: "vitest",
1090
+ phase: "mutantRun",
1091
+ cause: errorToString(cause)
1092
+ });
1093
+ })()));
543
1094
  const dispose = Effect$1.gen(function* () {
544
1095
  const state = yield* getState;
545
1096
  if (state.ctx !== void 0) {
@@ -554,7 +1105,7 @@ const makeVitestRunnerLayer = (input) => Layer.effect(TestRunner, Effect$1.gen(f
554
1105
  catch: (cause) => new TestRunnerFailed({
555
1106
  runnerName: "vitest",
556
1107
  phase: "dispose",
557
- cause
1108
+ cause: errorToString(cause)
558
1109
  })
559
1110
  });
560
1111
  }
@@ -567,12 +1118,15 @@ const makeVitestRunnerLayer = (input) => Layer.effect(TestRunner, Effect$1.gen(f
567
1118
  dispose
568
1119
  });
569
1120
  }));
1121
+ function isTestResultLike(value) {
1122
+ return typeof value === "object" && value !== null && "id" in value && typeof Reflect.get(value, "id") === "string";
1123
+ }
1124
+ function isIdRecord(value) {
1125
+ return typeof value === "object" && value !== null && "id" in value && typeof Reflect.get(value, "id") === "string";
1126
+ }
570
1127
  function mergeCoverage(to, from) {
571
- for (const [mutantId, hitCount] of Object.entries(from)) {
572
- const existing = to[mutantId];
573
- if (existing !== void 0) to[mutantId] = existing + hitCount;
574
- else to[mutantId] = hitCount;
575
- }
1128
+ for (const [mutantId, hitCount] of Object.entries(from)) if (mutantId in to) to[mutantId] = to[mutantId] + hitCount;
1129
+ else to[mutantId] = hitCount;
576
1130
  }
577
1131
  //#endregion
578
1132
  //#region src/index.ts
@@ -583,7 +1137,7 @@ function mergeCoverage(to, from) {
583
1137
  * in, so the requirement is visible in the type and an engine that does not
584
1138
  * provide it fails to compile.
585
1139
  */
586
- const strykerPlugins = [declarePlugin(PluginKind.TestRunner, "vitest", Layer.unwrap(Effect$1.gen(function* () {
1140
+ const strykerPlugins = [declarePlugin("TestRunner", "vitest", Layer.unwrap(Effect$1.gen(function* () {
587
1141
  const options = yield* RunConfiguration;
588
1142
  const sandboxDirectory = yield* SandboxDirectory;
589
1143
  return makeVitestRunnerLayer({