@systemfsoftware/stryker-js-vitest-runner 2.1.0 → 3.0.0

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.
Files changed (3) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/index.mjs +314 -387
  3. package/package.json +4 -5
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @systemfsoftware/stryker-js-vitest-runner
2
2
 
3
+ ## 3.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - The dry-run decision moved to a plain kernel (dryRunCell removed); the mutant-run decision splits into branded MutantKilled|MutantSurvived|MutantTimeout|MutantDryError variants
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies:
12
+ - @systemfsoftware/effect-cell-types@6.0.0
13
+ - @systemfsoftware/stryker-js@1.0.0
14
+
3
15
  ## 2.1.0
4
16
 
5
17
  ### Minor Changes
package/dist/index.mjs CHANGED
@@ -7,81 +7,61 @@ import { Cell, Workflow } from "@systemfsoftware/effect-cell-types";
7
7
  import { Module } from "@systemfsoftware/stryker-js/Module";
8
8
  import { INSTRUMENTER_CONSTANTS, errorToString, normalizeFileName } from "@systemfsoftware/stryker-js/Mutant";
9
9
  import { TestRunner, TestRunnerFailed, testFilesProvided } from "@systemfsoftware/stryker-js/TestRunner";
10
+ import * as Context from "effect/Context";
10
11
  import * as FileSystem from "effect/FileSystem";
11
- import { pipe } from "effect/Function";
12
+ import * as Match from "effect/Match";
12
13
  import * as Option from "effect/Option";
13
14
  import * as Path from "effect/Path";
14
15
  import * as Ref from "effect/Ref";
15
16
  import * as Result from "effect/Result";
16
17
  import { Effect } from "effect";
17
- import * as Match from "effect/Match";
18
- //#region src/Runner.schema.ts
19
- /**
20
- * Runner schemas — declarations for the vitest runner capability.
21
- *
22
- * Houses every Schema/Wire type this capability publishes or decodes
23
- * internally: option validation, coverage/task metadata, sandbox
24
- * manifest, and the dynamic vitest module shape.
25
- */
26
- /**
27
- * The `vitest` option section of the Stryker options document. `related`
28
- * defaults to `true` at decode, the other members are optional.
29
- */
30
- const VitestRunnerOptionsSchema = S.Struct({
31
- dir: S.optional(S.String),
32
- related: S.optional(S.Boolean).pipe(S.withDecodingDefault(Effect.succeed(true))),
33
- configFile: S.optional(S.String)
34
- });
35
- /**
36
- * The `vitest` section of the Stryker options document: absent from the input
37
- * document, or present as a partial, and decoded into the section defaults.
38
- */
39
- const VitestSectionSchema = S.optional(VitestRunnerOptionsSchema).pipe(S.withDecodingDefault(Effect.succeed({ related: true })));
40
- const HitCountMetaSchema = S.Struct({ hitCount: S.optional(S.Finite) });
41
- const MutantCoverageMetaSchema = S.Struct({ mutantCoverage: S.optional(S.Struct({
42
- static: S.Record(S.String, S.Finite),
43
- perTest: S.Record(S.String, S.Record(S.String, S.Finite))
44
- })) });
45
- const MutantCoverageShapeSchema = S.Struct({
46
- static: S.Record(S.String, S.Finite),
47
- perTest: S.Record(S.String, S.Record(S.String, S.Finite))
48
- });
49
- var CoverageDecodeFailed = class extends S.TaggedError()("CoverageDecodeFailed", { cause: S.Unknown }) {};
50
- const ExportEntry = S.Union([S.String, S.Record(S.String, S.Unknown)]);
51
- const PackageManifest = S.StructWithRest(S.Struct({
52
- name: S.optional(S.String),
53
- exports: S.optional(S.Record(S.String, ExportEntry))
54
- }), [S.Record(S.String, S.Unknown)]);
55
- /**
56
- * The dynamically imported project-local `vitest/node` module. The runtime
57
- * check only asserts object-likeness: the module namespace is whatever the
58
- * resolved package exports, and the consumers tolerate a missing
59
- * `createVitest` via their own fallbacks.
60
- */
61
- const VitestNodeModuleSchema = S.declare((input) => input !== null && typeof input === "object" && !Array.isArray(input), { description: "The project-local vitest/node module" });
62
- /** The `package.json` document of a resolved vitest package. */
63
- const VitestPackageSchema = S.Struct({ version: S.String });
64
- //#endregion
65
- //#region src/VitestDryRun.workflow.ts
66
- /**
67
- * VitestDryRun workflow — pure result-mapping for the dry-run phase.
68
- *
69
- * Every function here is pure: it maps raw vitest task payloads to
70
- * Stryker run results without touching the filesystem or spawning
71
- * processes. Impure orchestration lives in `Runner.ts`.
72
- */
18
+ //#region src/interpret-vitest-run.workflow.ts
19
+ var VitestMutantRunCommand = class extends S.TaggedClass()("VitestMutantRunCommand", {
20
+ rawTests: S.Array(S.Unknown),
21
+ projectRoot: S.String,
22
+ hasExternalError: S.Boolean,
23
+ externalErrorText: S.String,
24
+ hitCount: S.optional(S.Finite),
25
+ hitLimit: S.optional(S.Finite),
26
+ reportAllKillers: S.Boolean
27
+ }) {};
28
+ const VitestMutantRunTypeId = Symbol.for("@systemfsoftware/stryker-js-vitest-runner/VitestMutantRun");
29
+ var MutantKilled = class extends S.TaggedClass()("Killed", {
30
+ testsJson: S.String,
31
+ killerIds: S.optional(S.Array(S.String)),
32
+ failureMessage: S.optional(S.String)
33
+ }) {
34
+ [VitestMutantRunTypeId] = VitestMutantRunTypeId;
35
+ };
36
+ var MutantSurvived = class extends S.TaggedClass()("Survived", { testsJson: S.String }) {
37
+ [VitestMutantRunTypeId] = VitestMutantRunTypeId;
38
+ };
39
+ var MutantTimeout = class extends S.TaggedClass()("Timeout", {
40
+ testsJson: S.String,
41
+ reason: S.optional(S.String)
42
+ }) {
43
+ [VitestMutantRunTypeId] = VitestMutantRunTypeId;
44
+ };
45
+ var MutantDryError = class extends S.TaggedClass()("Error", {
46
+ testsJson: S.String,
47
+ errorMessage: S.optional(S.String)
48
+ }) {
49
+ [VitestMutantRunTypeId] = VitestMutantRunTypeId;
50
+ };
51
+ var VitestMutantRunError = class extends S.TaggedError()("VitestMutantRunError", { message: S.String }) {
52
+ [VitestMutantRunTypeId] = VitestMutantRunTypeId;
53
+ };
73
54
  var VitestDryRunCommand$1 = class extends S.TaggedClass()("VitestDryRunCommand", {
74
55
  rawTests: S.Array(S.Unknown),
75
56
  projectRoot: S.String,
76
57
  hasExternalError: S.Boolean,
77
58
  externalErrorText: S.String
78
59
  }) {};
79
- var VitestDryRunOutput$1 = class extends S.TaggedClass()("VitestDryRunOutput", {
60
+ var VitestDryRunOutput = class extends S.TaggedClass()("VitestDryRunOutput", {
80
61
  status: S.Literals(["Complete", "Error"]),
81
62
  testsJson: S.String,
82
63
  errorMessage: S.optional(S.String)
83
64
  }) {};
84
- S.TaggedError()("VitestDryRunError", { message: S.String });
85
65
  const recordOption$1 = (value) => S.decodeUnknownOption(S.Record(S.String, S.Unknown))(value);
86
66
  const getStringField$1 = (record, key) => Option.fromNullishOr(record[key]).pipe(Option.filter((v) => typeof v === "string"));
87
67
  const getNumberField$1 = (record, key) => Option.fromNullishOr(record[key]).pipe(Option.filter((v) => typeof v === "number"));
@@ -130,12 +110,6 @@ const toRawTestIdRaw$1 = (test) => {
130
110
  onSome: (file) => Option.getOrElse(Option.fromNullishOr(getFilepath$1(file)), () => "unknown.js")
131
111
  })}#${collectTestNameRaw$1(test)}`;
132
112
  };
133
- /**
134
- * A test id is `<file>#<test name>`, and the file is reported relative to the
135
- * project root so an id is stable across machines and sandbox directories.
136
- * Vitest reports an absolute path, so the root prefix is stripped here rather
137
- * than resolved — a decision body has no path service and needs none.
138
- */
139
113
  const normalizeTestIdRaw$1 = (id, projectRoot) => {
140
114
  const hash = id.indexOf("#");
141
115
  if (hash === -1) return id;
@@ -220,65 +194,146 @@ const convertTestRaw$1 = (test, projectRoot) => {
220
194
  status
221
195
  })));
222
196
  };
223
- const decideVitestDryRun$1 = (command) => {
224
- const tests = command.rawTests.map((t) => convertTestRaw$1(t, command.projectRoot));
225
- const hasFailure = tests.some((t) => t.status === "failed");
226
- return Match.value({
227
- hasFailure,
228
- hasExternalError: command.hasExternalError
229
- }).pipe(Match.when({
230
- hasFailure: false,
231
- hasExternalError: true
232
- }, () => Result.succeed(VitestDryRunOutput$1.make({
233
- status: "Error",
234
- testsJson: JSON.stringify(tests),
235
- errorMessage: `An error occurred outside of a test run: ${command.externalErrorText}`
236
- }))), Match.orElse(() => Result.succeed(VitestDryRunOutput$1.make({
237
- status: "Complete",
238
- testsJson: JSON.stringify(tests),
239
- errorMessage: void 0
240
- }))));
197
+ const decideVitestDryRun$1 = (command) => Match.value(command.rawTests.map((t) => convertTestRaw$1(t, command.projectRoot))).pipe(Match.when((tests) => tests.some((t) => t.status === "failed") === false && command.hasExternalError === true, (tests) => Result.succeed(VitestDryRunOutput.make({
198
+ status: "Error",
199
+ testsJson: JSON.stringify(tests),
200
+ errorMessage: `An error occurred outside of a test run: ${command.externalErrorText}`
201
+ }))), Match.orElse((tests) => Result.succeed(VitestDryRunOutput.make({
202
+ status: "Complete",
203
+ testsJson: JSON.stringify(tests),
204
+ errorMessage: void 0
205
+ }))));
206
+ const hitLimitReason = (hitCount, hitLimit) => {
207
+ if (hitCount === void 0 || hitLimit === void 0) return Option.none();
208
+ if (hitCount > hitLimit) return Option.some(`Hit limit reached (${hitCount}/${hitLimit})`);
209
+ return Option.none();
241
210
  };
242
- const vitestDryRunWorkflow = Workflow.make(VitestDryRunCommand$1, decideVitestDryRun$1);
211
+ const decideVitestMutantRun = (command) => Match.value(hitLimitReason(command.hitCount, command.hitLimit)).pipe(Match.when(Option.isSome, (hit) => Result.succeed(MutantTimeout.make({
212
+ testsJson: "[]",
213
+ reason: hit.value
214
+ }))), Match.when(Option.isNone, () => {
215
+ const dryOut = decideVitestDryRun$1(VitestDryRunCommand$1.make({
216
+ rawTests: command.rawTests,
217
+ projectRoot: command.projectRoot,
218
+ hasExternalError: command.hasExternalError,
219
+ externalErrorText: command.externalErrorText
220
+ }));
221
+ return Match.value(Result.isFailure(dryOut)).pipe(Match.when(true, () => Result.fail(new VitestMutantRunError({ message: "dry run mapping failed" }))), Match.when(false, () => {
222
+ const dry = Result.getOrElse(dryOut, () => VitestDryRunOutput.make({
223
+ status: "Complete",
224
+ testsJson: "[]",
225
+ errorMessage: void 0
226
+ }));
227
+ return Match.value(dry.status === "Error").pipe(Match.when(true, () => Result.succeed(MutantDryError.make({
228
+ testsJson: "[]",
229
+ errorMessage: dry.errorMessage
230
+ }))), Match.when(false, () => {
231
+ const testsOption = Option.liftThrowable((input) => JSON.parse(input))(dry.testsJson).pipe(Option.filter((v) => Array.isArray(v)));
232
+ const killed = Option.getOrElse(testsOption, () => []).filter((t) => t.status === "failed");
233
+ return Match.value(killed.length > 0).pipe(Match.when(true, () => Match.value(command.reportAllKillers).pipe(Match.when(true, () => {
234
+ const firstKiller = Option.fromNullishOr(killed[0]);
235
+ const failureMessage = Option.match(firstKiller, {
236
+ onNone: () => void 0,
237
+ onSome: (k) => k.failureMessage
238
+ });
239
+ return Result.succeed(MutantKilled.make({
240
+ testsJson: dry.testsJson,
241
+ killerIds: killed.map((t) => t.id),
242
+ failureMessage
243
+ }));
244
+ }), Match.when(false, () => {
245
+ const first = Option.fromNullishOr(killed[0]);
246
+ const failureMessage = Option.match(first, {
247
+ onNone: () => void 0,
248
+ onSome: (k) => k.failureMessage
249
+ });
250
+ return Match.value(Option.isSome(first)).pipe(Match.when(true, () => Result.succeed(MutantKilled.make({
251
+ testsJson: dry.testsJson,
252
+ killerIds: Option.match(first, {
253
+ onNone: () => void 0,
254
+ onSome: (k) => [k.id]
255
+ }),
256
+ failureMessage
257
+ }))), Match.when(false, () => Result.succeed(MutantKilled.make({
258
+ testsJson: dry.testsJson,
259
+ killerIds: Option.getOrUndefined(Option.match(first, {
260
+ onNone: () => Option.none(),
261
+ onSome: (k) => Option.some([k.id])
262
+ })),
263
+ failureMessage
264
+ }))), Match.exhaustive);
265
+ }), Match.exhaustive)), Match.when(false, () => Result.succeed(MutantSurvived.make({ testsJson: dry.testsJson }))), Match.exhaustive);
266
+ }), Match.exhaustive);
267
+ }), Match.exhaustive);
268
+ }), Match.exhaustive);
269
+ const interpretVitestRun = Workflow.make(VitestMutantRunCommand, decideVitestMutantRun);
243
270
  //#endregion
244
- //#region src/VitestMutantRun.workflow.ts
245
- /**
246
- * VitestMutantRun workflow — pure result-mapping for the mutant-run phase.
247
- */
248
- var VitestMutantRunCommand = class extends S.TaggedClass()("VitestMutantRunCommand", {
249
- rawTests: S.Array(S.Unknown),
250
- projectRoot: S.String,
251
- hasExternalError: S.Boolean,
252
- externalErrorText: S.String,
253
- hitCount: S.optional(S.Finite),
254
- hitLimit: S.optional(S.Finite),
255
- reportAllKillers: S.Boolean
256
- }) {};
257
- var VitestMutantRunOutput = class extends S.TaggedClass()("VitestMutantRunOutput", {
258
- status: S.Literals([
259
- "Killed",
260
- "Survived",
261
- "Timeout",
262
- "Error"
263
- ]),
264
- testsJson: S.String,
265
- errorMessage: S.optional(S.String),
266
- killerIds: S.optional(S.Array(S.String)),
267
- failureMessage: S.optional(S.String),
268
- reason: S.optional(S.String)
269
- }) {};
270
- var VitestMutantRunError = class extends S.TaggedError()("VitestMutantRunError", { message: S.String }) {};
271
+ //#region src/Runner.schema.ts
272
+ const VitestRunnerOptionsSchema = S.Struct({
273
+ dir: S.optional(S.String),
274
+ related: S.optional(S.Boolean).pipe(S.withDecodingDefault(Effect.succeed(true))),
275
+ configFile: S.optional(S.String)
276
+ });
277
+ const VitestSectionSchema = S.optional(VitestRunnerOptionsSchema).pipe(S.withDecodingDefault(Effect.succeed({ related: true })));
278
+ const HitCountMetaSchema = S.Struct({ hitCount: S.optional(S.Finite) });
279
+ const MutantCoverageMetaSchema = S.Struct({ mutantCoverage: S.optional(S.Struct({
280
+ static: S.Record(S.String, S.Finite),
281
+ perTest: S.Record(S.String, S.Record(S.String, S.Finite))
282
+ })) });
283
+ const MutantCoverageShapeSchema = S.Struct({
284
+ static: S.Record(S.String, S.Finite),
285
+ perTest: S.Record(S.String, S.Record(S.String, S.Finite))
286
+ });
287
+ var CoverageDecodeFailed = class extends S.TaggedError()("CoverageDecodeFailed", { cause: S.Unknown }) {};
288
+ const ExportEntry = S.Union([S.String, S.Record(S.String, S.Unknown)]);
289
+ const PackageManifest = S.StructWithRest(S.Struct({
290
+ name: S.optional(S.String),
291
+ exports: S.optional(S.Record(S.String, ExportEntry))
292
+ }), [S.Record(S.String, S.Unknown)]);
293
+ const VitestNodeModuleSchema = S.declare((input) => input !== null && typeof input === "object" && !Array.isArray(input), { description: "The project-local vitest/node module" });
294
+ const VitestPackageSchema = S.Struct({ version: S.String });
271
295
  var VitestDryRunCommand = class extends S.TaggedClass()("VitestDryRunCommand", {
272
296
  rawTests: S.Array(S.Unknown),
273
297
  projectRoot: S.String,
274
298
  hasExternalError: S.Boolean,
275
299
  externalErrorText: S.String
276
300
  }) {};
277
- var VitestDryRunOutput = class extends S.TaggedClass()("VitestDryRunOutput", {
278
- status: S.Literals(["Complete", "Error"]),
301
+ var DryRunComplete = class extends S.TaggedClass()("Complete", { testsJson: S.String }) {};
302
+ var DryRunExternalError = class extends S.TaggedClass()("Error", {
279
303
  testsJson: S.String,
280
- errorMessage: S.optional(S.String)
304
+ errorMessage: S.String
281
305
  }) {};
306
+ //#endregion
307
+ //#region src/Runner.ts
308
+ var VitestHarness = class extends Context.Service()("VitestHarness") {};
309
+ function fromTestId(id) {
310
+ const [file, ...name] = id.split("#");
311
+ return {
312
+ file,
313
+ test: name.join("#")
314
+ };
315
+ }
316
+ function normalizeTestId(id, projectRoot, pathService) {
317
+ const { file, test } = fromTestId(id);
318
+ return `${normalizeFileName(pathService.relative(projectRoot, file))}#${test}`;
319
+ }
320
+ function normalizeCoverage(rawCoverage, projectRoot, pathService) {
321
+ return {
322
+ perTest: Object.fromEntries(Object.entries(rawCoverage.perTest).map(([rawTestId, coverageData]) => [normalizeTestId(rawTestId, projectRoot, pathService), coverageData])),
323
+ static: rawCoverage.static
324
+ };
325
+ }
326
+ function collectTestsFromSuite(suite) {
327
+ return suite.tasks.flatMap((task) => {
328
+ if (task.type === "suite") return collectTestsFromSuite(task);
329
+ return task;
330
+ });
331
+ }
332
+ function isErrorCodeError(error) {
333
+ if (error instanceof Error && "code" in error) return typeof Reflect.get(error, "code") === "string";
334
+ return false;
335
+ }
336
+ const VITEST_ERROR_CODES = Object.freeze({ FILES_NOT_FOUND: "VITEST_FILES_NOT_FOUND" });
282
337
  const recordOption = (value) => S.decodeUnknownOption(S.Record(S.String, S.Unknown))(value);
283
338
  const getStringField = (record, key) => Option.fromNullishOr(record[key]).pipe(Option.filter((v) => typeof v === "string"));
284
339
  const getNumberField = (record, key) => Option.fromNullishOr(record[key]).pipe(Option.filter((v) => typeof v === "number"));
@@ -327,12 +382,6 @@ const toRawTestIdRaw = (test) => {
327
382
  onSome: (file) => Option.getOrElse(Option.fromNullishOr(getFilepath(file)), () => "unknown.js")
328
383
  })}#${collectTestNameRaw(test)}`;
329
384
  };
330
- /**
331
- * A test id is `<file>#<test name>`, and the file is reported relative to the
332
- * project root so an id is stable across machines and sandbox directories.
333
- * Vitest reports an absolute path, so the root prefix is stripped here rather
334
- * than resolved — a decision body has no path service and needs none.
335
- */
336
385
  const normalizeTestIdRaw = (id, projectRoot) => {
337
386
  const hash = id.indexOf("#");
338
387
  if (hash === -1) return id;
@@ -417,134 +466,14 @@ const convertTestRaw = (test, projectRoot) => {
417
466
  status
418
467
  })));
419
468
  };
420
- 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({
421
- status: "Error",
422
- testsJson: JSON.stringify(tests),
423
- errorMessage: `An error occurred outside of a test run: ${command.externalErrorText}`
424
- }))), Match.orElse((tests) => Result.succeed(VitestDryRunOutput.make({
425
- status: "Complete",
426
- testsJson: JSON.stringify(tests),
427
- errorMessage: void 0
428
- }))));
429
- /**
430
- * The hit-limit cutoff: `Some` iff both numbers are present AND the count
431
- * strictly exceeds the limit. The boundary is `>` and not `>=` on purpose —
432
- * `hitCount === hitLimit` is the last permitted hit, not one too many.
433
- */
434
- const hitLimitReason = (hitCount, hitLimit) => {
435
- if (hitCount === void 0 || hitLimit === void 0) return Option.none();
436
- if (hitCount > hitLimit) return Option.some(`Hit limit reached (${hitCount}/${hitLimit})`);
437
- return Option.none();
438
- };
439
- const decideVitestMutantRun = (command) => Match.value(hitLimitReason(command.hitCount, command.hitLimit)).pipe(Match.when(Option.isSome, (hit) => Result.succeed(VitestMutantRunOutput.make({
440
- status: "Timeout",
441
- testsJson: "[]",
442
- errorMessage: void 0,
443
- killerIds: void 0,
444
- failureMessage: void 0,
445
- reason: hit.value
446
- }))), Match.when(Option.isNone, () => {
447
- const dryOut = decideVitestDryRun(VitestDryRunCommand.make({
448
- rawTests: command.rawTests,
449
- projectRoot: command.projectRoot,
450
- hasExternalError: command.hasExternalError,
451
- externalErrorText: command.externalErrorText
452
- }));
453
- return Match.value(Result.isFailure(dryOut)).pipe(Match.when(true, () => Result.fail(new VitestMutantRunError({ message: "dry run mapping failed" }))), Match.when(false, () => {
454
- const dry = Result.getOrElse(dryOut, () => VitestDryRunOutput.make({
455
- status: "Complete",
456
- testsJson: "[]",
457
- errorMessage: void 0
458
- }));
459
- return Match.value(dry.status === "Error").pipe(Match.when(true, () => Result.succeed(VitestMutantRunOutput.make({
460
- status: "Error",
461
- testsJson: "[]",
462
- errorMessage: dry.errorMessage,
463
- killerIds: void 0,
464
- failureMessage: void 0
465
- }))), Match.when(false, () => {
466
- const testsOption = Option.liftThrowable((input) => JSON.parse(input))(dry.testsJson).pipe(Option.filter((v) => Array.isArray(v)));
467
- const killed = Option.getOrElse(testsOption, () => []).filter((t) => t.status === "failed");
468
- return Match.value(killed.length > 0).pipe(Match.when(true, () => Match.value(command.reportAllKillers).pipe(Match.when(true, () => {
469
- const firstKiller = Option.fromNullishOr(killed[0]);
470
- const failureMessage = Option.match(firstKiller, {
471
- onNone: () => void 0,
472
- onSome: (k) => k.failureMessage
473
- });
474
- return Result.succeed(VitestMutantRunOutput.make({
475
- status: "Killed",
476
- testsJson: dry.testsJson,
477
- errorMessage: void 0,
478
- killerIds: killed.map((t) => t.id),
479
- failureMessage
480
- }));
481
- }), Match.when(false, () => {
482
- const first = Option.fromNullishOr(killed[0]);
483
- const failureMessage = Option.match(first, {
484
- onNone: () => void 0,
485
- onSome: (k) => k.failureMessage
486
- });
487
- return Match.value(Option.isSome(first)).pipe(Match.when(true, () => Result.succeed(VitestMutantRunOutput.make({
488
- status: "Killed",
489
- testsJson: dry.testsJson,
490
- errorMessage: void 0,
491
- killerIds: Option.match(first, {
492
- onNone: () => void 0,
493
- onSome: (k) => [k.id]
494
- }),
495
- failureMessage
496
- }))), Match.when(false, () => Result.succeed(VitestMutantRunOutput.make({
497
- status: "Killed",
498
- testsJson: dry.testsJson,
499
- errorMessage: void 0,
500
- killerIds: Option.getOrUndefined(Option.match(first, {
501
- onNone: () => Option.none(),
502
- onSome: (k) => Option.some([k.id])
503
- })),
504
- failureMessage
505
- }))), Match.exhaustive);
506
- }), Match.exhaustive)), Match.when(false, () => Result.succeed(VitestMutantRunOutput.make({
507
- status: "Survived",
508
- testsJson: dry.testsJson,
509
- errorMessage: void 0,
510
- killerIds: void 0,
511
- failureMessage: void 0
512
- }))), Match.exhaustive);
513
- }), Match.exhaustive);
514
- }), Match.exhaustive);
515
- }), Match.exhaustive);
516
- const vitestMutantRunWorkflow = Workflow.make(VitestMutantRunCommand, decideVitestMutantRun);
517
- //#endregion
518
- //#region src/Runner.ts
519
- function fromTestId(id) {
520
- const [file, ...name] = id.split("#");
521
- return {
522
- file,
523
- test: name.join("#")
524
- };
525
- }
526
- function normalizeTestId(id, projectRoot, pathService) {
527
- const { file, test } = fromTestId(id);
528
- return `${normalizeFileName(pathService.relative(projectRoot, file))}#${test}`;
529
- }
530
- function normalizeCoverage(rawCoverage, projectRoot, pathService) {
531
- return {
532
- perTest: Object.fromEntries(Object.entries(rawCoverage.perTest).map(([rawTestId, coverageData]) => [normalizeTestId(rawTestId, projectRoot, pathService), coverageData])),
533
- static: rawCoverage.static
534
- };
535
- }
536
- function collectTestsFromSuite(suite) {
537
- return suite.tasks.flatMap((task) => {
538
- if (task.type === "suite") return collectTestsFromSuite(task);
539
- return task;
469
+ const decideVitestDryRun = (command) => {
470
+ const tests = command.rawTests.map((t) => convertTestRaw(t, command.projectRoot));
471
+ if (tests.some((t) => t.status === "failed") === false && command.hasExternalError) return DryRunExternalError.make({
472
+ testsJson: JSON.stringify(tests),
473
+ errorMessage: `An error occurred outside of a test run: ${command.externalErrorText}`
540
474
  });
541
- }
542
- function isErrorCodeError(error) {
543
- if (error instanceof Error && "code" in error) return typeof Reflect.get(error, "code") === "string";
544
- return false;
545
- }
546
- /** @see https://github.com/vitest-dev/vitest/blob/main/packages/vitest/src/node/errors.ts */
547
- const VITEST_ERROR_CODES = Object.freeze({ FILES_NOT_FOUND: "VITEST_FILES_NOT_FOUND" });
475
+ return DryRunComplete.make({ testsJson: JSON.stringify(tests) });
476
+ };
548
477
  const SOURCE_CONDITION = "@systemfsoftware/source";
549
478
  const sourceTargetOf = (entry) => {
550
479
  if (typeof entry === "string") return (() => {
@@ -592,12 +521,6 @@ const readSandboxSelfAliases = (projectRoot) => Effect$1.gen(function* () {
592
521
  onSome: (manifest) => sandboxSelfAliases(manifest, projectRoot, pathService)
593
522
  });
594
523
  });
595
- /**
596
- * Vite records a package specifier as a bare dep. Vitest related-mode then
597
- * joins that specifier onto the project root, misses the file, and reports
598
- * zero tests. Returning the sandbox source path from `resolveId` makes the
599
- * dep a real filesystem path related-mode can walk.
600
- */
601
524
  const sandboxSelfPlugin = (aliases) => ({
602
525
  name: "stryker-sandbox-self-exports",
603
526
  enforce: "pre",
@@ -924,169 +847,173 @@ const makeVitestRunnerLayer = (input) => Layer.effect(TestRunner, Effect$1.gen(f
924
847
  })()
925
848
  };
926
849
  });
927
- const dryRunDescription = pipe(Cell.read((command) => Effect$1.gen(function* () {
928
- (yield* requireCtx).provide("mode", "dry-run");
929
- const hasTestFiles = testFilesProvided(command);
850
+ const harnessImpl = {
851
+ setMode: (mode) => Effect$1.gen(function* () {
852
+ (yield* requireCtx).provide("mode", mode);
853
+ }),
854
+ provide: (key, value) => Effect$1.gen(function* () {
855
+ const ctx = yield* requireCtx;
856
+ if (key === "hitLimit") {
857
+ if (typeof value === "number" || value === void 0) ctx.provide("hitLimit", value);
858
+ else ctx.provide("hitLimit", void 0);
859
+ } else if (key === "mutantActivation") {
860
+ if (value === "runtime" || value === "static") ctx.provide("mutantActivation", value);
861
+ } else if (typeof value === "string") ctx.provide("activeMutant", value);
862
+ })
863
+ };
864
+ const mutantRunCell = Cell.layer({
865
+ read: (command) => Effect$1.gen(function* () {
866
+ const harness = yield* VitestHarness;
867
+ yield* harness.setMode("mutant");
868
+ yield* harness.provide("hitLimit", command.hitLimit);
869
+ yield* harness.provide("mutantActivation", command.mutantActivation);
870
+ yield* harness.provide("activeMutant", command.activeMutant.id);
871
+ const { rawTests, hasExternalError, externalErrorText } = yield* collectRaw({
872
+ testIds: (() => {
873
+ if (command.testFilter !== void 0) return [...command.testFilter];
874
+ })(),
875
+ relatedFiles: [command.sandboxFileName]
876
+ });
877
+ const hitCount = yield* readHitCount.pipe(Effect$1.mapError((cause) => new TestRunnerFailed({
878
+ runnerName: "vitest",
879
+ phase: "mutantRun",
880
+ cause: errorToString(cause)
881
+ })), Effect$1.option, Effect$1.map(Option.getOrUndefined));
882
+ const reportAllKillers = (() => {
883
+ if (typeof input.options.disableBail === "boolean") return input.options.disableBail;
884
+ return false;
885
+ })();
886
+ if (hitCount === void 0) return {
887
+ rawTests,
888
+ projectRoot: input.sandboxDirectory,
889
+ hasExternalError,
890
+ externalErrorText,
891
+ hitLimit: command.hitLimit,
892
+ reportAllKillers
893
+ };
894
+ return {
895
+ rawTests,
896
+ projectRoot: input.sandboxDirectory,
897
+ hasExternalError,
898
+ externalErrorText,
899
+ hitCount,
900
+ hitLimit: command.hitLimit,
901
+ reportAllKillers
902
+ };
903
+ }),
904
+ decode: (raw) => {
905
+ const base = {
906
+ rawTests: raw.rawTests,
907
+ projectRoot: raw.projectRoot,
908
+ hasExternalError: raw.hasExternalError,
909
+ externalErrorText: raw.externalErrorText,
910
+ reportAllKillers: raw.reportAllKillers
911
+ };
912
+ if (raw.hitCount !== void 0) {
913
+ if (raw.hitLimit !== void 0) return Result.succeed(new VitestMutantRunCommand({
914
+ ...base,
915
+ hitCount: raw.hitCount,
916
+ hitLimit: raw.hitLimit
917
+ }));
918
+ return Result.succeed(new VitestMutantRunCommand({
919
+ ...base,
920
+ hitCount: raw.hitCount
921
+ }));
922
+ }
923
+ if (raw.hitLimit !== void 0) return Result.succeed(new VitestMutantRunCommand({
924
+ ...base,
925
+ hitLimit: raw.hitLimit
926
+ }));
927
+ return Result.succeed(new VitestMutantRunCommand(base));
928
+ },
929
+ decide: interpretVitestRun,
930
+ encode: (outcome) => Result.match(outcome, {
931
+ onFailure: (e) => ({
932
+ status: "error",
933
+ errorMessage: e.message
934
+ }),
935
+ onSuccess: (out) => {
936
+ const nrOfTests = () => {
937
+ try {
938
+ const raw = JSON.parse(out.testsJson);
939
+ if (Array.isArray(raw)) return raw.filter(isIdRecord).length;
940
+ return 0;
941
+ } catch {
942
+ return 0;
943
+ }
944
+ };
945
+ return Match.value(out).pipe(Match.tag("Error", (error) => ({
946
+ status: "error",
947
+ errorMessage: error.errorMessage ?? "unknown"
948
+ })), Match.tag("Timeout", (timeout) => (() => {
949
+ if (timeout.reason === void 0) return { status: "timeout" };
950
+ return {
951
+ status: "timeout",
952
+ reason: timeout.reason
953
+ };
954
+ })()), Match.tag("Killed", (killed) => ({
955
+ status: "killed",
956
+ failureMessage: killed.failureMessage ?? "",
957
+ killedBy: (() => {
958
+ if (killed.killerIds !== void 0) return [...killed.killerIds];
959
+ return [];
960
+ })(),
961
+ nrOfTests: nrOfTests()
962
+ })), Match.tag("Survived", () => ({
963
+ status: "survived",
964
+ nrOfTests: nrOfTests()
965
+ })), Match.exhaustive);
966
+ }
967
+ }),
968
+ write: (output, _raw) => Effect$1.succeed(output)
969
+ });
970
+ const dryRun = (options) => Effect$1.gen(function* () {
971
+ yield* (yield* VitestHarness).setMode("dry-run");
972
+ const hasTestFiles = testFilesProvided(options);
930
973
  const filter = (() => {
931
974
  if (hasTestFiles) return {
932
- testFiles: [...command.testFiles ?? []],
975
+ testFiles: [...options.testFiles ?? []],
933
976
  relatedFiles: (() => {
934
- if (command.files !== void 0) return [...command.files];
977
+ if (options.files !== void 0) return [...options.files];
935
978
  })()
936
979
  };
937
980
  return { relatedFiles: (() => {
938
- if (command.files !== void 0) return [...command.files];
981
+ if (options.files !== void 0) return [...options.files];
939
982
  })() };
940
983
  })();
941
984
  const { rawTests, hasExternalError, externalErrorText } = yield* collectRaw(filter);
942
- return {
985
+ const decision = decideVitestDryRun(new VitestDryRunCommand({
943
986
  rawTests,
944
987
  projectRoot: input.sandboxDirectory,
945
988
  hasExternalError,
946
989
  externalErrorText
947
- };
948
- })), Cell.decode((raw) => Result.succeed(new VitestDryRunCommand$1({
949
- rawTests: raw.rawTests,
950
- projectRoot: raw.projectRoot,
951
- hasExternalError: raw.hasExternalError,
952
- externalErrorText: raw.externalErrorText
953
- }))), Cell.decide(vitestDryRunWorkflow), Cell.encode((outcome) => Result.match(outcome, {
954
- onFailure: (e) => ({
990
+ }));
991
+ const result = Match.value(decision).pipe(Match.tag("Error", (error) => ({
955
992
  status: "error",
956
- errorMessage: e.message
957
- }),
958
- onSuccess: (out) => {
959
- const raw = JSON.parse(out.testsJson);
993
+ errorMessage: error.errorMessage
994
+ })), Match.tag("Complete", (complete) => {
995
+ const raw = JSON.parse(complete.testsJson);
960
996
  let tests;
961
997
  if (Array.isArray(raw)) tests = raw.filter(isTestResultLike);
962
998
  else tests = [];
963
- if (out.status === "Error") return {
964
- status: "error",
965
- errorMessage: out.errorMessage ?? "unknown"
966
- };
967
999
  return {
968
1000
  status: "complete",
969
1001
  tests
970
1002
  };
971
- }
972
- })), Cell.write((output) => Effect$1.gen(function* () {
973
- if (output.status === "complete") {
1003
+ }), Match.exhaustive);
1004
+ if (result.status === "complete") {
974
1005
  const mutantCoverage = yield* readMutantCoverage.pipe(Effect$1.mapError((cause) => new TestRunnerFailed({
975
1006
  runnerName: "vitest",
976
1007
  phase: "dryRun",
977
1008
  cause: errorToString(cause)
978
1009
  })));
979
1010
  if (mutantCoverage !== void 0) return {
980
- ...output,
1011
+ ...result,
981
1012
  mutantCoverage
982
1013
  };
983
1014
  }
984
- return output;
985
- })));
986
- const mutantRunDescription = pipe(Cell.read((command) => Effect$1.gen(function* () {
987
- const ctx = yield* requireCtx;
988
- ctx.provide("mode", "mutant");
989
- ctx.provide("hitLimit", command.hitLimit);
990
- ctx.provide("mutantActivation", command.mutantActivation);
991
- ctx.provide("activeMutant", command.activeMutant.id);
992
- const { rawTests, hasExternalError, externalErrorText } = yield* collectRaw({
993
- testIds: (() => {
994
- if (command.testFilter !== void 0) return [...command.testFilter];
995
- })(),
996
- relatedFiles: [command.sandboxFileName]
997
- });
998
- const hitCount = yield* readHitCount.pipe(Effect$1.mapError((cause) => new TestRunnerFailed({
999
- runnerName: "vitest",
1000
- phase: "mutantRun",
1001
- cause: errorToString(cause)
1002
- })), Effect$1.option, Effect$1.map(Option.getOrUndefined));
1003
- const reportAllKillers = (() => {
1004
- if (typeof input.options.disableBail === "boolean") return input.options.disableBail;
1005
- return false;
1006
- })();
1007
- if (hitCount === void 0) return {
1008
- rawTests,
1009
- projectRoot: input.sandboxDirectory,
1010
- hasExternalError,
1011
- externalErrorText,
1012
- hitLimit: command.hitLimit,
1013
- reportAllKillers
1014
- };
1015
- return {
1016
- rawTests,
1017
- projectRoot: input.sandboxDirectory,
1018
- hasExternalError,
1019
- externalErrorText,
1020
- hitCount,
1021
- hitLimit: command.hitLimit,
1022
- reportAllKillers
1023
- };
1024
- })), Cell.decode((raw) => {
1025
- const base = {
1026
- rawTests: raw.rawTests,
1027
- projectRoot: raw.projectRoot,
1028
- hasExternalError: raw.hasExternalError,
1029
- externalErrorText: raw.externalErrorText,
1030
- reportAllKillers: raw.reportAllKillers
1031
- };
1032
- if (raw.hitCount !== void 0) {
1033
- if (raw.hitLimit !== void 0) return Result.succeed(new VitestMutantRunCommand({
1034
- ...base,
1035
- hitCount: raw.hitCount,
1036
- hitLimit: raw.hitLimit
1037
- }));
1038
- return Result.succeed(new VitestMutantRunCommand({
1039
- ...base,
1040
- hitCount: raw.hitCount
1041
- }));
1042
- }
1043
- if (raw.hitLimit !== void 0) return Result.succeed(new VitestMutantRunCommand({
1044
- ...base,
1045
- hitLimit: raw.hitLimit
1046
- }));
1047
- return Result.succeed(new VitestMutantRunCommand(base));
1048
- }), Cell.decide(vitestMutantRunWorkflow), Cell.encode((outcome) => Result.match(outcome, {
1049
- onFailure: (e) => ({
1050
- status: "error",
1051
- errorMessage: e.message
1052
- }),
1053
- onSuccess: (out) => {
1054
- let parsed;
1055
- try {
1056
- const raw = JSON.parse(out.testsJson);
1057
- if (Array.isArray(raw)) parsed = raw.filter(isIdRecord);
1058
- else parsed = [];
1059
- } catch {
1060
- parsed = [];
1061
- }
1062
- const nrOfTests = parsed.length;
1063
- if (out.status === "Error") return {
1064
- status: "error",
1065
- errorMessage: out.errorMessage ?? "unknown"
1066
- };
1067
- if (out.status === "Timeout") {
1068
- if (out.reason === void 0) return { status: "timeout" };
1069
- return {
1070
- status: "timeout",
1071
- reason: out.reason
1072
- };
1073
- }
1074
- if (out.status === "Killed") return {
1075
- status: "killed",
1076
- failureMessage: out.failureMessage ?? "",
1077
- killedBy: (() => {
1078
- if (out.killerIds !== void 0) return [...out.killerIds];
1079
- return [];
1080
- })(),
1081
- nrOfTests
1082
- };
1083
- return {
1084
- status: "survived",
1085
- nrOfTests
1086
- };
1087
- }
1088
- })), Cell.write((output) => Effect$1.succeed(output)));
1089
- const dryRun = (options) => Cell.apply(dryRunDescription, options).pipe(Effect$1.mapError((cause) => (() => {
1015
+ return result;
1016
+ }).pipe(Effect$1.provideService(VitestHarness, harnessImpl), Effect$1.mapError((cause) => (() => {
1090
1017
  if (cause instanceof TestRunnerFailed) return cause;
1091
1018
  return new TestRunnerFailed({
1092
1019
  runnerName: "vitest",
@@ -1094,7 +1021,7 @@ const makeVitestRunnerLayer = (input) => Layer.effect(TestRunner, Effect$1.gen(f
1094
1021
  cause: errorToString(cause)
1095
1022
  });
1096
1023
  })()));
1097
- const mutantRun = (options) => Cell.apply(mutantRunDescription, options).pipe(Effect$1.mapError((cause) => (() => {
1024
+ const mutantRun = (options) => Cell.run(mutantRunCell, options).pipe(Effect$1.provideService(VitestHarness, harnessImpl), Effect$1.mapError((cause) => (() => {
1098
1025
  if (cause instanceof TestRunnerFailed) return cause;
1099
1026
  return new TestRunnerFailed({
1100
1027
  runnerName: "vitest",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@systemfsoftware/stryker-js-vitest-runner",
3
- "version": "2.1.0",
3
+ "version": "3.0.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/systemfsoftware/systemfsoftware.git",
@@ -31,8 +31,8 @@
31
31
  "coverageAnalysis": "perTest"
32
32
  },
33
33
  "dependencies": {
34
- "@systemfsoftware/effect-cell-types": "^5.0.2",
35
- "@systemfsoftware/stryker-js": "^0.2.0"
34
+ "@systemfsoftware/stryker-js": "^1.0.0",
35
+ "@systemfsoftware/effect-cell-types": "^6.0.0"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "effect": "^4.0.0-rc.112",
@@ -50,10 +50,9 @@
50
50
  "tsdown": "^0.22.14",
51
51
  "typescript": "^7",
52
52
  "vitest": "^4",
53
- "@systemfsoftware/all": "^1.1.0",
53
+ "@systemfsoftware/all": "^1.1.2",
54
54
  "@systemfsoftware/effect-gherkin-spec": "^4.0.0",
55
55
  "@systemfsoftware/oxlint-config": "^0.1.0",
56
- "@systemfsoftware/stryker-js-platform-node": "^0.2.0",
57
56
  "@systemfsoftware/tsconfig": "^1.3.3",
58
57
  "@systemfsoftware/vitest-config": "^0.1.0"
59
58
  },