@vitest-agent/reporter 4.1.1 → 5.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,8 @@
1
1
  import { renderGithubLog, toDisplayPath } from "./githubLog.js";
2
2
  import { LiveViewEnv, startLiveView } from "./liveView.js";
3
3
  import { Doc, Render, Status } from "@effected/cli";
4
- import { CliUi, UiProvider } from "@effected/cli/ui";
5
4
  import { RUN_REPORT_FILE_SCHEMA_URL, countSuiteFailures, isTimeoutError } from "@vitest-agent/sdk";
6
- import { classifyOutcome, classifyRunShape, dispatch, dispatcherTable, reduceRenderStateAll, synthesizeFromAgentReport } from "@vitest-agent/ui";
7
- import { Effect } from "effect";
8
- import { createElement } from "react";
5
+ import { classifyOutcome, classifyRunShape, dispatch, reduceRenderStateAll, synthesizeFromAgentReport } from "@vitest-agent/ui";
9
6
 
10
7
  //#region src/defaultReporter.ts
11
8
  const countTimeouts = (report) => {
@@ -128,10 +125,21 @@ const renderAgentStringForReport = (report) => {
128
125
  * Returns the agent-string fallback when the matched cell has no Ink
129
126
  * half.
130
127
  *
128
+ * The output is laid out at `options.width` when given, else at the
129
+ * terminal's width (stdout's columns, else the `COLUMNS` variable), else at
130
+ * 80 columns, which is what a non-TTY run gets. React and Ink are loaded on
131
+ * the first call, never by importing this package.
132
+ *
131
133
  * @public
132
134
  */
133
- const renderHumanStringForReport = async (report, options = {}) => {
134
- const { renderToString } = await import("ink");
135
+ const renderHumanStringForReport = (report, options = {}) => renderHumanString(report, options, LiveViewEnv);
136
+ /**
137
+ * {@link renderHumanStringForReport} over an explicit environment, which
138
+ * decides the theme, the glyphs and the terminal width.
139
+ *
140
+ * @internal
141
+ */
142
+ const renderHumanString = async (report, options, env) => {
135
143
  const events = synthesizeFromAgentReport(report);
136
144
  const state = reduceRenderStateAll(events);
137
145
  const projects = [summarizeProject(report)];
@@ -148,11 +156,8 @@ const renderHumanStringForReport = async (report, options = {}) => {
148
156
  noColor: false,
149
157
  osc8: (_url, label) => label
150
158
  };
151
- const cell = dispatcherTable[inputs.shape][inputs.outcome];
152
- if (cell.ink === void 0) return dispatch(inputs, opts);
153
- const columns = options.width ?? 80;
154
- const context = await Effect.runPromise(CliUi.context.pipe(Effect.provide(LiveViewEnv)));
155
- return renderToString(createElement(UiProvider, { value: context }, cell.ink(inputs, opts)), { columns });
159
+ const { renderInkReport } = await import("./humanReport.js");
160
+ return await renderInkReport(inputs, opts, env, options.width) ?? dispatch(inputs, opts);
156
161
  };
157
162
  const NON_STABLE_SUMMARY_CLASSIFICATIONS = [
158
163
  "new-failure",
@@ -329,7 +334,10 @@ const renderGithubSummary = (input) => {
329
334
  const DefaultVitestAgentReporter = (kit) => {
330
335
  const live = kit.config.consoleMode === "stream" && kit.runEvents !== void 0 ? startLiveView(kit.runEvents) : void 0;
331
336
  return {
332
- ...live !== void 0 ? { close: live.close } : {},
337
+ ...live !== void 0 ? {
338
+ close: live.close,
339
+ printStrayLine: live.printStrayLine
340
+ } : {},
333
341
  render(input, renderKit) {
334
342
  const out = [];
335
343
  if (shouldRenderForMode(renderKit.config.consoleMode)) {
@@ -371,4 +379,4 @@ const DefaultVitestAgentReporter = (kit) => {
371
379
  };
372
380
 
373
381
  //#endregion
374
- export { DefaultVitestAgentReporter, buildDispatchInputs, renderAgentStringForReport, renderHumanStringForReport, resolveCellOptions };
382
+ export { DefaultVitestAgentReporter, buildDispatchInputs, renderAgentStringForReport, renderHumanString, renderHumanStringForReport, resolveCellOptions };
package/humanReport.js ADDED
@@ -0,0 +1,36 @@
1
+ import { CliUi, UiProvider } from "@effected/cli/ui";
2
+ import { Effect } from "effect";
3
+ import { TerminalEnv } from "@effected/env";
4
+ import { dispatchInk } from "@vitest-agent/ui/ink";
5
+ import { renderToString } from "ink";
6
+ import { createElement } from "react";
7
+
8
+ //#region src/humanReport.ts
9
+ /** The width a report is laid out at when no terminal width is known. */
10
+ const FALLBACK_COLUMNS = 80;
11
+ /**
12
+ * Render the matched cell's Ink half to a string, or `null` when the cell
13
+ * has no Ink half (the caller falls back to the agent string).
14
+ *
15
+ * The layout width is `width` when given, else the terminal's
16
+ * (`TerminalEnv.width`: stdout's columns, else `COLUMNS`, else 80 — so a
17
+ * non-TTY run lays out at 80).
18
+ *
19
+ * @internal
20
+ */
21
+ const renderInkReport = async (inputs, opts, env, width) => {
22
+ const element = dispatchInk(inputs, opts);
23
+ if (element === null) return null;
24
+ const { context, columns } = await Effect.runPromise(Effect.gen(function* () {
25
+ const context = yield* CliUi.context;
26
+ const terminal = yield* TerminalEnv;
27
+ return {
28
+ context,
29
+ columns: width ?? terminal.width(FALLBACK_COLUMNS)
30
+ };
31
+ }).pipe(Effect.provide(env)));
32
+ return renderToString(createElement(UiProvider, { value: context }, element), { columns });
33
+ };
34
+
35
+ //#endregion
36
+ export { renderInkReport };
package/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import { PubSub, Schema } from "effect";
2
+ import "@effected/cli";
3
+ import "@effected/env";
2
4
  //#region ../sdk/dist/dev/pkg/index.d.ts
3
5
  /**
4
6
  * Per-file coverage data including uncovered line ranges.
@@ -171,6 +173,21 @@ declare const RenderState: Schema.Struct<{
171
173
  readonly expected: Schema.optional<Schema.String>;
172
174
  readonly received: Schema.optional<Schema.String>;
173
175
  }>>;
176
+ /**
177
+ * What the run wrote straight to the terminal, past Vitest's console
178
+ * capture, folded from `RunFinished.strayOutput`. Absent when the run
179
+ * wrote nothing stray.
180
+ */
181
+ readonly strayOutput: Schema.optional<Schema.Struct<{
182
+ readonly total: Schema.Number;
183
+ readonly stdout: Schema.Number;
184
+ readonly stderr: Schema.Number;
185
+ readonly bytes: Schema.Number;
186
+ readonly samples: Schema.$Array<Schema.Struct<{
187
+ readonly stream: Schema.Literals<readonly ["stdout", "stderr"]>;
188
+ readonly text: Schema.String;
189
+ }>>;
190
+ }>>;
174
191
  }>;
175
192
  /** @public */
176
193
  type RenderState = typeof RenderState.Type;
@@ -509,6 +526,16 @@ declare const AgentReport: Schema.Struct<{
509
526
  readonly files: Schema.Number;
510
527
  }>>;
511
528
  }>>;
529
+ readonly strayOutput: Schema.optional<Schema.Struct<{
530
+ readonly total: Schema.Number;
531
+ readonly stdout: Schema.Number;
532
+ readonly stderr: Schema.Number;
533
+ readonly bytes: Schema.Number;
534
+ readonly samples: Schema.$Array<Schema.Struct<{
535
+ readonly stream: Schema.Literals<readonly ["stdout", "stderr"]>;
536
+ readonly text: Schema.String;
537
+ }>>;
538
+ }>>;
512
539
  }>;
513
540
  /** @public */
514
541
  type AgentReport = typeof AgentReport.Type;
@@ -541,13 +568,6 @@ type Environment = typeof Environment.Type;
541
568
  declare const Executor: Schema.Literals<readonly ["human", "agent", "ci"]>;
542
569
  /** @public */
543
570
  type Executor = typeof Executor.Type;
544
- /**
545
- * Output format for the reporter pipeline.
546
- * @public
547
- */
548
- declare const OutputFormat: Schema.Literals<readonly ["terminal", "markdown", "json", "vitest-bypass", "silent", "ci-annotations"]>;
549
- /** @public */
550
- type OutputFormat = typeof OutputFormat.Type;
551
571
  /**
552
572
  * Level of detail in reporter output.
553
573
  * @public
@@ -767,6 +787,21 @@ declare const RunEvent: Schema.Union<readonly [Schema.TaggedStruct<"RunStarted",
767
787
  readonly expected: Schema.optional<Schema.String>;
768
788
  readonly received: Schema.optional<Schema.String>;
769
789
  }>>>;
790
+ /**
791
+ * What the run wrote straight to the terminal, past Vitest's console
792
+ * capture (`AgentReport.strayOutput`). Optional: absent when the run
793
+ * wrote nothing stray, and in older replay data.
794
+ */
795
+ readonly strayOutput: Schema.optional<Schema.Struct<{
796
+ readonly total: Schema.Number;
797
+ readonly stdout: Schema.Number;
798
+ readonly stderr: Schema.Number;
799
+ readonly bytes: Schema.Number;
800
+ readonly samples: Schema.$Array<Schema.Struct<{
801
+ readonly stream: Schema.Literals<readonly ["stdout", "stderr"]>;
802
+ readonly text: Schema.String;
803
+ }>>;
804
+ }>>;
770
805
  }>]>;
771
806
  /** @public */
772
807
  type RunEvent = typeof RunEvent.Type;
@@ -829,10 +864,10 @@ type Transport = typeof Transport.Type;
829
864
  * plugin always populates it in practice — the option exists for clarity
830
865
  * and to leave room for future "no-persistence" experiments.
831
866
  *
832
- * `format` and `detail` are pre-resolved by the plugin (via
833
- * `FormatSelector` / `DetailResolver`) but reporters that want to override
834
- * can ignore them. `noColor` is the resolved value of the `NO_COLOR` env
835
- * var; reporters use it to gate ANSI escapes and OSC-8 hyperlinks.
867
+ * `detail` is pre-resolved by the plugin (via `DetailResolver`) but
868
+ * reporters that want to override can ignore it. `noColor` is the resolved
869
+ * value of the `NO_COLOR` env var; reporters use it to gate ANSI escapes and
870
+ * OSC-8 hyperlinks.
836
871
  * @public
837
872
  */
838
873
  interface ResolvedReporterConfig {
@@ -871,7 +906,6 @@ interface ResolvedReporterConfig {
871
906
  * @internal
872
907
  */
873
908
  readonly coverageMode: "full" | "ui-only";
874
- readonly format: OutputFormat;
875
909
  readonly detail: DetailLevel;
876
910
  readonly noColor: boolean;
877
911
  readonly runCommand?: string;
@@ -993,6 +1027,24 @@ interface VitestAgentReporter {
993
1027
  * awaits it and treats a rejection as a logged warning.
994
1028
  */
995
1029
  readonly close?: () => Promise<void>;
1030
+ /**
1031
+ * Print one whole line of stray output: bytes a test process wrote
1032
+ * straight to the terminal, past Vitest's console capture (a child
1033
+ * process spawned with inherited stdio is the usual source). A reporter
1034
+ * that draws in place (a live view) implements this to print the line
1035
+ * above its drawing instead of under it, where the next redraw would
1036
+ * strand it. Return `true` when the line was taken; `false` sends it to
1037
+ * the terminal unchanged.
1038
+ *
1039
+ * The plugin calls it synchronously, from the factory's return until just
1040
+ * before {@link VitestAgentReporter.close | close}, only for lines bound
1041
+ * for the process's own stdout or stderr (never when Vitest was given
1042
+ * other streams, as MCP's `run_tests` does), and only while it owns the
1043
+ * console (any mode but `passthrough`). The line carries no trailing
1044
+ * newline. Optional; without it every line passes through unchanged. The
1045
+ * run's stray output is reported on `AgentReport.strayOutput` either way.
1046
+ */
1047
+ readonly printStrayLine?: (stream: "stdout" | "stderr", line: string) => boolean;
996
1048
  }
997
1049
  /**
998
1050
  * Factory that the plugin's `reporter` option accepts. The plugin calls
@@ -1056,6 +1108,11 @@ export declare const renderAgentStringForReport: (report: AgentReport) => string
1056
1108
  * Returns the agent-string fallback when the matched cell has no Ink
1057
1109
  * half.
1058
1110
  *
1111
+ * The output is laid out at `options.width` when given, else at the
1112
+ * terminal's width (stdout's columns, else the `COLUMNS` variable), else at
1113
+ * 80 columns, which is what a non-TTY run gets. React and Ink are loaded on
1114
+ * the first call, never by importing this package.
1115
+ *
1059
1116
  * @public
1060
1117
  */
1061
1118
  export declare const renderHumanStringForReport: (report: AgentReport, options?: {
@@ -1097,5 +1154,5 @@ export declare const DefaultVitestAgentReporter: VitestAgentReporterFactory;
1097
1154
  */
1098
1155
  export declare const CURRENT_REPORTER_VERSION: string;
1099
1156
  //#endregion
1100
- export type { AgentReport, CellOptions, ConsoleMode, DetailLevel, DispatchInputs, Environment, Executor, FileCoverageReport, OutputFormat, ProjectSummary, RenderState, RenderedOutput, ReporterKit, ReporterRenderInput, ResolvedReporterConfig, ResolvedThresholds, RunEvent, RunOutcome, RunShape, TestClassification, Transport, TrendSummary, VitestAgentReporter, VitestAgentReporterFactory };
1157
+ export type { AgentReport, CellOptions, ConsoleMode, DetailLevel, DispatchInputs, Environment, Executor, FileCoverageReport, ProjectSummary, RenderState, RenderedOutput, ReporterKit, ReporterRenderInput, ResolvedReporterConfig, ResolvedThresholds, RunEvent, RunOutcome, RunShape, TestClassification, Transport, TrendSummary, VitestAgentReporter, VitestAgentReporterFactory };
1101
1158
  //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -8,7 +8,7 @@ import { DefaultVitestAgentReporter, buildDispatchInputs, renderAgentStringForRe
8
8
  *
9
9
  * @public
10
10
  */
11
- const CURRENT_REPORTER_VERSION = "4.1.1";
11
+ const CURRENT_REPORTER_VERSION = "5.0.1";
12
12
 
13
13
  //#endregion
14
14
  export { CURRENT_REPORTER_VERSION, DefaultVitestAgentReporter, buildDispatchInputs, renderAgentStringForReport, renderHumanStringForReport, resolveCellOptions };
package/liveView.js CHANGED
@@ -1,17 +1,22 @@
1
1
  import { CliEnv, Doc } from "@effected/cli";
2
- import { CliUi } from "@effected/cli/ui";
3
2
  import { initialRenderState } from "@vitest-agent/sdk";
4
- import { SPINNER_FRAME_MS, StreamApp, reduceRenderState, renderAgent } from "@vitest-agent/ui";
5
- import { Cause, Deferred, Effect, Exit, Layer, PubSub, Scope } from "effect";
6
- import { createElement } from "react";
3
+ import { SPINNER_FRAME_MS, reduceRenderState, renderAgent } from "@vitest-agent/ui";
7
4
  import * as NodeServices from "@effect/platform-node/NodeServices";
5
+ import { CliUi } from "@effected/cli/ui";
6
+ import { Cause, Deferred, Effect, Exit, Layer, PubSub, Scope } from "effect";
8
7
 
9
8
  //#region src/liveView.ts
10
9
  /**
11
10
  * The `stream` console mode's live view: the plugin's run-event channel
12
- * folded through `reduceRenderState` and drawn as {@link StreamApp} by the
11
+ * folded through `reduceRenderState` and drawn as `StreamApp` by the
13
12
  * kit's `CliUi.live`.
14
13
  *
14
+ * The drawing lives in `streamView.ts`, loaded through `CliUi.lazyView`, so
15
+ * this module (and everything that imports it) loads neither React nor Ink:
16
+ * the view module loads only when a run first mounts its Ink frame, and an
17
+ * agent, CI or piped run, which prints the `final` document instead, never
18
+ * loads it.
19
+ *
15
20
  * The kit owns the mount: a run mounts at `RunStarted` (or where `begins`
16
21
  * says), redraws on every event and on an 80 ms tick, and commits its final
17
22
  * frame to scrollback at the terminal event (`RunFinished` /
@@ -45,11 +50,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices";
45
50
  const liveViewOptions = {
46
51
  initial: initialRenderState,
47
52
  reduce: reduceRenderState,
48
- render: (state, frame) => createElement(StreamApp, {
49
- state,
50
- frameIndex: frame,
51
- nowMs: frame * SPINNER_FRAME_MS
52
- }),
53
+ render: CliUi.lazyView(() => import("./streamView.js")),
53
54
  isStart: (event) => event._tag === "RunStarted",
54
55
  isTerminal: (event) => event._tag === "RunFinished" || event._tag === "RunTimedOut",
55
56
  begins: (event, before, after) => event._tag === "RunStarted" || before.phase === "idle" && after.phase !== "idle",
@@ -60,13 +61,27 @@ const liveViewOptions = {
60
61
  /**
61
62
  * The environment a Vitest-hosted live view reads: the kit's `CliEnv` over
62
63
  * Node's stdio and terminal. It builds `CliTheme` (colour from the terminal,
63
- * glyphs `auto`, so `TERM=dumb` draws ASCII) and sets `CliInteractive` (a
64
+ * glyphs `auto`, so `TERM=dumb` draws ASCII) and `TerminalEnv` (the width
65
+ * the report-time human render lays out at), and sets `CliInteractive` (a
64
66
  * human audience with a TTY on stdin and stdout).
65
67
  *
66
68
  * @internal
67
69
  */
68
70
  const LiveViewEnv = CliEnv.layer().pipe(Layer.provide(NodeServices.layer));
69
71
  /**
72
+ * Print one line of stray output (bytes a test process wrote straight to the
73
+ * terminal) through the view's `logConsole`: above the frame while a run is
74
+ * drawn, straight to the stream otherwise, so it never lands under the frame
75
+ * where the next redraw would strand it.
76
+ *
77
+ * @internal
78
+ */
79
+ const printAbove = (handle) => (stream, line) => {
80
+ if (stream === "stderr") handle.logConsole.error(line);
81
+ else handle.logConsole.log(line);
82
+ return true;
83
+ };
84
+ /**
70
85
  * Start a live view over `channel`. The subscription is taken before this
71
86
  * returns, so an event published right after is seen.
72
87
  *
@@ -86,9 +101,16 @@ const startLiveView = (channel, env = LiveViewEnv) => {
86
101
  }).pipe(Effect.onExit((exit) => Deferred.done(mounted, exit)), Effect.flatMap((handle) => handle.done), Scope.provide(scope), Effect.provide(env));
87
102
  Effect.runFork(program);
88
103
  let closing;
104
+ let print;
105
+ const handle = Effect.runPromise(Deferred.await(mounted));
106
+ handle.then((mountedHandle) => {
107
+ if (closing === void 0) print = printAbove(mountedHandle);
108
+ }, () => void 0);
89
109
  return {
90
- handle: Effect.runPromise(Deferred.await(mounted)),
110
+ handle,
111
+ printStrayLine: (stream, line) => print?.(stream, line) ?? false,
91
112
  close: () => {
113
+ print = void 0;
92
114
  closing ??= Effect.runPromise(Deferred.await(mounted).pipe(Effect.flatMap((handle) => handle.close), Effect.catchCause((cause) => Cause.hasInterruptsOnly(cause) ? Effect.void : Effect.logWarning("vitest-agent: the live view ended with an error", cause)), Effect.ensuring(Scope.close(scope, Exit.void))));
93
115
  return closing;
94
116
  }
@@ -96,4 +118,4 @@ const startLiveView = (channel, env = LiveViewEnv) => {
96
118
  };
97
119
 
98
120
  //#endregion
99
- export { LiveViewEnv, liveViewOptions, startLiveView };
121
+ export { LiveViewEnv, liveViewOptions, printAbove, startLiveView };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitest-agent/reporter",
3
- "version": "4.1.1",
3
+ "version": "5.0.1",
4
4
  "private": false,
5
5
  "description": "Named VitestAgentReporterFactory implementations for the vitest-agent ecosystem.",
6
6
  "keywords": [
@@ -39,15 +39,15 @@
39
39
  "./package.json": "./package.json"
40
40
  },
41
41
  "dependencies": {
42
- "@effect/platform-node": "^4.0.0",
43
- "@vitest-agent/sdk": "6.1.0",
44
- "@vitest-agent/ui": "4.1.0",
45
- "effect": "^4.0.0",
46
- "ink": "^7.1.1",
42
+ "@effect/platform-node": "^4.0.2",
43
+ "@vitest-agent/sdk": "7.0.0",
44
+ "@vitest-agent/ui": "5.0.1",
45
+ "effect": "^4.0.2",
46
+ "ink": "^8.0.0",
47
47
  "react": "^19.3.0"
48
48
  },
49
49
  "peerDependencies": {
50
- "@effected/cli": "^0.12.0",
50
+ "@effected/cli": "^0.16.0",
51
51
  "@effected/env": "^0.1.0",
52
52
  "@effected/glob": "^0.10.0",
53
53
  "@effected/walker": "^0.15.0",
package/streamView.js ADDED
@@ -0,0 +1,13 @@
1
+ import { SPINNER_FRAME_MS } from "@vitest-agent/ui";
2
+ import { StreamApp } from "@vitest-agent/ui/ink";
3
+ import { createElement } from "react";
4
+
5
+ //#region src/streamView.ts
6
+ const streamView = (state, frame) => createElement(StreamApp, {
7
+ state,
8
+ frameIndex: frame,
9
+ nowMs: frame * SPINNER_FRAME_MS
10
+ });
11
+
12
+ //#endregion
13
+ export { streamView as default };