@vitest-agent/plugin 5.6.2 → 6.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.
@@ -0,0 +1,15 @@
1
+ import { RuntimeEnv } from "@effected/env";
2
+ import { Option } from "effect";
3
+
4
+ // src/utils/runs-in-ci.ts
5
+ /**
6
+ * Whether the process runs in CI, by `@effected/env`'s `RuntimeEnv` rule: a
7
+ * truthy `GITHUB_ACTIONS`, or a `CI` / `CONTINUOUS_INTEGRATION` that is set,
8
+ * non-empty, and not `false` / `0` (is-in-ci semantics). Reads `process.env`
9
+ * at call time, so the environment in force when the caller runs decides.
10
+ *
11
+ * @internal
12
+ */
13
+ const runsInCi = () => Option.isSome(RuntimeEnv.fromRecord(process.env).ci);
14
+
15
+ export { runsInCi };
@@ -1,45 +1,7 @@
1
- import { STRAY_OUTPUT_SOURCE, isEscapeOnly, makeStrayOutputRecorder } from "@vitest-agent/sdk";
2
- import { Writable } from "node:stream";
3
- import { StringDecoder } from "node:string_decoder";
1
+ import { STRAY_OUTPUT_SOURCE, makeStrayOutputRecorder } from "@vitest-agent/sdk";
2
+ import { CliUi } from "@effected/cli/ui";
4
3
 
5
4
  // src/utils/stray-output-capture.ts
6
- /**
7
- * Stray-output capture: a pass-through over Vitest's `Logger.outputStream` /
8
- * `errorStream`.
9
- *
10
- * Why those two streams (vitest\@5.0.0, `src/node/pools/workers/`): the forks
11
- * and vmForks pools fork each worker with `stdio: "pipe"` and pipe its stdout
12
- * and stderr into `project.vitest.logger.outputStream` / `errorStream`,
13
- * captured when the worker is constructed, so every raw byte a worker writes
14
- * (a grandchild process that inherits the worker's fds included, such as an
15
- * `execFileSync("git", …)` with default stdio) arrives on them. The threads
16
- * and vmThreads pools pipe a worker thread's JS-level `process.stdout` /
17
- * `stderr` the same way, but a child process spawned from a thread inherits
18
- * the main process's real fds 1/2, so its bytes reach the terminal directly
19
- * and nothing here can see them. Workers are constructed lazily by the pool,
20
- * at run time, long after `configureVitest`, so installing there is early
21
- * enough. The `Logger`'s own `console` was built from the ORIGINAL streams in
22
- * its constructor, so Vitest's logging bypasses the wrapper; its few direct
23
- * `outputStream.write` calls are escape-only (cursor show, clear screen),
24
- * which pass straight through unrecorded.
25
- *
26
- * Every byte is recorded (bounded: counts, a few sample lines). While a
27
- * printer is routed (the reporter's live view is up) and the stream's
28
- * original destination is the process terminal, bytes are line-buffered and
29
- * each whole line goes to the printer, which prints it above the live frame;
30
- * otherwise bytes pass through to the original stream unchanged. The
31
- * original is whatever stream object the Logger held: under MCP's
32
- * `run_tests` that is the server's null sink, so worker output still never
33
- * reaches the JSON-RPC stdout, and it is never routed.
34
- */
35
- /**
36
- * The most text a stream holds back waiting for a newline. Past it, the held
37
- * text is routed as a line of its own, so a child that never writes a newline
38
- * cannot grow the buffer without bound.
39
- *
40
- * @internal
41
- */
42
- const MAX_HELD_CHARS = 8192;
43
5
  const isCapture = (value) => typeof value === "object" && value !== null && "route" in value && "snapshot" in value;
44
6
  /**
45
7
  * The capture installed on `logger`, if any.
@@ -51,42 +13,6 @@ const strayOutputCaptureOf = (logger) => {
51
13
  const value = logger[STRAY_OUTPUT_SOURCE];
52
14
  return isCapture(value) ? value : void 0;
53
15
  };
54
- const DELEGATED = [
55
- "isTTY",
56
- "columns",
57
- "rows",
58
- "getColorDepth",
59
- "hasColors",
60
- "getWindowSize"
61
- ];
62
- var StrayStream = class extends Writable {
63
- original;
64
- take;
65
- constructor(original, take) {
66
- super({ decodeStrings: false });
67
- this.original = original;
68
- this.take = take;
69
- for (const key of DELEGATED) {
70
- if (!(key in original)) continue;
71
- Object.defineProperty(this, key, {
72
- configurable: true,
73
- enumerable: true,
74
- get: () => {
75
- const value = original[key];
76
- return typeof value === "function" ? value.bind(original) : value;
77
- }
78
- });
79
- }
80
- }
81
- _write(chunk, encoding, callback) {
82
- try {
83
- this.take(chunk, encoding);
84
- callback();
85
- } catch (err) {
86
- callback(err instanceof Error ? err : new Error(String(err)));
87
- }
88
- }
89
- };
90
16
  /**
91
17
  * Wrap `logger`'s output and error streams with the stray-output capture, once
92
18
  * per Logger (a second call returns the first capture). The capture is
@@ -103,67 +29,41 @@ const installStrayOutputCapture = (logger, terminal) => {
103
29
  const existing = strayOutputCaptureOf(logger);
104
30
  if (existing !== void 0) return existing;
105
31
  const recorder = makeStrayOutputRecorder();
106
- const originals = {
107
- stdout: logger.outputStream,
108
- stderr: logger.errorStream
109
- };
110
- const decoders = {
111
- stdout: new StringDecoder("utf8"),
112
- stderr: new StringDecoder("utf8")
113
- };
114
- const held = {
115
- stdout: "",
116
- stderr: ""
117
- };
118
32
  let printer;
119
- const flushHeld = (stream) => {
120
- const text = held[stream];
121
- if (text === "") return;
122
- held[stream] = "";
123
- originals[stream].write(text);
33
+ const print = (stream, line) => printer?.(stream, line) ?? false;
34
+ const wrap = (stream, original) => {
35
+ const routed = CliUi.routeStream(original, print, {
36
+ stream,
37
+ onText: (text) => recorder.write(stream, text)
38
+ });
39
+ routed.route(false);
40
+ return routed;
124
41
  };
125
- const take = (stream) => (chunk, encoding) => {
126
- const original = originals[stream];
127
- if (typeof chunk === "string" && isEscapeOnly(chunk)) {
128
- original.write(chunk, encoding);
129
- return;
130
- }
131
- const text = typeof chunk === "string" ? chunk : decoders[stream].write(chunk);
132
- recorder.write(stream, text);
133
- const route = printer !== void 0 && original === terminal[stream] ? printer : void 0;
134
- if (route === void 0) {
135
- original.write(chunk, encoding);
136
- return;
42
+ const streams = {
43
+ stdout: {
44
+ original: logger.outputStream,
45
+ routed: wrap("stdout", logger.outputStream)
46
+ },
47
+ stderr: {
48
+ original: logger.errorStream,
49
+ routed: wrap("stderr", logger.errorStream)
137
50
  }
138
- const lines = (held[stream] + text).split("\n");
139
- const rest = lines.pop() ?? "";
140
- if (rest.length > 8192) {
141
- lines.push(rest);
142
- held[stream] = "";
143
- } else held[stream] = rest;
144
- for (const line of lines) if (!route(stream, line)) original.write(`${line}\n`);
145
- };
146
- const flushHeldThrough = (stream) => {
147
- const text = held[stream];
148
- if (text === "") return;
149
- held[stream] = "";
150
- if (printer === void 0 || !printer(stream, text)) originals[stream].write(text);
151
51
  };
152
- logger.outputStream = new StrayStream(originals.stdout, take("stdout"));
153
- logger.errorStream = new StrayStream(originals.stderr, take("stderr"));
52
+ logger.outputStream = streams.stdout.routed.writable;
53
+ logger.errorStream = streams.stderr.routed.writable;
154
54
  const capture = {
155
55
  snapshot: recorder.snapshot,
156
56
  reset: recorder.reset,
157
57
  route: (next) => {
158
- if (next === void 0) {
159
- flushHeld("stdout");
160
- flushHeld("stderr");
161
- }
162
58
  printer = next;
59
+ for (const name of ["stdout", "stderr"]) {
60
+ const { original, routed } = streams[name];
61
+ routed.route(next !== void 0 && original === terminal[name]);
62
+ }
163
63
  },
164
64
  flush: () => {
165
- flushHeldThrough("stdout");
166
- flushHeldThrough("stderr");
65
+ streams.stdout.routed.flush();
66
+ streams.stderr.routed.flush();
167
67
  }
168
68
  };
169
69
  Object.defineProperty(logger, STRAY_OUTPUT_SOURCE, {
@@ -173,4 +73,4 @@ const installStrayOutputCapture = (logger, terminal) => {
173
73
  return capture;
174
74
  };
175
75
 
176
- export { MAX_HELD_CHARS, installStrayOutputCapture, strayOutputCaptureOf };
76
+ export { installStrayOutputCapture, strayOutputCaptureOf };
@@ -0,0 +1,173 @@
1
+ // src/utils/vitest-diagnostics.ts
2
+ /**
3
+ * Ported from vitest@5.0.5 packages/vitest/src/node/reporters/diagnostics.ts
4
+ * and base.ts (report*Diagnostic). Not exported by Vitest; replace with the
5
+ * public export when available. Re-check on every Vitest bump.
6
+ *
7
+ * The functions below diagnostics.ts's are kept verbatim (names, thresholds,
8
+ * arithmetic). `getIsolateDiagnostic` is the computation extracted from
9
+ * `BaseReporter.reportIsolateDiagnostic` (its watch / flag gating stays with
10
+ * the caller), and `formatTime` is ported from
11
+ * packages/vitest/src/node/reporters/renderers/utils.ts.
12
+ *
13
+ * @packageDocumentation
14
+ */
15
+ const DOM_ENVIRONMENTS = /* @__PURE__ */ new Set(["jsdom", "happy-dom"]);
16
+ /** Minimum summed environment setup time before the hint is worth printing. */
17
+ const MIN_ENVIRONMENT_TIME = 2e3;
18
+ /** Minimum share of the project's tracked time spent setting up environments. */
19
+ const MIN_ENVIRONMENT_SHARE = .25;
20
+ /**
21
+ * A hint has to be worth acting on: the estimated saving must be noticeable.
22
+ * Run-to-run noise of real suites is commonly a few percent, so anything below
23
+ * ~5% of the wall time cannot even be confirmed by trying the change - except
24
+ * on long runs, where 10 seconds is worth attention regardless of percentage.
25
+ * @internal
26
+ */
27
+ function isSavingWorthHinting(saving, executionTime) {
28
+ if (saving < 250) return false;
29
+ return saving >= executionTime * .05 || saving >= 1e4;
30
+ }
31
+ /** Minimum summed import time before the hint is worth printing. */
32
+ const MIN_IMPORT_TIME = 2e3;
33
+ /** Minimum share of the project's tracked time spent importing modules. */
34
+ const MIN_IMPORT_SHARE = .25;
35
+ /**
36
+ * Minimum fraction of module fetches that re-evaluate an already evaluated
37
+ * module. Below this the test files import mostly disjoint graphs and
38
+ * reusing workers would not meaningfully reduce the import work.
39
+ */
40
+ const MIN_IMPORT_DUPLICATION = .2;
41
+ /**
42
+ * Detects projects where test files repeatedly evaluate the same module
43
+ * graph. Typical for barrel-file imports: every test file pulls hundreds of
44
+ * shared modules to use a few of them, and `isolate: true` re-evaluates that
45
+ * graph for every file. The duplication is measured from the server-side
46
+ * fetch counts, so suites with disjoint per-file graphs - where reusing
47
+ * workers would not help - stay quiet.
48
+ * @internal
49
+ */
50
+ function getImportDiagnostics(projects) {
51
+ const diagnostics = [];
52
+ for (const project of projects) {
53
+ if (project.pool !== "forks" && project.pool !== "threads" || !project.isolate || project.isolateProvided || project.browser || project.fileCount <= project.parallelism || project.importTime < MIN_IMPORT_TIME || project.trackedTime <= 0 || project.importTime / project.trackedTime < MIN_IMPORT_SHARE) continue;
54
+ const parallelism = Math.max(1, project.parallelism);
55
+ let totalFetches = 0;
56
+ let avoidableFetches = 0;
57
+ for (const count of project.fetchCounts) {
58
+ totalFetches += count;
59
+ avoidableFetches += Math.max(0, count - parallelism);
60
+ }
61
+ if (totalFetches === 0) continue;
62
+ const duplication = avoidableFetches / totalFetches;
63
+ if (duplication < MIN_IMPORT_DUPLICATION) continue;
64
+ const estimatedSaving = project.importTime * duplication / parallelism;
65
+ if (!isSavingWorthHinting(estimatedSaving, project.executionTime)) continue;
66
+ diagnostics.push({
67
+ name: project.name,
68
+ importTime: project.importTime,
69
+ share: project.importTime / project.trackedTime,
70
+ totalFetches,
71
+ uniqueModules: project.fetchCounts.length,
72
+ duplication,
73
+ estimatedSaving
74
+ });
75
+ }
76
+ return diagnostics;
77
+ }
78
+ /**
79
+ * Estimates the wall-clock time saved by evaluating shared modules once per
80
+ * worker instead of once per test file. `moduleSelfTimes` holds, per module,
81
+ * the module's own evaluation time in every test file that evaluated it.
82
+ * Reused workers keep ~`parallelism` evaluations of each module (one per
83
+ * lane); the rest of the summed time is avoidable and spread across the
84
+ * lanes.
85
+ * @internal
86
+ */
87
+ function estimateModuleEvaluationSaving(moduleSelfTimes, parallelism) {
88
+ const lanes = Math.max(1, parallelism);
89
+ let avoidable = 0;
90
+ for (const times of moduleSelfTimes) {
91
+ if (times.length <= lanes) continue;
92
+ let sum = 0;
93
+ for (const time of times) sum += time;
94
+ avoidable += sum * (times.length - lanes) / times.length;
95
+ }
96
+ return avoidable / lanes;
97
+ }
98
+ /** Minimum summed transform time before the hint is worth printing. */
99
+ const MIN_TRANSFORM_TIME = 2e3;
100
+ /** Minimum share of the project's tracked time spent transforming modules. */
101
+ const MIN_TRANSFORM_SHARE = .25;
102
+ /**
103
+ * Detects projects that spend the run transforming modules. Without the fs
104
+ * module cache every `vitest run` starts from scratch and transforms the
105
+ * whole module graph again; `fsModuleCache` persists the results on disk so
106
+ * repeated runs skip them.
107
+ * @internal
108
+ */
109
+ function getTransformDiagnostics(projects) {
110
+ return projects.filter((project) => {
111
+ if (project.fsModuleCache || project.fsModuleCacheProvided || project.transformTime < MIN_TRANSFORM_TIME || project.trackedTime <= 0 || project.transformTime / project.trackedTime < MIN_TRANSFORM_SHARE) return false;
112
+ return isSavingWorthHinting(project.transformTime, project.executionTime);
113
+ }).map((project) => ({
114
+ name: project.name,
115
+ transformTime: project.transformTime,
116
+ share: project.transformTime / project.trackedTime
117
+ }));
118
+ }
119
+ /**
120
+ * Detects projects where re-creating a DOM environment for every test file
121
+ * dominates the run. With an isolating pool the environment is set up once
122
+ * per file; `vmThreads`/`vmForks` set it up once per worker while still
123
+ * giving every file a fresh VM context.
124
+ * @internal
125
+ */
126
+ function getEnvironmentDiagnostics(projects) {
127
+ return projects.filter((project) => {
128
+ if (!DOM_ENVIRONMENTS.has(project.environment) || project.pool !== "forks" && project.pool !== "threads" || !project.isolate || project.browser || project.poolProvided || project.environmentCount <= 1 || project.environmentTime < MIN_ENVIRONMENT_TIME || project.trackedTime <= 0 || project.environmentTime / project.trackedTime < MIN_ENVIRONMENT_SHARE) return false;
129
+ const parallelism = Math.max(1, project.parallelism);
130
+ return isSavingWorthHinting(project.environmentTime / parallelism - project.environmentTime / project.environmentCount, project.executionTime);
131
+ }).map((project) => ({
132
+ name: project.name,
133
+ environment: project.environment,
134
+ environmentTime: project.environmentTime,
135
+ environmentCount: project.environmentCount,
136
+ share: project.environmentTime / project.trackedTime,
137
+ suggestIsolate: !project.isolateProvided
138
+ }));
139
+ }
140
+ /**
141
+ * Vitest's duration formatting (renderers/utils.ts `formatTime`).
142
+ * @internal
143
+ */
144
+ function formatTime(time) {
145
+ if (time > 1e3) return `${(time / 1e3).toFixed(2)}s`;
146
+ return `${Math.round(time)}ms`;
147
+ }
148
+ /**
149
+ * Surfaces the cost of `isolate: true`: with isolation enabled Vitest spawns a
150
+ * fresh worker (and re-creates the test environment) for every test file.
151
+ * Extracted from `BaseReporter.reportIsolateDiagnostic`.
152
+ * @internal
153
+ */
154
+ function getIsolateDiagnostic(input) {
155
+ const numWorkers = input.numWorkers;
156
+ if (!numWorkers || !input.isolates) return;
157
+ const startupTime = input.startupTime;
158
+ const avgStartup = startupTime / numWorkers;
159
+ const parallelism = Math.max(1, Math.min(input.numFiles, input.maxWorkers));
160
+ if (numWorkers <= parallelism) return;
161
+ const wallStartup = startupTime / parallelism;
162
+ const moduleSavings = input.measuresModules ? estimateModuleEvaluationSaving(input.moduleSelfTimes, parallelism) : 0;
163
+ const estimatedSaving = wallStartup - avgStartup + moduleSavings;
164
+ if (!isSavingWorthHinting(estimatedSaving, input.executionTime)) return;
165
+ return {
166
+ numWorkers,
167
+ avgStartup,
168
+ estimatedSaving,
169
+ measuresModules: input.measuresModules
170
+ };
171
+ }
172
+
173
+ export { estimateModuleEvaluationSaving, formatTime, getEnvironmentDiagnostics, getImportDiagnostics, getIsolateDiagnostic, getTransformDiagnostics, isSavingWorthHinting };
@@ -1,26 +1,11 @@
1
- import { readdir, stat } from "node:fs/promises";
2
-
3
1
  // src/utils/walker-fs.ts
4
- /**
5
- * {@link WalkerFileSystem} over `node:fs/promises` — the binding every
6
- * production call site uses, and the default when a walker is called without
7
- * an explicit port.
8
- * @public
9
- */
10
- const nodeWalkerFs = {
11
- readDirectory: async (dir) => await readdir(dir, { withFileTypes: true }),
12
- statEntry: async (path) => {
13
- try {
14
- const info = await stat(path);
15
- return {
16
- isFile: info.isFile(),
17
- isDirectory: info.isDirectory(),
18
- mtimeMs: info.mtimeMs
19
- };
20
- } catch {
21
- return null;
22
- }
2
+ /** `stat` of `path`, links followed, or `null` when it is missing or unreadable. */
3
+ function statOrNull(ops, path) {
4
+ try {
5
+ return ops.fileSystem.stat(path);
6
+ } catch {
7
+ return null;
23
8
  }
24
- };
9
+ }
25
10
 
26
- export { nodeWalkerFs };
11
+ export { statOrNull };
package/version.js CHANGED
@@ -8,6 +8,6 @@
8
8
  *
9
9
  * @public
10
10
  */
11
- const CURRENT_PLUGIN_VERSION = "5.6.2";
11
+ const CURRENT_PLUGIN_VERSION = "6.0.0";
12
12
 
13
13
  export { CURRENT_PLUGIN_VERSION };