@vitest-agent/plugin 5.1.2 → 5.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import "@effected/env";
1
2
  import { EnvironmentDetector, PlatformOptions, PlatformServices, SettingsInput, StackFrameInput } from "@vitest-agent/engine";
2
3
  import { AgentPluginOptions, AgentReporterOptions, ConsoleMode, CoverageBaselines, CoverageInput, CoverageLevel, CoverageLevelName, CoverageLevelName as CoverageLevelName$1, CoverageReport, OutputFormat, ResolvedReporterConfig, ResolvedThresholds, RunEvent, Transport, VitestAgentReporterFactory, resolveCoverageInput, validateCoverageConfig } from "@vitest-agent/sdk";
3
4
  import { Context, Effect, Layer, Option } from "effect";
@@ -693,6 +694,13 @@ export declare class AgentReporter {
693
694
  * @internal
694
695
  */
695
696
  private reporters;
697
+ /**
698
+ * The one close of the run-event channel and the reporters, started at
699
+ * Vitest's close (see `closeReporters`). Undefined until then.
700
+ *
701
+ * @internal
702
+ */
703
+ private closing;
696
704
  /**
697
705
  * Whether the user supplied a custom `reporter` factory. A custom
698
706
  * reporter may subscribe to the run-event channel in any console mode,
@@ -777,6 +785,18 @@ export declare class AgentReporter {
777
785
  * @param vitest - The Vitest instance
778
786
  */
779
787
  onInit(vitest: unknown): Promise<void>;
788
+ /**
789
+ * Close the reporters resolved at `onInit`, then end the run-event
790
+ * channel, once, at Vitest's close. Reporters first: a live view folds
791
+ * every event still queued in its subscription and commits its last
792
+ * frame in `close`, and `PubSub.shutdown` would drop whatever a
793
+ * subscriber has not taken yet. The shutdown after is harmless to a
794
+ * closed view and ends any other subscriber. A failing close is logged,
795
+ * never thrown — Vitest is shutting down.
796
+ *
797
+ * @internal
798
+ */
799
+ private closeReporters;
780
800
  /**
781
801
  * Resolve the reporter factory at run start.
782
802
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitest-agent/plugin",
3
- "version": "5.1.2",
3
+ "version": "5.2.1",
4
4
  "private": false,
5
5
  "description": "Vitest plugin for the vitest-agent ecosystem: owns persistence, classification, baselines, trends, and dispatches rendering to a configurable reporter.",
6
6
  "keywords": [
@@ -43,15 +43,18 @@
43
43
  "vitest-agent-mcp": "bin/vitest-agent-mcp.js"
44
44
  },
45
45
  "dependencies": {
46
- "@effect/platform-node": "4.0.0-rc.118",
47
- "@effected/glob": "^0.9.0",
48
- "@effected/workspaces": "^0.30.3",
49
- "@vitest-agent/cli": "3.2.7",
50
- "@vitest-agent/engine": "0.3.2",
51
- "@vitest-agent/mcp": "5.1.5",
52
- "@vitest-agent/reporter": "3.1.3",
53
- "@vitest-agent/sdk": "5.1.3",
54
- "effect": "4.0.0-rc.118",
46
+ "@effect/platform-node": "^4.0.0",
47
+ "@effected/cli": "^0.11.0",
48
+ "@effected/env": "^0.1.0",
49
+ "@effected/glob": "^0.10.0",
50
+ "@effected/walker": "^0.15.0",
51
+ "@effected/workspaces": "^0.31.0",
52
+ "@vitest-agent/cli": "4.0.1",
53
+ "@vitest-agent/engine": "0.5.0",
54
+ "@vitest-agent/mcp": "5.1.7",
55
+ "@vitest-agent/reporter": "4.0.1",
56
+ "@vitest-agent/sdk": "6.0.0",
57
+ "effect": "^4.0.0",
55
58
  "magic-string": "^1.4.2"
56
59
  },
57
60
  "peerDependencies": {
package/plugin.js CHANGED
@@ -20,9 +20,11 @@ import { execSync } from "node:child_process";
20
20
  import { mkdtempSync, rmSync } from "node:fs";
21
21
  import { tmpdir } from "node:os";
22
22
  import { join } from "node:path";
23
- import { EnvironmentDetector, EnvironmentDetectorLive, resolveLogLevel } from "@vitest-agent/engine";
23
+ import { CliLog } from "@effected/cli";
24
+ import { Audience, EnvOverride } from "@effected/env";
25
+ import { EnvironmentDetector, EnvironmentDetectorLive, ExecutorResolver, ExecutorResolverLive, LoggerLive, resolveLogLevel } from "@vitest-agent/engine";
24
26
  import { AgentConsoleMode, CiConsoleMode, CoverageLevel, HumanConsoleMode, SRC_DIR, TEST_DIR, formatFatalError, isTestFileName } from "@vitest-agent/sdk";
25
- import { Effect, Schema } from "effect";
27
+ import { Effect, Layer, Option } from "effect";
26
28
 
27
29
  //#region src/plugin.ts
28
30
  /**
@@ -41,29 +43,53 @@ import { Effect, Schema } from "effect";
41
43
  * the dedicated `ci-annotations` reporter is opt-in until the GHA
42
44
  * annotations writer ships).
43
45
  *
44
- * An invalid `VITEST_AGENT_CONSOLE` value is ignored with a diagnostic
45
- * line sent to `report` (default: `process.stderr`). `configureVitest`
46
- * passes the per-Vitest-instance dedupe sink so the line prints once per
47
- * run rather than once per project (issue #459).
46
+ * `VITEST_AGENT_CONSOLE` is read through `@effected/env`'s `EnvOverride`
47
+ * (case-insensitive, per-audience accepted literals), with the executor as
48
+ * the `Audience`. A value the executor does not accept is ignored with one
49
+ * `[vitest-agent:plugin]` diagnostic line, worded here from
50
+ * `EnvOverride.readResult`'s structured rejection and sent to `report`
51
+ * (default: `process.stderr`), never stdout. `configureVitest` passes the
52
+ * per-Vitest-instance dedupe sink so the line prints once per run rather than
53
+ * once per project (issue #459).
48
54
  *
49
55
  * @internal
50
56
  */
51
57
  function resolveConsoleMode(options, executor, _env, report = (line) => {
52
58
  process.stderr.write(line);
53
59
  }) {
54
- const override = process.env.VITEST_AGENT_CONSOLE;
55
- if (override !== void 0 && override !== "") {
56
- if (executor === "human" && Schema.is(HumanConsoleMode)(override)) return override;
57
- if (executor === "agent" && Schema.is(AgentConsoleMode)(override)) return override;
58
- if (executor !== "human" && executor !== "agent" && Schema.is(CiConsoleMode)(override)) return override;
59
- report(`[vitest-agent:plugin] ignoring invalid VITEST_AGENT_CONSOLE="${override}" for ${executor} executor; accepted for ${executor}: ${(executor === "human" ? HumanConsoleMode.literals : executor === "agent" ? AgentConsoleMode.literals : CiConsoleMode.literals).join(" | ")}\n`);
60
+ const { accepted, rejected } = Effect.runSync(readConsoleOverride.pipe(Effect.provideService(Audience, {
61
+ kind: executor,
62
+ source: "detected"
63
+ })));
64
+ if (Option.isSome(rejected)) {
65
+ const { value, audience, accepts } = rejected.value;
66
+ report(`[vitest-agent:plugin] ignoring VITEST_AGENT_CONSOLE=${value}: not accepted for the ${audience} audience (accepts ${accepts.join("|")})\n`);
60
67
  }
68
+ if (Option.isSome(accepted)) return accepted.value;
61
69
  const console = options.console;
62
70
  if (executor === "human") return console?.human ?? "passthrough";
63
71
  if (executor === "agent") return console?.agent ?? "agent";
64
72
  return console?.ci ?? "passthrough";
65
73
  }
66
74
  /**
75
+ * The `VITEST_AGENT_CONSOLE` override for the current `Audience`, without
76
+ * logging: `accepted` is the literal that audience accepts, `rejected` a set
77
+ * value it does not (with the value, the audience and its literals), both
78
+ * `None` when unset or empty. The accepted type is the union of every slot's
79
+ * literals, not the current audience's; the audience check is a runtime one.
80
+ *
81
+ * @internal
82
+ */
83
+ const readConsoleOverride = EnvOverride.readResult({
84
+ envVar: "VITEST_AGENT_CONSOLE",
85
+ source: process.env,
86
+ accepts: {
87
+ human: HumanConsoleMode.literals,
88
+ agent: AgentConsoleMode.literals,
89
+ ci: CiConsoleMode.literals
90
+ }
91
+ });
92
+ /**
67
93
  * The plugin owns stdout when the resolved console mode produces visible
68
94
  * output that would conflict with Vitest's own reporters. Passthrough lets
69
95
  * Vitest emit progress normally; silent strips everything; the other modes
@@ -143,19 +169,6 @@ const reportedConfigDiagnosticsByVitest = /* @__PURE__ */ new WeakMap();
143
169
  const TEST_FILE_DIR_RE = new RegExp(`/(?:${SRC_DIR}|${TEST_DIR})/`);
144
170
  const isTestFile = (id) => isTestFileName(id) && TEST_FILE_DIR_RE.test(id);
145
171
  /**
146
- * Map a detected {@link Environment} to its {@link Executor}. Inline copy
147
- * of the `ExecutorResolverLive` mapping so the plugin can compute it
148
- * synchronously inside `configureVitest` without spinning up an Effect
149
- * runtime.
150
- *
151
- * @internal
152
- */
153
- function envToExecutor(env) {
154
- if (env === "agent-shell") return "agent";
155
- if (env === "terminal") return "human";
156
- return "ci";
157
- }
158
- /**
159
172
  * Wraps `resolvedConfig.logger.warn` in place so the benign Vite
160
173
  * "Failed to load source map" / ENOENT `.js.map` noise (GitHub issue #110)
161
174
  * never reaches stdout, while every other warning still passes through
@@ -192,7 +205,10 @@ function installViteSourceMapWarningFilter(resolvedConfig) {
192
205
  function AgentPlugin(options = {}, _layer) {
193
206
  const layer = _layer ?? EnvironmentDetectorLive(process.env);
194
207
  const logLevel = resolveLogLevel(process.env);
195
- const log = logLevel !== void 0 && logLevel !== "None" ? (...args) => process.stderr.write(`[vitest-agent:plugin] ${args.map(String).join(" ")}\n`) : (..._args) => {};
208
+ const diagnostics = logLevel === "Debug" || logLevel === "Trace" || logLevel === "All" ? LoggerLive(logLevel, void 0, process.env) : void 0;
209
+ const log = diagnostics ? (...args) => {
210
+ Effect.runSync(Effect.logDebug(args.map(String).join(" ")).pipe(CliLog.component("vitest-agent:plugin"), Effect.provide(diagnostics)));
211
+ } : (..._args) => {};
196
212
  const discoverStrategyResolved = options.discoverStrategy === false ? null : options.discoverStrategy ?? new DefaultDiscoverStrategy();
197
213
  const classifyForCache = (id) => {
198
214
  if (!discoverStrategyResolved) return void 0;
@@ -216,8 +232,13 @@ function AgentPlugin(options = {}, _layer) {
216
232
  ctx.defineCacheKeyGenerator(makeTagCacheKeyGenerator(classifyForCache));
217
233
  log("registered fsModuleCache tag cache-key generator");
218
234
  }
219
- const env = await Effect.runPromise(Effect.provide(Effect.flatMap(EnvironmentDetector, (d) => d.detect()), layer));
220
- const executor = envToExecutor(env);
235
+ const { env, executor } = await Effect.runPromise(Effect.gen(function* () {
236
+ const env = yield* (yield* EnvironmentDetector).detect();
237
+ return {
238
+ env,
239
+ executor: yield* (yield* ExecutorResolver).resolve(env)
240
+ };
241
+ }).pipe(Effect.provide(Layer.merge(layer, ExecutorResolverLive))));
221
242
  const reportOption = options.report;
222
243
  const reportScope = reportOption === false ? void 0 : executor === "human" && reportOption === void 0 ? void 0 : reportOption?.scope ?? "vitest-agent";
223
244
  if (reportScope !== void 0) assertFlatScope(reportScope);
@@ -451,4 +472,4 @@ function makeDiscoverBuilder(options) {
451
472
  })(AgentPlugin || (AgentPlugin = {}));
452
473
 
453
474
  //#endregion
454
- export { AgentPlugin, CURRENT_PLUGIN_VERSION, makeDiscoverBuilder, resolveConsoleMode };
475
+ export { AgentPlugin, CURRENT_PLUGIN_VERSION, makeDiscoverBuilder, readConsoleOverride, resolveConsoleMode };
package/reporter.js CHANGED
@@ -7,6 +7,7 @@ import { captureSettings, hashSettings } from "./utils/capture-settings.js";
7
7
  import { isPartialRun } from "./utils/is-partial-run.js";
8
8
  import { processFailure } from "./utils/process-failure.js";
9
9
  import { assertReportCapable, createReportWriter } from "./utils/report-writer.js";
10
+ import { resolveNoColor } from "./utils/resolve-no-color.js";
10
11
  import { routeRenderedOutput } from "./utils/route-rendered-output.js";
11
12
  import { stringifyFailureValue } from "./utils/stringify-failure-value.js";
12
13
  import { mkdirSync, statSync } from "node:fs";
@@ -15,7 +16,7 @@ import { DataReader, DataStore, DetailResolver, EnvironmentDetector, ExecutorRes
15
16
  import { buildAgentReport, coerceErrorField, computeTrend, formatFatalError, isTimeoutError, probeHostMetadataFromEnv } from "@vitest-agent/sdk";
16
17
  import { Effect, Option, PubSub } from "effect";
17
18
  import { randomUUID } from "node:crypto";
18
- import { NodeServices } from "@effect/platform-node";
19
+ import * as NodeServices from "@effect/platform-node/NodeServices";
19
20
  import { DefaultVitestAgentReporter } from "@vitest-agent/reporter";
20
21
 
21
22
  //#region src/reporter.ts
@@ -339,6 +340,13 @@ var AgentReporter = class {
339
340
  */
340
341
  reporters;
341
342
  /**
343
+ * The one close of the run-event channel and the reporters, started at
344
+ * Vitest's close (see `closeReporters`). Undefined until then.
345
+ *
346
+ * @internal
347
+ */
348
+ closing;
349
+ /**
342
350
  * Whether the user supplied a custom `reporter` factory. A custom
343
351
  * reporter may subscribe to the run-event channel in any console mode,
344
352
  * so the streaming hooks publish events whenever this is true.
@@ -492,6 +500,36 @@ var AgentReporter = class {
492
500
  await this.ensureDbPath();
493
501
  } catch {}
494
502
  await this.initReporters();
503
+ const onClose = vitest?.onClose;
504
+ if (typeof onClose === "function") onClose.call(vitest, () => this.closeReporters());
505
+ }
506
+ /**
507
+ * Close the reporters resolved at `onInit`, then end the run-event
508
+ * channel, once, at Vitest's close. Reporters first: a live view folds
509
+ * every event still queued in its subscription and commits its last
510
+ * frame in `close`, and `PubSub.shutdown` would drop whatever a
511
+ * subscriber has not taken yet. The shutdown after is harmless to a
512
+ * closed view and ends any other subscriber. A failing close is logged,
513
+ * never thrown — Vitest is shutting down.
514
+ *
515
+ * @internal
516
+ */
517
+ closeReporters() {
518
+ this.closing ??= (async () => {
519
+ await Promise.all((this.reporters ?? []).map(async (reporter) => {
520
+ try {
521
+ await reporter.close?.();
522
+ } catch (err) {
523
+ process.stderr.write(`vitest-agent: reporter close failed: ${formatFatalError(err)}\n`);
524
+ }
525
+ }));
526
+ try {
527
+ Effect.runSync(PubSub.shutdown(this.runEvents));
528
+ } catch (err) {
529
+ process.stderr.write(`vitest-agent: run-event channel shutdown threw: ${formatFatalError(err)}\n`);
530
+ }
531
+ })();
532
+ return this.closing;
495
533
  }
496
534
  /**
497
535
  * Resolve the reporter factory at run start.
@@ -527,16 +565,17 @@ var AgentReporter = class {
527
565
  hasFailures: false,
528
566
  belowTargets: false,
529
567
  hasTargets: !!opts.coverageTargets
530
- }, opts.detail)
568
+ }, opts.detail),
569
+ noColor: yield* resolveNoColor
531
570
  };
532
571
  });
533
- const { env, executor, format, detail } = await Effect.runPromise(initProgram.pipe(Effect.provide(OutputPipelineLive(process.env)), Effect.provide(NodeServices.layer)));
572
+ const { env, executor, format, detail, noColor } = await Effect.runPromise(initProgram.pipe(Effect.provide(OutputPipelineLive(process.env)), Effect.provide(NodeServices.layer)));
534
573
  const kit = buildReporterKit({
535
574
  env,
536
575
  executor,
537
576
  format,
538
577
  detail,
539
- noColor: !!process.env.NO_COLOR,
578
+ noColor,
540
579
  consoleMode: opts.consoleMode ?? "passthrough",
541
580
  mcp: opts.mcp ?? false,
542
581
  githubActions: opts.githubActions,
@@ -1139,7 +1178,7 @@ var AgentReporter = class {
1139
1178
  executor,
1140
1179
  format,
1141
1180
  detail,
1142
- noColor: !!process.env.NO_COLOR,
1181
+ noColor: yield* resolveNoColor,
1143
1182
  consoleMode: opts.consoleMode ?? "passthrough",
1144
1183
  mcp: opts.mcp ?? false,
1145
1184
  githubActions: opts.githubActions,
@@ -1191,7 +1230,7 @@ var AgentReporter = class {
1191
1230
  return;
1192
1231
  }
1193
1232
  if (dbPath !== void 0 && persistDisabled === void 0) try {
1194
- await ensureMigrated(dbPath, logLevel, logFile);
1233
+ await ensureMigrated(dbPath, logLevel, logFile, process.env);
1195
1234
  } catch (err) {
1196
1235
  persistDisabled = formatFatalError(err);
1197
1236
  }
@@ -1691,7 +1730,7 @@ var AgentReporter = class {
1691
1730
  executor,
1692
1731
  format,
1693
1732
  detail,
1694
- noColor: !!process.env.NO_COLOR,
1733
+ noColor: yield* resolveNoColor,
1695
1734
  consoleMode: opts.consoleMode ?? "passthrough",
1696
1735
  mcp: opts.mcp ?? false,
1697
1736
  githubActions: opts.githubActions,
@@ -2,7 +2,9 @@ import { nodeWalkerFs } from "./walker-fs.js";
2
2
  import { findTestFiles } from "./find-test-files.js";
3
3
  import { Tag } from "./tag.js";
4
4
  import { join, sep } from "node:path";
5
+ import { RuntimeEnv } from "@effected/env";
5
6
  import { NON_DISCOVERABLE_DIRS, SRC_DIR, TEST_DIR, TEST_FILE_GLOB_SUFFIX, TEST_HELPER_DIRS } from "@vitest-agent/sdk";
7
+ import { Option } from "effect";
6
8
  import { configDefaults } from "vitest/config";
7
9
 
8
10
  //#region src/utils/discover-strategy.ts
@@ -80,12 +82,20 @@ var ConcreteDiscoverStrategy = class ConcreteDiscoverStrategy extends DiscoverSt
80
82
  return new ConcreteDiscoverStrategy(newTags, newClassifyLayers, newBuildProjectLayers);
81
83
  }
82
84
  };
83
- const DEFAULT_TAGS = [
85
+ /**
86
+ * Whether the process runs in CI, by `@effected/env`'s `RuntimeEnv` rule: a
87
+ * truthy `GITHUB_ACTIONS`, or a `CI` / `CONTINUOUS_INTEGRATION` that is set,
88
+ * non-empty, and not `false` / `0` (is-in-ci semantics). Read when a strategy is
89
+ * constructed rather than at module load, so the environment in force when
90
+ * `AgentPlugin.discover()` runs is the one that decides.
91
+ */
92
+ const runsInCi = () => Option.isSome(RuntimeEnv.fromRecord(process.env).ci);
93
+ const defaultTags = (inCi) => [
84
94
  Tag.make("unit"),
85
95
  Tag.make("int", { timeout: 6e4 }),
86
96
  Tag.make("e2e", {
87
97
  timeout: 12e4,
88
- retry: process.env.CI ? 2 : 0
98
+ retry: inCi ? 2 : 0
89
99
  })
90
100
  ];
91
101
  const E2E_RE = /\.e2e\.(test|spec)\.(ts|tsx|js|jsx)$/;
@@ -98,7 +108,7 @@ const INT_RE = /\.int\.(test|spec)\.(ts|tsx|js|jsx)$/;
98
108
  * @public
99
109
  */
100
110
  var DefaultDiscoverStrategy = class extends DiscoverStrategy {
101
- tags = DEFAULT_TAGS;
111
+ tags = defaultTags(runsInCi());
102
112
  get tagDefinitions() {
103
113
  return this.tags.map((t) => t.definition);
104
114
  }
@@ -0,0 +1,13 @@
1
+ import { TerminalEnv } from "@effected/env";
2
+ import { Effect } from "effect";
3
+
4
+ //#region src/utils/resolve-no-color.ts
5
+ /**
6
+ * Whether the reporter should render without colour, decided by the kit's
7
+ * terminal rule rather than `NO_COLOR` alone: `FORCE_COLOR` beats `NO_COLOR`,
8
+ * and `NODE_DISABLE_COLORS`, `TERM=dumb` and a non-TTY stdout all disable it.
9
+ */
10
+ const resolveNoColor = TerminalEnv.colorLevel("stdout").pipe(Effect.map((level) => level === "none"));
11
+
12
+ //#endregion
13
+ export { resolveNoColor };
package/version.js CHANGED
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * @public
10
10
  */
11
- const CURRENT_PLUGIN_VERSION = "5.1.2";
11
+ const CURRENT_PLUGIN_VERSION = "5.2.1";
12
12
 
13
13
  //#endregion
14
14
  export { CURRENT_PLUGIN_VERSION };