@shivaedev/vitest-rerun 0.0.0-stage → 0.1.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 (71) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/LICENSE +21 -0
  3. package/README.md +93 -2
  4. package/dist/cli.d.ts +3 -0
  5. package/dist/cli.d.ts.map +1 -0
  6. package/dist/cli.js +7 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/error.d.ts +11 -0
  9. package/dist/error.d.ts.map +1 -0
  10. package/dist/error.js +7 -0
  11. package/dist/error.js.map +1 -0
  12. package/dist/flakyTests.d.ts +30 -0
  13. package/dist/flakyTests.d.ts.map +1 -0
  14. package/dist/flakyTests.js +24 -0
  15. package/dist/flakyTests.js.map +1 -0
  16. package/dist/internal/TimedJsonReporter.d.ts +5 -0
  17. package/dist/internal/TimedJsonReporter.d.ts.map +1 -0
  18. package/dist/internal/TimedJsonReporter.js +17 -0
  19. package/dist/internal/TimedJsonReporter.js.map +1 -0
  20. package/dist/internal/attempt.d.ts +2 -0
  21. package/dist/internal/attempt.d.ts.map +1 -0
  22. package/dist/internal/attempt.js +10 -0
  23. package/dist/internal/attempt.js.map +1 -0
  24. package/dist/internal/junit.d.ts +8 -0
  25. package/dist/internal/junit.d.ts.map +1 -0
  26. package/dist/internal/junit.js +22 -0
  27. package/dist/internal/junit.js.map +1 -0
  28. package/dist/internal/main.d.ts +3 -0
  29. package/dist/internal/main.d.ts.map +1 -0
  30. package/dist/internal/main.js +7 -0
  31. package/dist/internal/main.js.map +1 -0
  32. package/dist/internal/mergeAttempt.d.ts +9 -0
  33. package/dist/internal/mergeAttempt.d.ts.map +1 -0
  34. package/dist/internal/mergeAttempt.js +87 -0
  35. package/dist/internal/mergeAttempt.js.map +1 -0
  36. package/dist/internal/reportPaths.d.ts +13 -0
  37. package/dist/internal/reportPaths.d.ts.map +1 -0
  38. package/dist/internal/reportPaths.js +18 -0
  39. package/dist/internal/reportPaths.js.map +1 -0
  40. package/dist/internal/runAttempt.d.ts +8 -0
  41. package/dist/internal/runAttempt.d.ts.map +1 -0
  42. package/dist/internal/runAttempt.js +102 -0
  43. package/dist/internal/runAttempt.js.map +1 -0
  44. package/dist/internal/snapshot.d.ts +20 -0
  45. package/dist/internal/snapshot.d.ts.map +1 -0
  46. package/dist/internal/snapshot.js +22 -0
  47. package/dist/internal/snapshot.js.map +1 -0
  48. package/dist/options.d.ts +12 -0
  49. package/dist/options.d.ts.map +1 -0
  50. package/dist/options.js +40 -0
  51. package/dist/options.js.map +1 -0
  52. package/dist/rerun.d.ts +5 -0
  53. package/dist/rerun.d.ts.map +1 -0
  54. package/dist/rerun.js +51 -0
  55. package/dist/rerun.js.map +1 -0
  56. package/docs/north-star.md +38 -0
  57. package/docs/roadmap.md +18 -0
  58. package/package.json +77 -3
  59. package/src/cli.ts +7 -0
  60. package/src/error.ts +10 -0
  61. package/src/flakyTests.ts +31 -0
  62. package/src/internal/TimedJsonReporter.ts +17 -0
  63. package/src/internal/attempt.ts +15 -0
  64. package/src/internal/junit.ts +25 -0
  65. package/src/internal/main.ts +10 -0
  66. package/src/internal/mergeAttempt.ts +105 -0
  67. package/src/internal/reportPaths.ts +20 -0
  68. package/src/internal/runAttempt.ts +126 -0
  69. package/src/internal/snapshot.ts +43 -0
  70. package/src/options.ts +47 -0
  71. package/src/rerun.ts +64 -0
@@ -0,0 +1,102 @@
1
+ import { Config, Console, Effect, FileSystem, Schema } from "effect";
2
+ import * as VitestNode from "vitest/node";
3
+ import { failWith } from "#error.ts";
4
+ import { writeFlakyTests } from "#flakyTests.ts";
5
+ import { RerunJunitReporter } from "#internal/junit.ts";
6
+ import { mergeAttempt } from "#internal/mergeAttempt.ts";
7
+ import { attemptPaths, reportPaths } from "#internal/reportPaths.ts";
8
+ import { failedFiles, readSnapshot, saveSnapshot, tests } from "#internal/snapshot.ts";
9
+ import { TimedJsonReporter } from "#internal/TimedJsonReporter.ts";
10
+ const CoverageReport = Schema.Struct({ coverageMap: Schema.optional(Schema.Unknown) });
11
+ const decodeCoverage = Schema.decodeUnknownEffect(Schema.fromJsonString(CoverageReport));
12
+ function promise(message, run) {
13
+ return Effect.tryPromise({ catch: failWith(message), try: run });
14
+ }
15
+ function start(options, reports, cacheDir) {
16
+ return Effect.acquireRelease(promise("Vitest could not start", () => VitestNode.createVitest({
17
+ ...options,
18
+ reporters: ["default", new TimedJsonReporter({ outputFile: reports.json }), new RerunJunitReporter(reports.junit)],
19
+ retry: 0,
20
+ watch: false,
21
+ }, cacheDir === undefined ? {} : { cacheDir })), (vitest) => Effect.promise(() => vitest.close()));
22
+ }
23
+ const failedSpecifications = Effect.fn("VitestRerun.failedSpecifications")(function* (vitest, previous, json) {
24
+ const failed = new Set(yield* failedFiles(json));
25
+ yield* promise("Vitest could not prepare the rerun", () => vitest.standalone());
26
+ const specifications = yield* promise("Vitest could not list test files", () => vitest.globTestSpecifications());
27
+ return specifications.flatMap((spec) => {
28
+ const file = previous.files.find((item) => item.id === spec.taskId && failed.has(spec.moduleId) && item.result?.state === "fail");
29
+ if (file === undefined) {
30
+ return [];
31
+ }
32
+ const runnable = tests(file.tasks).filter((test) => !["pass", "skip", "todo"].includes(test.result?.state ?? test.mode));
33
+ return [spec.project.createSpecification(spec.moduleId, file.tasks.length === 0 ? {} : { testIds: runnable.map((test) => test.id) }, spec.pool)];
34
+ });
35
+ });
36
+ const mergeCoverage = Effect.fn("VitestRerun.mergeCoverage")(function* (vitest, outputDir, attempt) {
37
+ const fs = yield* FileSystem.FileSystem;
38
+ const maps = [];
39
+ for (let earlier = 1; earlier <= attempt; earlier += 1) {
40
+ const { coverageMap } = yield* decodeCoverage(yield* fs.readFileString(attemptPaths(outputDir, earlier).json));
41
+ if (coverageMap !== undefined) {
42
+ maps.push(coverageMap);
43
+ }
44
+ }
45
+ const provider = yield* promise("Vitest could not load its coverage provider", () => vitest.createCoverageProvider());
46
+ yield* promise("Vitest could not merge coverage", async () => provider?.mergeReports?.(maps));
47
+ });
48
+ const summaryLine = Effect.fn("VitestRerun.summaryLine")(function* (line) {
49
+ const fs = yield* FileSystem.FileSystem;
50
+ const summary = yield* Config.String("GITHUB_STEP_SUMMARY").pipe(Config.withDefault(""));
51
+ yield* Console.log(line);
52
+ if (summary !== "") {
53
+ yield* fs.writeFileString(summary, `${line}\n`, { flag: "a" });
54
+ }
55
+ });
56
+ const rerunFailed = Effect.fn("VitestRerun.rerunFailed")(function* (vitest, { attempt, outputDir }) {
57
+ const previous = yield* readSnapshot(attemptPaths(outputDir, attempt - 1).snapshot);
58
+ const final = reportPaths(outputDir);
59
+ const specifications = yield* failedSpecifications(vitest, previous, final.json);
60
+ if (specifications.length === 0) {
61
+ return yield* Effect.fail(failWith("No failed specifications found for the fresh rerun")(undefined));
62
+ }
63
+ yield* promise("Vitest could not rerun the failed tests", () => vitest.runTestSpecifications(specifications));
64
+ const lines = [];
65
+ const merged = mergeAttempt(previous, vitest.state.getFiles(), vitest.config.root, (line) => lines.push(line));
66
+ for (const file of merged.files) {
67
+ vitest.state.collectFiles(vitest.getProjectByName(file.projectName ?? ""), [file]);
68
+ }
69
+ yield* Effect.forEach(lines, summaryLine);
70
+ for (const reporter of [new TimedJsonReporter({ outputFile: final.json }), new RerunJunitReporter(final.junit)]) {
71
+ yield* promise("Vitest could not write the merged reports", async () => {
72
+ await reporter.onInit(vitest);
73
+ await reporter.onTestRunEnd(vitest.state.getTestModules());
74
+ });
75
+ }
76
+ if (merged.flaky.length > 0) {
77
+ yield* writeFlakyTests(final.flaky, merged.flaky);
78
+ }
79
+ if (vitest.config.coverage.enabled) {
80
+ yield* mergeCoverage(vitest, outputDir, attempt);
81
+ }
82
+ const snapshot = {
83
+ errors: vitest.state.getUnhandledErrors(),
84
+ failures: merged.failures,
85
+ files: vitest.state.getFiles(),
86
+ flaky: merged.flaky,
87
+ };
88
+ return snapshot;
89
+ });
90
+ export const runAttempt = Effect.fn("VitestRerun.runAttempt")(function* (input) {
91
+ const { filter, options } = VitestNode.parseCLI(["vitest", "run", ...input.vitestArgs]);
92
+ const rerun = input.attempt > 1;
93
+ const { shard: _shard, ...unsharded } = options;
94
+ const reports = rerun ? attemptPaths(input.outputDir, input.attempt) : reportPaths(input.outputDir);
95
+ const vitest = yield* start(rerun ? unsharded : options, reports, rerun ? reportPaths(input.outputDir).rerunCache : undefined);
96
+ const snapshot = rerun
97
+ ? yield* rerunFailed(vitest, input)
98
+ : yield* promise("Vitest could not run the tests", () => vitest.start(filter)).pipe(Effect.map(() => ({ errors: vitest.state.getUnhandledErrors(), failures: {}, files: vitest.state.getFiles(), flaky: [] })));
99
+ yield* saveSnapshot(attemptPaths(input.outputDir, input.attempt).snapshot, snapshot);
100
+ return snapshot.files.some((file) => file.result?.state === "fail") || snapshot.errors.length > 0;
101
+ }, Effect.scoped);
102
+ //# sourceMappingURL=runAttempt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runAttempt.js","sourceRoot":"","sources":["../../src/internal/runAttempt.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ import { Effect, FileSystem, Schema } from "effect";
2
+ import type { Vitest } from "vitest/node";
3
+ import type { FlakyTest } from "#flakyTests.ts";
4
+ export type Files = ReturnType<Vitest["state"]["getFiles"]>;
5
+ export type Task = Files[number]["tasks"][number];
6
+ export interface FailureHistory {
7
+ readonly attempts: number;
8
+ readonly messages: readonly string[];
9
+ }
10
+ export interface Snapshot {
11
+ readonly errors: readonly unknown[];
12
+ readonly failures: Readonly<Record<string, FailureHistory>>;
13
+ readonly files: Files;
14
+ readonly flaky: readonly FlakyTest[];
15
+ }
16
+ export declare const failedFiles: (path: string) => Effect.Effect<string[], import("effect/PlatformError").PlatformError | Schema.SchemaError, FileSystem.FileSystem>;
17
+ export declare function tests(tasks: readonly Task[]): Task[];
18
+ export declare const saveSnapshot: (path: string, snapshot: Snapshot) => Effect.Effect<void, import("effect/PlatformError").PlatformError, FileSystem.FileSystem>;
19
+ export declare const readSnapshot: (path: string) => Effect.Effect<Snapshot, import("effect/PlatformError").PlatformError, FileSystem.FileSystem>;
20
+ //# sourceMappingURL=snapshot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshot.d.ts","sourceRoot":"","sources":["../../src/internal/snapshot.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACpD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEhD,MAAM,MAAM,KAAK,GAAG,UAAU,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC;AAC5D,MAAM,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC;AAElD,MAAM,WAAW,cAAc;IAC9B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAED,MAAM,WAAW,QAAQ;IACxB,QAAQ,CAAC,MAAM,EAAE,SAAS,OAAO,EAAE,CAAC;IACpC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,CAAC;IAC5D,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,SAAS,SAAS,EAAE,CAAC;CACrC;AAKD,eAAO,MAAM,WAAW,qIAItB,CAAC;AAEH,wBAAgB,KAAK,CAAC,KAAK,EAAE,SAAS,IAAI,EAAE,GAAG,IAAI,EAAE,CAEpD;AAED,eAAO,MAAM,YAAY,gIAGvB,CAAC;AAEH,eAAO,MAAM,YAAY,gHAIvB,CAAC"}
@@ -0,0 +1,22 @@
1
+ import { deserialize, serialize } from "node:v8";
2
+ import { Effect, FileSystem, Schema } from "effect";
3
+ const Report = Schema.Struct({ testResults: Schema.Array(Schema.Struct({ name: Schema.String, status: Schema.Literals(["failed", "passed"]) })) });
4
+ const decodeReport = Schema.decodeUnknownEffect(Schema.fromJsonString(Report));
5
+ export const failedFiles = Effect.fn("VitestRerun.failedFiles")(function* (path) {
6
+ const fs = yield* FileSystem.FileSystem;
7
+ const report = yield* decodeReport(yield* fs.readFileString(path));
8
+ return [...new Set(report.testResults.filter((file) => file.status === "failed").map((file) => file.name))];
9
+ });
10
+ export function tests(tasks) {
11
+ return tasks.flatMap((task) => (task.type === "suite" ? tests(task.tasks) : [task]));
12
+ }
13
+ export const saveSnapshot = Effect.fn("VitestRerun.saveSnapshot")(function* (path, snapshot) {
14
+ const fs = yield* FileSystem.FileSystem;
15
+ yield* fs.writeFile(path, serialize(snapshot));
16
+ });
17
+ export const readSnapshot = Effect.fn("VitestRerun.readSnapshot")(function* (path) {
18
+ const fs = yield* FileSystem.FileSystem;
19
+ const snapshot = deserialize(yield* fs.readFile(path));
20
+ return snapshot;
21
+ });
22
+ //# sourceMappingURL=snapshot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshot.js","sourceRoot":"","sources":["../../src/internal/snapshot.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACjD,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAmBpD,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC;AACnJ,MAAM,YAAY,GAAG,MAAM,CAAC,mBAAmB,CAAC,MAAM,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC;AAE/E,MAAM,CAAC,MAAM,WAAW,GAAG,MAAM,CAAC,EAAE,CAAC,yBAAyB,CAAC,CAAC,QAAQ,CAAC,EAAE,IAAY;IACtF,MAAM,EAAE,GAAG,KAAK,CAAC,CAAC,UAAU,CAAC,UAAU,CAAC;IACxC,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC;IACnE,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAC7G,CAAC,CAAC,CAAC;AAEH,MAAM,UAAU,KAAK,CAAC,KAAsB;IAC3C,OAAO,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACtF,CAAC;AAED,MAAM,CAAC,MAAM,YAAY,GAAG,MAAM,CAAC,EAAE,CAAC,0BAA0B,CAAC,CAAC,QAAQ,CAAC,EAAE,IAAY,EAAE,QAAkB;IAC5G,MAAM,EAAE,GAAG,KAAK,CAAC,CAAC,UAAU,CAAC,UAAU,CAAC;IACxC,KAAK,CAAC,CAAC,EAAE,CAAC,SAAS,CAAC,IAAI,EAAE,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC;AAChD,CAAC,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,YAAY,GAAG,MAAM,CAAC,EAAE,CAAC,0BAA0B,CAAC,CAAC,QAAQ,CAAC,EAAE,IAAY;IACxF,MAAM,EAAE,GAAG,KAAK,CAAC,CAAC,UAAU,CAAC,UAAU,CAAC;IACxC,MAAM,QAAQ,GAAa,WAAW,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;IACjE,OAAO,QAAQ,CAAC;AACjB,CAAC,CAAC,CAAC"}
@@ -0,0 +1,12 @@
1
+ import { Effect } from "effect";
2
+ import { VitestRerunError } from "#error.ts";
3
+ export interface RerunOptions {
4
+ readonly cwd: string;
5
+ readonly outputDir: string;
6
+ readonly reruns: number;
7
+ readonly vitestArgs: readonly string[];
8
+ }
9
+ export declare const DEFAULT_OUTPUT_DIR = "test-results";
10
+ export declare const DEFAULT_RERUNS = 1;
11
+ export declare function parseOptions(args: readonly string[], cwd: string): Effect.Effect<RerunOptions, VitestRerunError>;
12
+ //# sourceMappingURL=options.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAE7C,MAAM,WAAW,YAAY;IAC5B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;CACvC;AAED,eAAO,MAAM,kBAAkB,iBAAiB,CAAC;AACjD,eAAO,MAAM,cAAc,IAAI,CAAC;AAShC,wBAAgB,YAAY,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,YAAY,EAAE,gBAAgB,CAAC,CAyBhH"}
@@ -0,0 +1,40 @@
1
+ import { resolve } from "node:path";
2
+ import { Effect } from "effect";
3
+ import { VitestRerunError } from "#error.ts";
4
+ export const DEFAULT_OUTPUT_DIR = "test-results";
5
+ export const DEFAULT_RERUNS = 1;
6
+ function option(arg, name, next) {
7
+ if (arg === name && next !== undefined) {
8
+ return { consumed: 2, value: next };
9
+ }
10
+ return arg.startsWith(`${name}=`) ? { consumed: 1, value: arg.slice(name.length + 1) } : undefined;
11
+ }
12
+ export function parseOptions(args, cwd) {
13
+ let outputDir = DEFAULT_OUTPUT_DIR;
14
+ let reruns = String(DEFAULT_RERUNS);
15
+ const vitestArgs = [];
16
+ for (let index = 0; index < args.length;) {
17
+ const arg = args[index] ?? "";
18
+ if (arg === "--") {
19
+ vitestArgs.push(...args.slice(index + 1));
20
+ break;
21
+ }
22
+ const output = option(arg, "--output-dir", args[index + 1]);
23
+ const count = option(arg, "--reruns", args[index + 1]);
24
+ if (output !== undefined) {
25
+ outputDir = output.value;
26
+ }
27
+ else if (count === undefined) {
28
+ vitestArgs.push(arg);
29
+ }
30
+ else {
31
+ reruns = count.value;
32
+ }
33
+ index += output?.consumed ?? count?.consumed ?? 1;
34
+ }
35
+ if (!/^\d+$/u.test(reruns)) {
36
+ return Effect.fail(new VitestRerunError({ message: `--reruns expects a whole number, got ${JSON.stringify(reruns)}` }));
37
+ }
38
+ return Effect.succeed({ cwd, outputDir: resolve(cwd, outputDir), reruns: Number(reruns), vitestArgs });
39
+ }
40
+ //# sourceMappingURL=options.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"options.js","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAS7C,MAAM,CAAC,MAAM,kBAAkB,GAAG,cAAc,CAAC;AACjD,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAC;AAEhC,SAAS,MAAM,CAAC,GAAW,EAAE,IAAY,EAAE,IAAwB;IAClE,IAAI,GAAG,KAAK,IAAI,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACxC,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IACrC,CAAC;IACD,OAAO,GAAG,CAAC,UAAU,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;AACpG,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,IAAuB,EAAE,GAAW;IAChE,IAAI,SAAS,GAAG,kBAAkB,CAAC;IACnC,IAAI,MAAM,GAAG,MAAM,CAAC,cAAc,CAAC,CAAC;IACpC,MAAM,UAAU,GAAa,EAAE,CAAC;IAChC,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC,MAAM,GAAI,CAAC;QAC3C,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;QAC9B,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;YAClB,UAAU,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC;YAC1C,MAAM;QACP,CAAC;QACD,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC;QAC5D,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,EAAE,UAAU,EAAE,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC;QACvD,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YAC1B,SAAS,GAAG,MAAM,CAAC,KAAK,CAAC;QAC1B,CAAC;aAAM,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YAChC,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACtB,CAAC;aAAM,CAAC;YACP,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC;QACtB,CAAC;QACD,KAAK,IAAI,MAAM,EAAE,QAAQ,IAAI,KAAK,EAAE,QAAQ,IAAI,CAAC,CAAC;IACnD,CAAC;IACD,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QAC5B,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,gBAAgB,CAAC,EAAE,OAAO,EAAE,wCAAwC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;IACzH,CAAC;IACD,OAAO,MAAM,CAAC,OAAO,CAAC,EAAE,GAAG,EAAE,SAAS,EAAE,OAAO,CAAC,GAAG,EAAE,SAAS,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC;AACxG,CAAC"}
@@ -0,0 +1,5 @@
1
+ import { Effect, FileSystem } from "effect";
2
+ import { ChildProcessSpawner } from "effect/process";
3
+ import type { RerunOptions } from "#options.ts";
4
+ export declare const rerun: (options: RerunOptions) => Effect.Effect<number, import("effect/PlatformError").PlatformError | import("effect/Schema").SchemaError, ChildProcessSpawner.ChildProcessSpawner | FileSystem.FileSystem>;
5
+ //# sourceMappingURL=rerun.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rerun.d.ts","sourceRoot":"","sources":["../src/rerun.ts"],"names":[],"mappings":"AAGA,OAAO,EAAW,MAAM,EAAE,UAAU,EAAU,MAAM,QAAQ,CAAC;AAC7D,OAAO,EAAgB,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAGnE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AA2ChD,eAAO,MAAM,KAAK,uMAahB,CAAC"}
package/dist/rerun.js ADDED
@@ -0,0 +1,51 @@
1
+ import { extname, join } from "node:path";
2
+ import process from "node:process";
3
+ import { fileURLToPath } from "node:url";
4
+ import { Console, Effect, FileSystem, Option } from "effect";
5
+ import { ChildProcess, ChildProcessSpawner } from "effect/process";
6
+ import { ATTEMPT_REPORT, attemptPaths, reportPaths } from "#internal/reportPaths.ts";
7
+ import { failedFiles, readSnapshot } from "#internal/snapshot.ts";
8
+ const ATTEMPT_MODULE = fileURLToPath(new URL(`./internal/attempt${extname(fileURLToPath(import.meta.url))}`, import.meta.url));
9
+ const removeReports = Effect.fn("VitestRerun.removeReports")(function* (outputDir) {
10
+ const fs = yield* FileSystem.FileSystem;
11
+ yield* fs.makeDirectory(outputDir, { recursive: true });
12
+ const { flaky, json, junit } = reportPaths(outputDir);
13
+ const attempts = (yield* fs.readDirectory(outputDir)).filter((name) => ATTEMPT_REPORT.test(name));
14
+ yield* Effect.forEach([flaky, json, junit, ...attempts.map((name) => join(outputDir, name))], (path) => fs.remove(path, { force: true }));
15
+ });
16
+ const spawnAttempt = Effect.fn("VitestRerun.spawnAttempt")(function* (options, attempt) {
17
+ const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
18
+ const command = ChildProcess.make(process.execPath, [...process.execArgv, ATTEMPT_MODULE, String(attempt), options.outputDir, ...options.vitestArgs], { cwd: options.cwd, extendEnv: true, stderr: "inherit", stdin: "inherit", stdout: "inherit" });
19
+ return yield* spawner.exitCode(command).pipe(Effect.map((code) => Option.some(Number(code))), Effect.catch((error) => Effect.as(Console.error(`vitest-rerun: attempt ${attempt} ended without an exit code: ${error.message}`), Option.none())));
20
+ });
21
+ const rerunnable = Effect.fn("VitestRerun.rerunnable")(function* (outputDir, attempt) {
22
+ const fs = yield* FileSystem.FileSystem;
23
+ const { snapshot } = attemptPaths(outputDir, attempt);
24
+ const { json } = reportPaths(outputDir);
25
+ if (!((yield* fs.exists(snapshot)) && (yield* fs.exists(json)))) {
26
+ return false;
27
+ }
28
+ return (yield* readSnapshot(snapshot)).errors.length === 0 && (yield* failedFiles(json)).length > 0;
29
+ });
30
+ const keepFirstAttempt = Effect.fn("VitestRerun.keepFirstAttempt")(function* (outputDir) {
31
+ const fs = yield* FileSystem.FileSystem;
32
+ const final = reportPaths(outputDir);
33
+ const first = attemptPaths(outputDir, 1);
34
+ yield* fs.copyFile(final.json, first.json);
35
+ yield* fs.copyFile(final.junit, first.junit);
36
+ });
37
+ export const rerun = Effect.fn("VitestRerun.rerun")(function* (options) {
38
+ yield* removeReports(options.outputDir);
39
+ let code = yield* spawnAttempt(options, 1);
40
+ for (let attempt = 1; attempt <= options.reruns; attempt += 1) {
41
+ if (!(Option.exists(code, (status) => status !== 0) && (yield* rerunnable(options.outputDir, attempt)))) {
42
+ break;
43
+ }
44
+ if (attempt === 1) {
45
+ yield* keepFirstAttempt(options.outputDir);
46
+ }
47
+ code = yield* spawnAttempt(options, attempt + 1);
48
+ }
49
+ return Option.getOrElse(code, () => 1);
50
+ });
51
+ //# sourceMappingURL=rerun.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rerun.js","sourceRoot":"","sources":["../src/rerun.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,OAAO,MAAM,cAAc,CAAC;AACnC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC7D,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AACnE,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACrF,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAGlE,MAAM,cAAc,GAAG,aAAa,CAAC,IAAI,GAAG,CAAC,qBAAqB,OAAO,CAAC,aAAa,CAAC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAE/H,MAAM,aAAa,GAAG,MAAM,CAAC,EAAE,CAAC,2BAA2B,CAAC,CAAC,QAAQ,CAAC,EAAE,SAAiB;IACxF,MAAM,EAAE,GAAG,KAAK,CAAC,CAAC,UAAU,CAAC,UAAU,CAAC;IACxC,KAAK,CAAC,CAAC,EAAE,CAAC,aAAa,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACxD,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,WAAW,CAAC,SAAS,CAAC,CAAC;IACtD,MAAM,QAAQ,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IAClG,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;AAC3I,CAAC,CAAC,CAAC;AAEH,MAAM,YAAY,GAAG,MAAM,CAAC,EAAE,CAAC,0BAA0B,CAAC,CAAC,QAAQ,CAAC,EAAE,OAAqB,EAAE,OAAe;IAC3G,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,mBAAmB,CAAC,mBAAmB,CAAC;IAC/D,MAAM,OAAO,GAAG,YAAY,CAAC,IAAI,CAChC,OAAO,CAAC,QAAQ,EAChB,CAAC,GAAG,OAAO,CAAC,QAAQ,EAAE,cAAc,EAAE,MAAM,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,SAAS,EAAE,GAAG,OAAO,CAAC,UAAU,CAAC,EAChG,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,CAC7F,CAAC;IACF,OAAO,KAAK,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,IAAI,CAC3C,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAC/C,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,yBAAyB,OAAO,gCAAgC,KAAK,CAAC,OAAO,EAAE,CAAC,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,CACjJ,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,MAAM,UAAU,GAAG,MAAM,CAAC,EAAE,CAAC,wBAAwB,CAAC,CAAC,QAAQ,CAAC,EAAE,SAAiB,EAAE,OAAe;IACnG,MAAM,EAAE,GAAG,KAAK,CAAC,CAAC,UAAU,CAAC,UAAU,CAAC;IACxC,MAAM,EAAE,QAAQ,EAAE,GAAG,YAAY,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;IACtD,MAAM,EAAE,IAAI,EAAE,GAAG,WAAW,CAAC,SAAS,CAAC,CAAC;IACxC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;QACjE,OAAO,KAAK,CAAC;IACd,CAAC;IACD,OAAO,CAAC,KAAK,CAAC,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;AACrG,CAAC,CAAC,CAAC;AAEH,MAAM,gBAAgB,GAAG,MAAM,CAAC,EAAE,CAAC,8BAA8B,CAAC,CAAC,QAAQ,CAAC,EAAE,SAAiB;IAC9F,MAAM,EAAE,GAAG,KAAK,CAAC,CAAC,UAAU,CAAC,UAAU,CAAC;IACxC,MAAM,KAAK,GAAG,WAAW,CAAC,SAAS,CAAC,CAAC;IACrC,MAAM,KAAK,GAAG,YAAY,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC;IACzC,KAAK,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;IAC3C,KAAK,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;AAC9C,CAAC,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,EAAE,CAAC,mBAAmB,CAAC,CAAC,QAAQ,CAAC,EAAE,OAAqB;IACnF,KAAK,CAAC,CAAC,aAAa,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IACxC,IAAI,IAAI,GAAG,KAAK,CAAC,CAAC,YAAY,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;IAC3C,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,OAAO,CAAC,MAAM,EAAE,OAAO,IAAI,CAAC,EAAE,CAAC;QAC/D,IAAI,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,KAAK,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,UAAU,CAAC,OAAO,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;YACzG,MAAM;QACP,CAAC;QACD,IAAI,OAAO,KAAK,CAAC,EAAE,CAAC;YACnB,KAAK,CAAC,CAAC,gBAAgB,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QAC5C,CAAC;QACD,IAAI,GAAG,KAAK,CAAC,CAAC,YAAY,CAAC,OAAO,EAAE,OAAO,GAAG,CAAC,CAAC,CAAC;IAClD,CAAC;IACD,OAAO,MAAM,CAAC,SAAS,CAAC,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC;AACxC,CAAC,CAAC,CAAC"}
@@ -0,0 +1,38 @@
1
+ # North star
2
+
3
+ ## The problem
4
+
5
+ Some tests fail now and then for reasons that have nothing to do with the change under test: timing, ports, a slow runner. In a merge queue, each such failure blocks a merge and costs a full rerun of CI. Vitest's `retry` option looks like the fix, but it repeats the test in the same worker. Module state, mocks, timers and handles left behind by the failed attempt are still there, so the retry is not a clean second chance, and a pass after it says little.
6
+
7
+ Rerunning by hand or rerunning the whole job hides the flake entirely. Nobody records which tests needed a second attempt, so flaky tests accumulate until the suite is unreliable.
8
+
9
+ ## The ideal
10
+
11
+ A repository replaces `vitest run` in CI with `vitest-rerun`. Failed tests get a clean rerun in a fresh process, only the failed tests run again, and the job's result reflects whether each test eventually passed. Every recovered test is written to `flaky-tests.json` with its JUnit identity, its attempt count and the messages of its failed attempts, so a dashboard, a bot or a person can find it, count it and fix it.
12
+
13
+ The final reports read as if the run happened once: one JUnit testcase per test with its final result, a JSON report with summed durations, and coverage merged from every attempt. Tools that read JUnit or LCOV need no knowledge of reruns.
14
+
15
+ ## What good looks like
16
+
17
+ - A test that fails once and then passes yields a passing run and one `flaky-tests.json` entry whose `classname` and `name` match its JUnit testcase.
18
+ - A test that keeps failing fails the run and produces no flaky entry.
19
+ - A clean run writes no flaky file and runs every test once.
20
+ - A file that runs in several Vitest projects keeps a distinct JUnit identity per project.
21
+ - Tests that passed, including passing tests beside a failed one, never run twice.
22
+
23
+ ## Trade-offs
24
+
25
+ **Truth before green.** The package exists to let flaky tests pass, so it must be strict about everything else: an unhandled error, a test still failing after the last rerun or a broken report fails the run.
26
+
27
+ **Clean attempts before speed.** A fresh process costs startup time on every rerun. That cost is the point; an in-process shortcut would bring back the problem this package solves.
28
+
29
+ **A stable contract before a richer one.** `flaky-tests.json` stays small and its fields keep their names. New information goes into new fields only when a real consumer needs it.
30
+
31
+ **Vitest's machinery before our own.** Reports come from Vitest's JSON and JUnit reporters and coverage from its coverage provider. The package selects tests, merges attempt results and writes one file of its own.
32
+
33
+ ## What it leaves out
34
+
35
+ - Quarantining, skipping or ranking flaky tests across runs; that belongs to whatever reads `flaky-tests.json`.
36
+ - Retrying unhandled errors, crashed workers or failed setup outside a test file.
37
+ - Test runners other than Vitest, and in-process retries.
38
+ - Deciding which CI events may rerun; a repository chooses `--reruns` per event.
@@ -0,0 +1,18 @@
1
+ # Roadmap
2
+
3
+ This file owns vitest-rerun's implementation status and open questions. A story in this package establishes the runner's behavior against real Vitest processes, not every consumer's CI.
4
+
5
+ ## Built
6
+
7
+ - [x] A `vitest-rerun` command that runs Vitest with retries off and reruns only the failed tests in fresh processes, up to `--reruns` times (default 1).
8
+ - [x] Final JSON and JUnit reports for the whole run, with summed durations and the raw report of each attempt.
9
+ - [x] Coverage merged from every attempt, and a separate Vite cache for reruns.
10
+ - [x] `flaky-tests.json` with each recovered test's JUnit `classname` and `name`, file, attempt count and failure messages, plus the `FlakyTests` schema to read it.
11
+ - [x] A JUnit `classname` that names the Vitest project, so a file that runs in two projects stays distinct.
12
+ - [x] No rerun after an unhandled error; a recovered collection failure is reported in the job summary.
13
+ - [x] Platform's CI shards run through the package, with reruns only in the merge queue.
14
+
15
+ ## Open questions
16
+
17
+ - Whether a file that failed to load and then passed belongs in `flaky-tests.json`. It has no testcase in the final JUnit report, so today it appears only in the job summary.
18
+ - Whether a rerun should also cover failures in `beforeAll` and `afterAll` hooks that Vitest reports on a suite rather than a test.
package/package.json CHANGED
@@ -1,6 +1,80 @@
1
1
  {
2
2
  "name": "@shivaedev/vitest-rerun",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "Reruns failed Vitest tests in a fresh process and reports which tests were flaky",
5
+ "homepage": "https://github.com/ShivaeDev/platform/tree/main/packages/vitest-rerun#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/ShivaeDev/platform/issues"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/ShivaeDev/platform.git",
12
+ "directory": "packages/vitest-rerun"
13
+ },
14
+ "license": "MIT",
15
+ "sideEffects": false,
16
+ "type": "module",
17
+ "imports": {
18
+ "#test/*": "./src/test-support/*",
19
+ "#*.ts": {
20
+ "source": "./src/*.ts",
21
+ "types": "./dist/*.d.ts",
22
+ "default": "./dist/*.js"
23
+ }
24
+ },
25
+ "exports": {
26
+ "./*.ts": {
27
+ "source": "./src/*.ts",
28
+ "types": "./dist/*.d.ts",
29
+ "default": "./dist/*.js"
30
+ },
31
+ "./internal/*": null,
32
+ "./package.json": "./package.json"
33
+ },
34
+ "bin": {
35
+ "vitest-rerun": "./dist/cli.js"
36
+ },
37
+ "files": [
38
+ "dist",
39
+ "src",
40
+ "!src/**/*.test.ts",
41
+ "!src/**/*.spec.ts",
42
+ "!src/**/test-support",
43
+ "docs",
44
+ "CHANGELOG.md",
45
+ "README.md"
46
+ ],
47
+ "devDependencies": {
48
+ "@effect/platform-node": "4.0.4",
49
+ "@effect/platform-node-shared": "4.0.4",
50
+ "@effect/vitest": "4.0.4",
51
+ "@shivaedev/effect-test": "0.5.0",
52
+ "@shivaedev/quality": "0.24.0",
53
+ "@shivaedev/test-story": "0.7.0",
54
+ "@types/node": "24.19.2",
55
+ "@typescript/native": "npm:typescript@7.0.2",
56
+ "@vitest/coverage-v8": "5.0.3",
57
+ "effect": "4.0.4",
58
+ "typescript": "npm:@typescript/typescript6@6.0.2",
59
+ "vitest": "5.0.3"
60
+ },
61
+ "peerDependencies": {
62
+ "@effect/platform-node": "4.0.4",
63
+ "@effect/platform-node-shared": "4.0.4",
64
+ "effect": "4.0.4",
65
+ "vitest": "5.0.3"
66
+ },
67
+ "engines": {
68
+ "node": ">=24"
69
+ },
70
+ "publishConfig": {
71
+ "access": "public",
72
+ "provenance": true
73
+ },
74
+ "scripts": {
75
+ "build": "node --eval \"import('node:fs').then(({ rmSync }) => rmSync('dist', { force: true, recursive: true }))\" && tsc --project tsconfig.build.json",
76
+ "ready": "pnpm typecheck && pnpm test && pnpm build",
77
+ "test": "vitest run",
78
+ "typecheck": "tsc --noEmit"
79
+ }
6
80
  }
package/src/cli.ts ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import process from "node:process";
3
+ import { NodeServices } from "@effect/platform-node";
4
+ import { Effect } from "effect";
5
+ import { main } from "#internal/main.ts";
6
+
7
+ process.exitCode = await Effect.runPromise(main(process.argv.slice(2), process.cwd()).pipe(Effect.provide(NodeServices.layer)));
package/src/error.ts ADDED
@@ -0,0 +1,10 @@
1
+ import { Data } from "effect";
2
+
3
+ export class VitestRerunError extends Data.TaggedError("VitestRerunError")<{
4
+ readonly message: string;
5
+ readonly cause?: unknown;
6
+ }> {}
7
+
8
+ export function failWith(message: string) {
9
+ return (cause: unknown): VitestRerunError => new VitestRerunError({ cause, message });
10
+ }
@@ -0,0 +1,31 @@
1
+ import { Effect, FileSystem, Schema } from "effect";
2
+ import type { Task } from "#internal/snapshot.ts";
3
+
4
+ export const FlakyTest = Schema.Struct({
5
+ attempts: Schema.Number,
6
+ classname: Schema.String,
7
+ file: Schema.String,
8
+ messages: Schema.Array(Schema.String),
9
+ name: Schema.String,
10
+ });
11
+ export type FlakyTest = typeof FlakyTest.Type;
12
+
13
+ export const FlakyTests = Schema.Array(FlakyTest);
14
+
15
+ export function testcases(tasks: readonly Task[], ancestors = ""): { readonly name: string; readonly task: Task }[] {
16
+ return tasks.flatMap((task) => {
17
+ const name = ancestors === "" ? task.name : `${ancestors} > ${task.name}`;
18
+ return task.type === "suite" ? testcases(task.tasks, name) : [{ name, task }];
19
+ });
20
+ }
21
+
22
+ export function failureMessage(task: Task): string {
23
+ return (task.result?.errors ?? []).map((error) => error.message).join("\n");
24
+ }
25
+
26
+ const encode = Schema.encodeEffect(Schema.fromJsonString(FlakyTests));
27
+
28
+ export const writeFlakyTests = Effect.fn("VitestRerun.writeFlakyTests")(function* (path: string, flaky: readonly FlakyTest[]) {
29
+ const fs = yield* FileSystem.FileSystem;
30
+ yield* fs.writeFileString(path, `${yield* encode(flaky)}\n`);
31
+ });
@@ -0,0 +1,17 @@
1
+ import { readFileSync, writeFileSync } from "node:fs";
2
+ import { experimental_getRunnerTask, JsonReporter, type TestModule } from "vitest/node";
3
+
4
+ export class TimedJsonReporter extends JsonReporter {
5
+ override async onTestRunEnd(modules: readonly TestModule[]) {
6
+ await super.onTestRunEnd([...modules]);
7
+ const path = this.options.outputFile;
8
+ if (path === undefined) {
9
+ throw new Error("Timed JSON reports require an output file");
10
+ }
11
+ const report = JSON.parse(readFileSync(path, "utf8"));
12
+ for (const [index, module] of modules.entries()) {
13
+ report.testResults[index].durationMs = experimental_getRunnerTask(module).result?.duration ?? 0;
14
+ }
15
+ writeFileSync(path, JSON.stringify(report));
16
+ }
17
+ }
@@ -0,0 +1,15 @@
1
+ import process from "node:process";
2
+ import { NodeServices } from "@effect/platform-node";
3
+ import { Console, Effect } from "effect";
4
+ import { runAttempt } from "#internal/runAttempt.ts";
5
+
6
+ const [attempt = "1", outputDir = ".", ...vitestArgs] = process.argv.slice(2);
7
+ const failed = await Effect.runPromise(
8
+ runAttempt({ attempt: Number(attempt), outputDir, vitestArgs }).pipe(
9
+ Effect.catchTag("VitestRerunError", (error) => Effect.as(Console.error(`vitest-rerun: ${error.message}`, error.cause), true)),
10
+ Effect.provide(NodeServices.layer),
11
+ ),
12
+ );
13
+ if (failed) {
14
+ process.exitCode = 1;
15
+ }
@@ -0,0 +1,25 @@
1
+ import { relative } from "node:path";
2
+ import { JUnitReporter, type Vitest } from "vitest/node";
3
+
4
+ export function junitClassname(file: string, project: string): string {
5
+ return project === "" ? file : `|${project}| ${file}`;
6
+ }
7
+
8
+ export class RerunJunitReporter extends JUnitReporter {
9
+ readonly #root: { path: string };
10
+
11
+ constructor(outputFile: string) {
12
+ const root = { path: "" };
13
+ super({
14
+ addFileAttribute: true,
15
+ classnameTemplate: ({ displayName, filepath }) => junitClassname(relative(root.path, filepath), displayName),
16
+ outputFile,
17
+ });
18
+ this.#root = root;
19
+ }
20
+
21
+ override onInit(vitest: Vitest) {
22
+ this.#root.path = vitest.config.root;
23
+ return super.onInit(vitest);
24
+ }
25
+ }
@@ -0,0 +1,10 @@
1
+ import { Console, Effect } from "effect";
2
+ import { parseOptions } from "#options.ts";
3
+ import { rerun } from "#rerun.ts";
4
+
5
+ export function main(args: readonly string[], cwd: string) {
6
+ return parseOptions(args, cwd).pipe(
7
+ Effect.flatMap(rerun),
8
+ Effect.catchTag("VitestRerunError", (error) => Effect.as(Console.error(`vitest-rerun: ${error.message}`), 2)),
9
+ );
10
+ }