@vitest-agent/sdk 5.2.0 → 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.
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  > **Part of the [vitest-agent](https://vitest-agent.dev) ecosystem.** Most users want **[@vitest-agent/plugin](https://www.npmjs.com/package/@vitest-agent/plugin)**, which pulls this package in automatically. Install `@vitest-agent/sdk` directly only if you build tooling on the shared schemas or contract types.
8
8
 
9
- The platform-free core of the vitest-agent ecosystem. Carries the Effect Schemas, the public reporter and dispatcher contract types, the tagged errors, the pure formatters and utilities, the pure sidecar `dispatch` entry, and the published JSON Schemas. It has no workspace dependencies and never imports `node:*`, so it runs anywhere Effect does.
9
+ The platform-free core of the vitest-agent ecosystem. Carries the Effect Schemas, the public reporter and dispatcher contract types, the tagged errors, the pure utilities, the pure sidecar `dispatch` entry, and the published JSON Schemas. It has no workspace dependencies and never imports `node:*`, so it runs anywhere Effect does.
10
10
 
11
11
  Everything that touches a filesystem, a process, or SQLite — the `DataStore` / `DataReader` services and their live layers, migrations, `ensureMigrated`, `PlatformLive`, `resolveDataPath`, and the test-layer helpers — lives in **[@vitest-agent/engine](https://www.npmjs.com/package/@vitest-agent/engine)**.
12
12
 
@@ -15,7 +15,7 @@ Everything that touches a filesystem, a process, or SQLite — the `DataStore` /
15
15
  - **Effect Schemas** — all domain types (`AgentReport`, `RunEvent`, `RenderState`, `CoverageTargets`, `TurnPayload`, identity types) defined with Effect Schema; runtime validation and TypeScript types from one source
16
16
  - **Reporter and dispatcher contracts** — `VitestAgentReporterFactory`, `ReporterKit`, `ResolvedReporterConfig`, `DispatchInputs` and the types consumed by every other package
17
17
  - **Tagged errors** — `Data.TaggedError` families for data-store, discovery, path-resolution, project-identity, run-context, TDD and agent failures
18
- - **Pure formatters and utilities** — terminal, markdown, GFM, JSON and CI-annotation formatters, plus `classifyTestPath`, `buildAgentReport`, `validatePhaseTransition` and the posix path helpers
18
+ - **Pure utilities** — `classifyTestPath`, `buildAgentReport`, `validatePhaseTransition` and the posix path helpers
19
19
  - **Sidecar dispatch core** — `dispatch`, `injectEnv` and `exitCodeForTag` on the `@vitest-agent/sdk/dispatch` sub-path for a minimal SEA bundle
20
20
  - **JSON Schemas** — generated schemas on the `@vitest-agent/sdk/schemas/*.json` sub-path (for example the run-report file schema)
21
21
 
package/index.d.ts CHANGED
@@ -552,6 +552,31 @@ export interface CellOptions {
552
552
  readonly osc8: (url: string, label: string) => string;
553
553
  }
554
554
  //#endregion
555
+ //#region src/formatters/types.d.ts
556
+ /**
557
+ * One unit of rendered reporter output, tagged by where it goes.
558
+ *
559
+ * `stdout` writes to the process stdout stream, `github-summary`
560
+ * appends to the GitHub Actions step-summary file, and `file` is
561
+ * reserved for reporters that embed their own destination path.
562
+ *
563
+ * `report` writes `filename` into `.vitest/<scope>/` through Vitest 5's
564
+ * `createReport`. The plugin drops these when reporting is disabled,
565
+ * exactly as it drops `github-summary` outside GitHub Actions.
566
+ *
567
+ * @public
568
+ */
569
+ export type RenderedOutput = {
570
+ readonly target: "stdout" | "file" | "github-summary";
571
+ readonly content: string;
572
+ readonly contentType: string;
573
+ } | {
574
+ readonly target: "report";
575
+ readonly filename: string;
576
+ readonly content: string;
577
+ readonly contentType: string;
578
+ };
579
+ //#endregion
555
580
  //#region src/schemas/AgentReport.d.ts
556
581
  /**
557
582
  * Aggregate test run statistics.
@@ -940,61 +965,6 @@ export declare const ReportError: Schema.Struct<{
940
965
  /** @public */
941
966
  export type ReportError = typeof ReportError.Type;
942
967
  //#endregion
943
- //#region src/formatters/types.d.ts
944
- /**
945
- * One unit of rendered reporter output, tagged by where it goes.
946
- *
947
- * `stdout` writes to the process stdout stream, `github-summary`
948
- * appends to the GitHub Actions step-summary file, and `file` is
949
- * reserved for reporters that embed their own destination path.
950
- *
951
- * `report` writes `filename` into `.vitest/<scope>/` through Vitest 5's
952
- * `createReport`. The plugin drops these when reporting is disabled,
953
- * exactly as it drops `github-summary` outside GitHub Actions.
954
- *
955
- * @public
956
- */
957
- export type RenderedOutput = {
958
- readonly target: "stdout" | "file" | "github-summary";
959
- readonly content: string;
960
- readonly contentType: string;
961
- } | {
962
- readonly target: "report";
963
- readonly filename: string;
964
- readonly content: string;
965
- readonly contentType: string;
966
- };
967
- /** @public */
968
- export interface FormatterContext {
969
- readonly detail: DetailLevel;
970
- /**
971
- * Absolute project root the run executed from. Formatters strip it from
972
- * absolute file paths for display and resolve relative paths back against
973
- * it for `file://` hyperlinks — the core never reads `process.cwd()`.
974
- */
975
- readonly cwd: string;
976
- readonly noColor: boolean;
977
- readonly coverageConsoleLimit: number;
978
- readonly trendSummary?: {
979
- direction: "improving" | "regressing" | "stable";
980
- runCount: number;
981
- firstMetric?: {
982
- name: string;
983
- from: number;
984
- to: number;
985
- target?: number;
986
- };
987
- };
988
- readonly runCommand?: string;
989
- readonly githubSummaryFile?: string;
990
- readonly mcp?: boolean;
991
- }
992
- /** @public */
993
- export interface Formatter {
994
- readonly format: string;
995
- readonly render: (reports: ReadonlyArray<AgentReport>, context: FormatterContext) => ReadonlyArray<RenderedOutput>;
996
- }
997
- //#endregion
998
968
  //#region src/schemas/RunEvent.d.ts
999
969
  /**
1000
970
  * Coverage metric whose threshold can be violated independently.
@@ -1955,40 +1925,6 @@ export declare class IllegalStatusTransitionError extends IllegalStatusTransitio
1955
1925
  });
1956
1926
  }
1957
1927
  //#endregion
1958
- //#region src/formatters/ci-annotations.d.ts
1959
- /** @public */
1960
- export declare const ciAnnotationsFormatter: Formatter;
1961
- //#endregion
1962
- //#region src/formatters/gfm.d.ts
1963
- /** @public */
1964
- export declare const GfmFormatter: Formatter;
1965
- //#endregion
1966
- //#region src/formatters/json.d.ts
1967
- /** @public */
1968
- export declare const JsonFormatter: Formatter;
1969
- //#endregion
1970
- //#region src/formatters/markdown.d.ts
1971
- /**
1972
- * Markdown formatter for surfaces that *render* markdown.
1973
- *
1974
- * Stdout output goes through {@link TerminalFormatter} (plain text +
1975
- * ANSI), not this formatter — markdown syntax (e.g. heading prefixes,
1976
- * backticks, list dashes) is visual noise in a terminal that doesn't
1977
- * render it. This formatter remains for callers that explicitly request
1978
- * `format: "markdown"`, e.g. when piping the rendered output into a
1979
- * Markdown-aware sink.
1980
- * @public
1981
- */
1982
- export declare const MarkdownFormatter: Formatter;
1983
- //#endregion
1984
- //#region src/formatters/silent.d.ts
1985
- /** @public */
1986
- export declare const SilentFormatter: Formatter;
1987
- //#endregion
1988
- //#region src/formatters/terminal.d.ts
1989
- /** @public */
1990
- export declare const TerminalFormatter: Formatter;
1991
- //#endregion
1992
1928
  //#region src/schemas/Agent.d.ts
1993
1929
  declare const Agent_base: Schema.Class<Agent, Schema.TaggedStruct<"Agent", {
1994
1930
  readonly agentId: Schema.brand<Schema.String, "AgentId">;
package/index.js CHANGED
@@ -5,19 +5,6 @@ import { PathResolutionError } from "./errors/PathResolutionError.js";
5
5
  import { ProjectIdentityNotResolvableError } from "./errors/ProjectIdentityError.js";
6
6
  import { GitCommandError, ProbeMissError } from "./errors/RunContextError.js";
7
7
  import { BehaviorNotFoundError, GoalNotFoundError, IllegalStatusTransitionError, TddTaskAlreadyEndedError, TddTaskNotFoundError } from "./errors/TddErrors.js";
8
- import { ciAnnotationsFormatter } from "./formatters/ci-annotations.js";
9
- import { formatGfm } from "./utils/format-gfm.js";
10
- import { GfmFormatter } from "./formatters/gfm.js";
11
- import { JsonFormatter } from "./formatters/json.js";
12
- import { ansi, stripAnsi } from "./utils/ansi.js";
13
- import { formatScopedCoverageNote } from "./utils/format-scoped-coverage-note.js";
14
- import { determineTier, formatConsoleMarkdown, getWorstMetric, relativePath } from "./utils/format-console.js";
15
- import { MarkdownFormatter } from "./formatters/markdown.js";
16
- import { SilentFormatter } from "./formatters/silent.js";
17
- import { compressLines } from "./utils/compress-lines.js";
18
- import { osc8 } from "./utils/hyperlink.js";
19
- import { basenamePosix, joinPosix, relativePosix, toPosix } from "./utils/posix-path.js";
20
- import { TerminalFormatter } from "./formatters/terminal.js";
21
8
  import { ActorType, AgentId, ChatId, ConversationId, HostKind, ProjectKey, TddTaskId } from "./schemas/Identity.js";
22
9
  import { Agent, IdempotencyHit } from "./schemas/Agent.js";
23
10
  import { AgentConsoleMode, CiConsoleMode, ConsoleMode, ConsoleOutputMode, DetailLevel, Environment, Executor, HumanConsoleMode, OutputFormat, PackageManager, ReportError, TestClassification, TestRunReason, TestState } from "./schemas/Common.js";
@@ -48,19 +35,26 @@ import { ToolCallPayload } from "./schemas/turns/ToolCallPayload.js";
48
35
  import { ToolResultPayload } from "./schemas/turns/ToolResultPayload.js";
49
36
  import { UserPromptPayload } from "./schemas/turns/UserPromptPayload.js";
50
37
  import { TurnPayload } from "./schemas/turns/index.js";
38
+ import { ansi, stripAnsi } from "./utils/ansi.js";
51
39
  import { coerceErrorField, coerceErrorText } from "./utils/coerce-error-text.js";
52
40
  import { buildAgentReport, countSuiteFailures } from "./utils/build-report.js";
53
41
  import { canonicalizeGitUrl, gitUrlToProjectKey } from "./utils/canonicalize-git-url.js";
54
42
  import { classifyTest } from "./utils/classify-test.js";
43
+ import { compressLines } from "./utils/compress-lines.js";
55
44
  import { computeTrend, getRecentDirection, hashTargets } from "./utils/compute-trend.js";
56
45
  import { buildConsoleLeaks, collectConsoleLeakEntries } from "./utils/console-leaks.js";
57
46
  import { detectNonDefaultDiscoverStrategy } from "./utils/detect-non-default-discover-strategy.js";
58
47
  import { detectPackageManager, getRunCommand } from "./utils/detect-pm.js";
59
48
  import { isTimeoutError } from "./utils/detect-timeout.js";
49
+ import { formatScopedCoverageNote } from "./utils/format-scoped-coverage-note.js";
50
+ import { determineTier, formatConsoleMarkdown, getWorstMetric, relativePath } from "./utils/format-console.js";
60
51
  import { formatFatalError } from "./utils/format-fatal-error.js";
52
+ import { formatGfm } from "./utils/format-gfm.js";
61
53
  import { findFunctionBoundary } from "./utils/function-boundary.js";
54
+ import { osc8 } from "./utils/hyperlink.js";
62
55
  import { buildEnvPrefix, detectVitestScripts, isVitestInvocation, rewriteBashCommand } from "./utils/match-vitest-command.js";
63
56
  import { normalizeWorkspaceKey } from "./utils/normalize-workspace-key.js";
57
+ import { basenamePosix, joinPosix, relativePosix, toPosix } from "./utils/posix-path.js";
64
58
  import { probeHostMetadataFromEnv } from "./utils/probe-host-metadata.js";
65
59
  import { safeFilename } from "./utils/safe-filename.js";
66
60
  import { BATS_FILE_GLOB_SUFFIX, NON_DISCOVERABLE_DIRS, SRC_DIR, TEST_DIR, TEST_FILE_GLOB_SUFFIX, TEST_HELPER_DIRS, classifyTestPath, findOwningWorkspace, isTestFileName } from "./utils/test-location.js";
@@ -68,4 +62,4 @@ import { validateCoverageTargetsShape } from "./utils/validate-coverage-targets-
68
62
  import { requiredArtifactForTransition, transitionEnforcesBehaviorMatch, validatePhaseTransition } from "./utils/validate-phase-transition.js";
69
63
  import { CURRENT_SDK_VERSION } from "./version.js";
70
64
 
71
- export { ActionSeverity, ActorType, Agent, AgentConsoleMode, AgentId, AgentNotFoundError, AgentPluginOptions, AgentReport, AgentReporterOptions, BATS_FILE_GLOB_SUFFIX, BehaviorAbandonedEvent, BehaviorAddedEvent, BehaviorCompletedEvent, BehaviorDetail, BehaviorNotFoundError, BehaviorRow, BehaviorScopedEventTypes, BehaviorStartedEvent, BehaviorStatus, BehaviorsReadyEvent, BlockedEvent, CURRENT_SDK_VERSION, CacheManifest, CacheManifestEntry, ChannelEvent, ChatId, CiConsoleMode, ConsoleLeakFile, ConsoleLeaks, ConsoleLeaksFromFailingTests, ConsoleLogLevel, ConsoleMode, ConsoleOutputMode, ConsoleOutputs, ConversationId, ConversationNotFoundError, CoverageBaselines, FileCoverageReport as CoverageFile, CoverageGap, CoverageLevel, CoverageMetric, CoverageRenderState, CoverageReport, CoverageTargets, CoverageTargetsMetrics, CoverageTotals, DataStoreError, DetailLevel, DiscoveryError, Environment, Executor, FailureRecord, FileCoverageReport, FileEditPayload, GfmFormatter, GitCommandError, GoalAbandonedEvent, GoalAddedEvent, GoalCompletedEvent, GoalDetail, GoalNotFoundError, GoalRow, GoalStartedEvent, GoalStatus, GoalsReadyEvent, HistoryRecord, HookFirePayload, HookStatus, HookType, HostKind, HostMetadataParseError, HumanConsoleMode, HypothesisPayload, IdempotencyHit, IllegalStatusTransitionError, JsonFormatter, MarkdownFormatter, MetricThresholds, ModuleRecord, ModuleReport, NON_DISCOVERABLE_DIRS, NotePayload, OutputFormat, PackageManager, PathResolutionError, PatternMetricThresholds, PatternThresholds, PerFileThresholds, PhaseTransitionEvent, ProbeMissError, ProjectIdentityNotResolvableError, ProjectKey, RUN_REPORT_FILE_SCHEMA_URL, RegistrationConflictError, RenderState, RenderTotals, ReportError, ReportOption, ReportSettings, ReportSummary, ResolvedThresholds, RunEvent, RunReportFile, SRC_DIR, SessionCompleteEvent, SessionNotFoundError, SessionOutcome, SidecarTimeoutError, SilentFormatter, SuggestedActionRecord, TEST_DIR, TEST_FILE_GLOB_SUFFIX, TEST_HELPER_DIRS, TagCountEntry, TddTaskAlreadyEndedError, TddTaskId, TddTaskNotFoundError, TerminalFormatter, TestAnnotation, TestArtifact, TestArtifactLocation, TestAttachment, TestClassification, TestHistory, TestRecord, TestReport, TestRun, TestRunReason, TestState, ToolCallPayload, ToolResultPayload, Transport, TrendEntry, TrendRecord, TurnPayload, UserPromptPayload, VitestAgentConfig, ansi, basenamePosix, buildAgentReport, buildConsoleLeaks, buildEnvPrefix, canonicalizeGitUrl, ciAnnotationsFormatter, classifyTest, classifyTestPath, coerceErrorField, coerceErrorText, collectConsoleLeakEntries, compressLines, computeTrend, countSuiteFailures, detectNonDefaultDiscoverStrategy, detectPackageManager, detectVitestScripts, determineTier, extractSqlReason, findFunctionBoundary, findOwningWorkspace, formatConsoleMarkdown, formatFatalError, formatGfm, formatScopedCoverageNote, getRecentDirection, getRunCommand, getWorstMetric, gitUrlToProjectKey, hashTargets, initialRenderState, isTestFileName, isTimeoutError, isVitestInvocation, joinPosix, normalizeWorkspaceKey, osc8, probeHostMetadataFromEnv, relativePath, relativePosix, requiredArtifactForTransition, resolveCoverageInput, rewriteBashCommand, safeFilename, stripAnsi, toPosix, transitionEnforcesBehaviorMatch, validateCoverageConfig, validateCoverageTargetsShape, validatePhaseTransition };
65
+ export { ActionSeverity, ActorType, Agent, AgentConsoleMode, AgentId, AgentNotFoundError, AgentPluginOptions, AgentReport, AgentReporterOptions, BATS_FILE_GLOB_SUFFIX, BehaviorAbandonedEvent, BehaviorAddedEvent, BehaviorCompletedEvent, BehaviorDetail, BehaviorNotFoundError, BehaviorRow, BehaviorScopedEventTypes, BehaviorStartedEvent, BehaviorStatus, BehaviorsReadyEvent, BlockedEvent, CURRENT_SDK_VERSION, CacheManifest, CacheManifestEntry, ChannelEvent, ChatId, CiConsoleMode, ConsoleLeakFile, ConsoleLeaks, ConsoleLeaksFromFailingTests, ConsoleLogLevel, ConsoleMode, ConsoleOutputMode, ConsoleOutputs, ConversationId, ConversationNotFoundError, CoverageBaselines, FileCoverageReport as CoverageFile, CoverageGap, CoverageLevel, CoverageMetric, CoverageRenderState, CoverageReport, CoverageTargets, CoverageTargetsMetrics, CoverageTotals, DataStoreError, DetailLevel, DiscoveryError, Environment, Executor, FailureRecord, FileCoverageReport, FileEditPayload, GitCommandError, GoalAbandonedEvent, GoalAddedEvent, GoalCompletedEvent, GoalDetail, GoalNotFoundError, GoalRow, GoalStartedEvent, GoalStatus, GoalsReadyEvent, HistoryRecord, HookFirePayload, HookStatus, HookType, HostKind, HostMetadataParseError, HumanConsoleMode, HypothesisPayload, IdempotencyHit, IllegalStatusTransitionError, MetricThresholds, ModuleRecord, ModuleReport, NON_DISCOVERABLE_DIRS, NotePayload, OutputFormat, PackageManager, PathResolutionError, PatternMetricThresholds, PatternThresholds, PerFileThresholds, PhaseTransitionEvent, ProbeMissError, ProjectIdentityNotResolvableError, ProjectKey, RUN_REPORT_FILE_SCHEMA_URL, RegistrationConflictError, RenderState, RenderTotals, ReportError, ReportOption, ReportSettings, ReportSummary, ResolvedThresholds, RunEvent, RunReportFile, SRC_DIR, SessionCompleteEvent, SessionNotFoundError, SessionOutcome, SidecarTimeoutError, SuggestedActionRecord, TEST_DIR, TEST_FILE_GLOB_SUFFIX, TEST_HELPER_DIRS, TagCountEntry, TddTaskAlreadyEndedError, TddTaskId, TddTaskNotFoundError, TestAnnotation, TestArtifact, TestArtifactLocation, TestAttachment, TestClassification, TestHistory, TestRecord, TestReport, TestRun, TestRunReason, TestState, ToolCallPayload, ToolResultPayload, Transport, TrendEntry, TrendRecord, TurnPayload, UserPromptPayload, VitestAgentConfig, ansi, basenamePosix, buildAgentReport, buildConsoleLeaks, buildEnvPrefix, canonicalizeGitUrl, classifyTest, classifyTestPath, coerceErrorField, coerceErrorText, collectConsoleLeakEntries, compressLines, computeTrend, countSuiteFailures, detectNonDefaultDiscoverStrategy, detectPackageManager, detectVitestScripts, determineTier, extractSqlReason, findFunctionBoundary, findOwningWorkspace, formatConsoleMarkdown, formatFatalError, formatGfm, formatScopedCoverageNote, getRecentDirection, getRunCommand, getWorstMetric, gitUrlToProjectKey, hashTargets, initialRenderState, isTestFileName, isTimeoutError, isVitestInvocation, joinPosix, normalizeWorkspaceKey, osc8, probeHostMetadataFromEnv, relativePath, relativePosix, requiredArtifactForTransition, resolveCoverageInput, rewriteBashCommand, safeFilename, stripAnsi, toPosix, transitionEnforcesBehaviorMatch, validateCoverageConfig, validateCoverageTargetsShape, validatePhaseTransition };
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@vitest-agent/sdk",
3
- "version": "5.2.0",
3
+ "version": "6.0.0",
4
4
  "private": false,
5
- "description": "Platform-free core for the vitest-agent package family: Effect Schema definitions, reporter and dispatcher contracts, errors, formatters, and pure utilities.",
5
+ "description": "Platform-free core for the vitest-agent package family: Effect Schema definitions, reporter and dispatcher contracts, errors, and pure utilities.",
6
6
  "keywords": [
7
7
  "vitest",
8
8
  "agent",
package/version.js CHANGED
@@ -11,7 +11,7 @@
11
11
  * survives into the published artifact.
12
12
  * @public
13
13
  */
14
- const CURRENT_SDK_VERSION = "5.2.0";
14
+ const CURRENT_SDK_VERSION = "6.0.0";
15
15
 
16
16
  //#endregion
17
17
  export { CURRENT_SDK_VERSION };
@@ -1,61 +0,0 @@
1
- //#region src/formatters/ci-annotations.ts
2
- const escapeData = (s) => s.replace(/%/g, "%25").replace(/\n/g, "%0A").replace(/\r/g, "%0D");
3
- const escapeProperty = (s) => s.replace(/%/g, "%25").replace(/\n/g, "%0A").replace(/\r/g, "%0D").replace(/:/g, "%3A").replace(/,/g, "%2C");
4
- const STACK_FILE_LINE = /\(([^():\s]+):(\d+):\d+\)/;
5
- function extractFileAndLine(error, fallbackFile) {
6
- if (error.stack !== void 0) {
7
- const m = STACK_FILE_LINE.exec(error.stack);
8
- if (m) return {
9
- file: m[1],
10
- line: Number.parseInt(m[2], 10)
11
- };
12
- }
13
- return {
14
- file: fallbackFile,
15
- line: 1
16
- };
17
- }
18
- function annotateFailure(report) {
19
- const lines = [];
20
- for (const mod of report.failed) for (const test of mod.tests) {
21
- if (test.state !== "failed") continue;
22
- const errors = test.errors ?? [];
23
- if (errors.length === 0) {
24
- lines.push(`::error file=${escapeProperty(mod.file)},line=1,title=${escapeProperty(test.fullName)}::${escapeData(`${test.fullName} failed`)}`);
25
- continue;
26
- }
27
- for (const err of errors) {
28
- const { file, line } = extractFileAndLine(err, mod.file);
29
- lines.push(`::error file=${escapeProperty(file)},line=${line},title=${escapeProperty(test.fullName)}::${escapeData(err.message)}`);
30
- }
31
- }
32
- for (const err of report.unhandledErrors) lines.push(`::error title=Unhandled%20error::${escapeData(err.message)}`);
33
- return lines;
34
- }
35
- /** @public */
36
- const ciAnnotationsFormatter = {
37
- format: "ci-annotations",
38
- render: (reports, _context) => {
39
- const allLines = [];
40
- let totalPassed = 0;
41
- let totalFailed = 0;
42
- let totalSkipped = 0;
43
- let totalDuration = 0;
44
- for (const r of reports) {
45
- allLines.push(...annotateFailure(r));
46
- totalPassed += r.summary.passed;
47
- totalFailed += r.summary.failed;
48
- totalSkipped += r.summary.skipped;
49
- totalDuration += r.summary.duration;
50
- }
51
- if (allLines.length === 0) allLines.push(`::notice::${escapeData(`Vitest: ${totalPassed} passed, ${totalSkipped} skipped (${totalDuration}ms)`)}`);
52
- return [{
53
- target: "stdout",
54
- content: `${allLines.join("\n")}\n`,
55
- contentType: "text/plain"
56
- }];
57
- }
58
- };
59
-
60
- //#endregion
61
- export { ciAnnotationsFormatter };
package/formatters/gfm.js DELETED
@@ -1,20 +0,0 @@
1
- import { formatGfm } from "../utils/format-gfm.js";
2
-
3
- //#region src/formatters/gfm.ts
4
- /** @public */
5
- const GfmFormatter = {
6
- format: "gfm",
7
- render: (reports, _context) => {
8
- const outputs = [];
9
- const gfm = formatGfm([...reports]);
10
- if (gfm) outputs.push({
11
- target: "github-summary",
12
- content: gfm,
13
- contentType: "text/markdown"
14
- });
15
- return outputs;
16
- }
17
- };
18
-
19
- //#endregion
20
- export { GfmFormatter };
@@ -1,18 +0,0 @@
1
- //#region src/formatters/json.ts
2
- /** @public */
3
- const JsonFormatter = {
4
- format: "json",
5
- render: (reports, _context) => {
6
- const outputs = [];
7
- const json = JSON.stringify(reports, null, 2);
8
- outputs.push({
9
- target: "stdout",
10
- content: json,
11
- contentType: "application/json"
12
- });
13
- return outputs;
14
- }
15
- };
16
-
17
- //#endregion
18
- export { JsonFormatter };
@@ -1,40 +0,0 @@
1
- import { formatConsoleMarkdown } from "../utils/format-console.js";
2
-
3
- //#region src/formatters/markdown.ts
4
- /**
5
- * Markdown formatter for surfaces that *render* markdown.
6
- *
7
- * Stdout output goes through {@link TerminalFormatter} (plain text +
8
- * ANSI), not this formatter — markdown syntax (e.g. heading prefixes,
9
- * backticks, list dashes) is visual noise in a terminal that doesn't
10
- * render it. This formatter remains for callers that explicitly request
11
- * `format: "markdown"`, e.g. when piping the rendered output into a
12
- * Markdown-aware sink.
13
- * @public
14
- */
15
- const MarkdownFormatter = {
16
- format: "markdown",
17
- render: (reports, context) => {
18
- const outputs = [];
19
- for (const report of reports) {
20
- const md = formatConsoleMarkdown(report, {
21
- consoleOutput: context.detail === "minimal" ? "failures" : "full",
22
- coverageConsoleLimit: context.coverageConsoleLimit,
23
- cwd: context.cwd,
24
- noColor: context.noColor,
25
- ...context.trendSummary !== void 0 ? { trendSummary: context.trendSummary } : {},
26
- ...context.runCommand !== void 0 ? { runCommand: context.runCommand } : {},
27
- ...context.mcp !== void 0 ? { mcp: context.mcp } : {}
28
- });
29
- if (md) outputs.push({
30
- target: "stdout",
31
- content: md,
32
- contentType: "text/markdown"
33
- });
34
- }
35
- return outputs;
36
- }
37
- };
38
-
39
- //#endregion
40
- export { MarkdownFormatter };
@@ -1,9 +0,0 @@
1
- //#region src/formatters/silent.ts
2
- /** @public */
3
- const SilentFormatter = {
4
- format: "silent",
5
- render: () => []
6
- };
7
-
8
- //#endregion
9
- export { SilentFormatter };
@@ -1,58 +0,0 @@
1
- import { formatTerminal } from "../utils/format-terminal.js";
2
- import { osc8 } from "../utils/hyperlink.js";
3
- import { joinPosix } from "../utils/posix-path.js";
4
-
5
- //#region src/formatters/terminal.ts
6
- /**
7
- * Wrap test-file paths in failing-test rows with `file://` OSC-8
8
- * hyperlinks. Only fires when the target is stdout and noColor is
9
- * unset; MCP responses never reach this formatter so they cannot
10
- * accidentally pick up escape sequences.
11
- *
12
- * Failing-test rows are emitted by `renderFailedTest` in
13
- * `format-terminal.ts` as
14
- *
15
- * ` ` + ANSI(red, ✗) + ` ` + path + ` > ` + fullName
16
- *
17
- * The ✗ is wrapped in ANSI color escapes whenever color is on (which
18
- * is always when this function runs — `wrapHyperlinks` is gated on
19
- * `!noColor`). The optional `(?:\x1b\[\d+m)?` groups around `✗` make
20
- * the pattern tolerant of those escapes; without them the regex
21
- * silently never matched and the hyperlink feature was a no-op.
22
- *
23
- * The captured path is project-relative (the failing-test row goes
24
- * through `relativePath()` in `format-terminal.ts`). RFC 8089
25
- * requires an absolute filesystem path inside a `file://` URL —
26
- * iTerm2 / WezTerm / Kitty / VSCode all silently fail to open
27
- * relative targets. Resolve the captured value back to absolute
28
- * against `ctx.cwd` before handing it to `osc8`. The display label
29
- * stays relative so the rendered output is unchanged.
30
- * @public
31
- */
32
- const FAILED_TEST_ROW = /^( {4}(?:\x1b\[\d+m)?✗(?:\x1b\[\d+m)? )([^ ]+)( > )/gm;
33
- const wrapHyperlinks = (text, ctx) => text.replace(FAILED_TEST_ROW, (_match, prefix, captured, suffix) => {
34
- const absolute = captured.startsWith("/") ? captured : joinPosix(ctx.cwd, captured);
35
- return `${prefix}${osc8(`file://${absolute}`, captured, { enabled: !ctx.noColor })}${suffix}`;
36
- });
37
- /** @public */
38
- const TerminalFormatter = {
39
- format: "terminal",
40
- render: (reports, context) => {
41
- const text = formatTerminal(reports, {
42
- cwd: context.cwd,
43
- noColor: context.noColor,
44
- coverageConsoleLimit: context.coverageConsoleLimit,
45
- ...context.trendSummary !== void 0 ? { trendSummary: context.trendSummary } : {},
46
- ...context.mcp !== void 0 ? { mcp: context.mcp } : {}
47
- });
48
- if (text === "") return [];
49
- return [{
50
- target: "stdout",
51
- content: context.noColor ? text : wrapHyperlinks(text, context),
52
- contentType: "text/plain"
53
- }];
54
- }
55
- };
56
-
57
- //#endregion
58
- export { TerminalFormatter };
@@ -1,493 +0,0 @@
1
- import { ansi } from "./ansi.js";
2
- import { formatScopedCoverageNote } from "./format-scoped-coverage-note.js";
3
- import { relativePath } from "./format-console.js";
4
- import { compressLines } from "./compress-lines.js";
5
-
6
- //#region src/utils/format-terminal.ts
7
- /**
8
- * Compress a comma-separated line list into ranges where consecutive
9
- * numbers run together. Defensive: callers may pass already-compressed
10
- * strings (e.g. "29-32, 34-38") or uncompressed (e.g. "29,30,31,32") —
11
- * both are handled. If parsing fails, the original string is returned
12
- * unchanged.
13
- *
14
- * @internal
15
- */
16
- const compressUncov = (uncoveredLines) => {
17
- if (uncoveredLines.length === 0) return "";
18
- if (uncoveredLines.includes("-")) return uncoveredLines;
19
- const numbers = [];
20
- for (const piece of uncoveredLines.split(",")) {
21
- const n = Number.parseInt(piece.trim(), 10);
22
- if (Number.isNaN(n)) return uncoveredLines;
23
- numbers.push(n);
24
- }
25
- return compressLines(numbers);
26
- };
27
- const METRICS = [
28
- {
29
- key: "lines",
30
- label: "lines"
31
- },
32
- {
33
- key: "branches",
34
- label: "branches"
35
- },
36
- {
37
- key: "functions",
38
- label: "funcs"
39
- },
40
- {
41
- key: "statements",
42
- label: "stmts"
43
- }
44
- ];
45
- /**
46
- * Format a duration in milliseconds as a short human string.
47
- *
48
- * Sub-second durations stay in ms (`"328ms"`); 1s and longer switch to
49
- * seconds with one decimal (`"5.6s"`). Avoids the noise of "5577ms" in
50
- * summary rows where second-level precision is plenty.
51
- *
52
- * @internal
53
- */
54
- const formatDuration = (ms) => {
55
- if (ms < 1e3) return `${Math.round(ms)}ms`;
56
- return `${(ms / 1e3).toFixed(1)}s`;
57
- };
58
- /**
59
- * Decide whether the project label should appear in the row.
60
- *
61
- * Single-project setups with no explicit `project.name` (Vitest's
62
- * default) have `project` set to `"default"`. In that case there's
63
- * nothing to disambiguate; suppress the label.
64
- *
65
- * @internal
66
- */
67
- const showProjectLabel = (reports) => {
68
- if (reports.length > 1) return true;
69
- const single = reports[0];
70
- if (!single?.project) return false;
71
- if (single.project === "default") return false;
72
- return true;
73
- };
74
- /**
75
- * Right-pad a column value to a fixed width with spaces. Plain ASCII
76
- * width — does not account for ANSI escapes (caller is responsible).
77
- *
78
- * @internal
79
- */
80
- const pad = (s, width) => s.length >= width ? s : s + " ".repeat(width - s.length);
81
- /**
82
- * Build an inline tag-count summary suffix for a project row.
83
- * Only shown when two or more distinct tag kinds are present.
84
- *
85
- * @internal
86
- */
87
- const buildTagSummary = (tagCounts) => {
88
- if (!tagCounts) return "";
89
- const keys = Object.keys(tagCounts).sort();
90
- if (keys.length < 2) return "";
91
- return " " + keys.map((k) => {
92
- const c = tagCounts[k];
93
- return `${k}:${(c?.passed ?? 0) + (c?.failed ?? 0) + (c?.skipped ?? 0)}`;
94
- }).join(" ");
95
- };
96
- /**
97
- * Render indented per-tag pass/fail breakdown rows for a project that has failures.
98
- * The `s` (skipped) column only appears when skipped \> 0 for that tag.
99
- *
100
- * @internal
101
- */
102
- const renderTagFailureBreakdown = (tagCounts) => {
103
- const lines = [];
104
- const keys = Object.keys(tagCounts).sort();
105
- if (keys.length === 0) return lines;
106
- const tagWidth = Math.max(...keys.map((k) => k.length));
107
- for (const k of keys) {
108
- const c = tagCounts[k];
109
- const p = c?.passed ?? 0;
110
- const f = c?.failed ?? 0;
111
- const s = c?.skipped ?? 0;
112
- const skipPart = s > 0 ? ` ${s}s` : "";
113
- lines.push(` ${k.padEnd(tagWidth)} ${p}p ${f}f${skipPart}`);
114
- }
115
- return lines;
116
- };
117
- /**
118
- * Project row: `✓ name 103 passed (2.1s)` or
119
- * `✗ name 102 passed, 1 failed (2.1s)`.
120
- *
121
- * @internal
122
- */
123
- const renderProjectRow = (report, ao, nameWidth) => {
124
- const failed = report.summary.failed;
125
- const passed = report.summary.passed;
126
- const skipped = report.summary.skipped;
127
- const duration = formatDuration(report.summary.duration);
128
- const name = report.project ?? "default";
129
- const tick = failed === 0 ? ansi("✓", "green", ao) : ansi("✗", "red", ao);
130
- const counts = [];
131
- if (failed > 0) counts.push(`${failed} failed`);
132
- counts.push(`${passed} passed`);
133
- if (skipped > 0) counts.push(`${skipped} skipped`);
134
- return `${` ${tick} ${pad(name, nameWidth)} ${counts.join(", ")} (${duration})`}${buildTagSummary(report.tagCounts)}`;
135
- };
136
- /**
137
- * Format the global target spec compactly.
138
- *
139
- * If every metric has the same target (the common case), collapse to
140
- * `≥ 80% lines, branches, funcs, stmts`. If targets differ, list per
141
- * metric: `lines ≥ 80%, branches ≥ 75%, funcs ≥ 70%, stmts ≥ 80%`.
142
- *
143
- * @internal
144
- */
145
- const formatTargetSpec = (targets) => {
146
- const set = /* @__PURE__ */ new Set();
147
- const present = [];
148
- for (const m of METRICS) {
149
- const v = targets[m.key];
150
- if (v == null) continue;
151
- set.add(v);
152
- present.push({
153
- label: m.label,
154
- v
155
- });
156
- }
157
- if (present.length === 0) return "";
158
- if (set.size === 1) {
159
- const v = [...set][0];
160
- return `${present.map((p) => p.label).join(", ")} ≥ ${v}%`;
161
- }
162
- return present.map((p) => `${p.label} ≥ ${p.v}%`).join(", ");
163
- };
164
- /**
165
- * Header labels for the v8-style coverage table. The order is fixed
166
- * and matches the column order rendered by {@link renderBelowTargetTable}.
167
- *
168
- * @internal
169
- */
170
- const TABLE_HEADERS = {
171
- file: "File",
172
- stmts: "% Stmts",
173
- branches: "% Branch",
174
- funcs: "% Funcs",
175
- lines: "% Lines",
176
- uncov: "Uncovered Line #s"
177
- };
178
- /**
179
- * Pad a cell with one space on each side of its content. Numeric
180
- * cells are right-aligned, string cells are left-aligned. The width
181
- * argument is the inner content width (without the padding spaces).
182
- *
183
- * @internal
184
- */
185
- const cell = (s, width, align) => {
186
- return ` ${align === "right" ? s.padStart(width) : s.padEnd(width)} `;
187
- };
188
- /**
189
- * Render a separator row matching the column geometry of the header.
190
- * Width per column = inner width + 2 (the padding spaces) so the row
191
- * lines up with the headers and data rows when joined by `|`.
192
- *
193
- * @internal
194
- */
195
- const sepRow = (widths) => widths.map((w) => "-".repeat(w + 2)).join("|");
196
- /**
197
- * v8-style coverage table for files below aspirational targets.
198
- *
199
- * Columns: File, % Stmts, % Branch, % Funcs, % Lines, Uncovered
200
- * Line #s. Numeric columns are right-aligned and show the file's
201
- * actual coverage value (no `%` suffix — the header carries it).
202
- * Showing every metric per row, not just the below-target ones,
203
- * matches v8's reporter and gives an LLM agent a complete picture
204
- * of where the file stands relative to all four metrics.
205
- *
206
- * Returns header rows + data rows + a trailing separator. The caller
207
- * is responsible for the surrounding caption and any truncation
208
- * footer.
209
- *
210
- * @internal
211
- */
212
- const renderBelowTargetTable = (files, cwd) => {
213
- if (files.length === 0) return [];
214
- const rows = files.map((f) => ({
215
- file: relativePath(f.file, cwd),
216
- stmts: String(Math.round(f.summary.statements)),
217
- branches: String(Math.round(f.summary.branches)),
218
- funcs: String(Math.round(f.summary.functions)),
219
- lines: String(Math.round(f.summary.lines)),
220
- uncov: compressUncov(f.uncoveredLines)
221
- }));
222
- const widths = [
223
- Math.max(TABLE_HEADERS.file.length, ...rows.map((r) => r.file.length)),
224
- Math.max(TABLE_HEADERS.stmts.length, ...rows.map((r) => r.stmts.length)),
225
- Math.max(TABLE_HEADERS.branches.length, ...rows.map((r) => r.branches.length)),
226
- Math.max(TABLE_HEADERS.funcs.length, ...rows.map((r) => r.funcs.length)),
227
- Math.max(TABLE_HEADERS.lines.length, ...rows.map((r) => r.lines.length)),
228
- Math.max(TABLE_HEADERS.uncov.length, ...rows.map((r) => r.uncov.length))
229
- ];
230
- const out = [];
231
- out.push(sepRow(widths));
232
- out.push([
233
- cell(TABLE_HEADERS.file, widths[0], "left"),
234
- cell(TABLE_HEADERS.stmts, widths[1], "right"),
235
- cell(TABLE_HEADERS.branches, widths[2], "right"),
236
- cell(TABLE_HEADERS.funcs, widths[3], "right"),
237
- cell(TABLE_HEADERS.lines, widths[4], "right"),
238
- cell(TABLE_HEADERS.uncov, widths[5], "left")
239
- ].join("|"));
240
- out.push(sepRow(widths));
241
- for (const r of rows) out.push([
242
- cell(r.file, widths[0], "left"),
243
- cell(r.stmts, widths[1], "right"),
244
- cell(r.branches, widths[2], "right"),
245
- cell(r.funcs, widths[3], "right"),
246
- cell(r.lines, widths[4], "right"),
247
- cell(r.uncov, widths[5], "left")
248
- ].join("|"));
249
- out.push(sepRow(widths));
250
- return out;
251
- };
252
- /**
253
- * Aggregated coverage state across reports.
254
- *
255
- * @internal
256
- */
257
- const aggregateCoverage = (reports) => {
258
- let hasCoverage = false;
259
- let thresholdsGlobal;
260
- let targetsGlobal;
261
- let scoped = false;
262
- let scopedFileCount = 0;
263
- let totalFiles;
264
- const belowTargetByFile = /* @__PURE__ */ new Map();
265
- const belowThresholdByFile = /* @__PURE__ */ new Map();
266
- for (const r of reports) {
267
- const cov = r.coverage;
268
- if (!cov) continue;
269
- hasCoverage = true;
270
- thresholdsGlobal ??= cov.thresholds.global;
271
- targetsGlobal ??= cov.targets?.global;
272
- if (cov.scoped) {
273
- scoped = true;
274
- scopedFileCount = Math.max(scopedFileCount, cov.scopedFiles?.length ?? 0);
275
- if (cov.totalFiles !== void 0) totalFiles = cov.totalFiles;
276
- }
277
- if (cov.lowCoverage) {
278
- for (const f of cov.lowCoverage) if (!belowThresholdByFile.has(f.file)) belowThresholdByFile.set(f.file, f);
279
- }
280
- if (cov.belowTarget) {
281
- for (const f of cov.belowTarget) if (!belowTargetByFile.has(f.file)) belowTargetByFile.set(f.file, f);
282
- }
283
- }
284
- const belowTargetFiles = [...belowTargetByFile.values()];
285
- const belowThresholdCount = belowThresholdByFile.size;
286
- return {
287
- hasCoverage,
288
- thresholdsMet: belowThresholdCount === 0,
289
- belowThresholdCount,
290
- belowTargetFiles,
291
- scoped,
292
- scopedFileCount,
293
- ...thresholdsGlobal !== void 0 ? { thresholdsGlobal } : {},
294
- ...targetsGlobal !== void 0 ? { targetsGlobal } : {},
295
- ...totalFiles !== void 0 ? { totalFiles } : {}
296
- };
297
- };
298
- /**
299
- * Coverage section: one or two summary lines plus per-file rows.
300
- *
301
- * @internal
302
- */
303
- const renderCoverageSection = (reports, options, ao) => {
304
- const agg = aggregateCoverage(reports);
305
- if (!agg.hasCoverage) return [];
306
- const lines = [];
307
- const thresholdSpec = agg.thresholdsGlobal ? formatTargetSpec(agg.thresholdsGlobal) : "";
308
- const targetSpec = agg.targetsGlobal ? formatTargetSpec(agg.targetsGlobal) : "";
309
- if (agg.scoped) lines.push(formatScopedCoverageNote(agg.scopedFileCount, agg.totalFiles));
310
- else if (!agg.thresholdsMet) {
311
- const cross = ansi("✗", "red", ao);
312
- const fileWord = agg.belowThresholdCount === 1 ? "file" : "files";
313
- lines.push(`Coverage: ${cross} ${agg.belowThresholdCount} ${fileWord} below minimum thresholds${thresholdSpec ? ` (${thresholdSpec})` : ""}`);
314
- } else if (agg.belowTargetFiles.length > 0) {
315
- const tick = ansi("✓", "green", ao);
316
- const warn = ansi("⚠", "yellow", ao);
317
- const fileWord = agg.belowTargetFiles.length === 1 ? "file" : "files";
318
- lines.push(`Coverage: ${tick} minimum thresholds met${thresholdSpec ? ` (${thresholdSpec})` : ""}`);
319
- lines.push(`Coverage: ${warn} ${agg.belowTargetFiles.length} ${fileWord} below aspirational targets${targetSpec ? ` (${targetSpec})` : ""}`);
320
- } else {
321
- const tick = ansi("✓", "green", ao);
322
- if (agg.targetsGlobal) lines.push(`Coverage: ${tick} minimum thresholds + aspirational targets met${targetSpec ? ` (${targetSpec})` : ""}`);
323
- else lines.push(`Coverage: ${tick} minimum thresholds met${thresholdSpec ? ` (${thresholdSpec})` : ""}`);
324
- }
325
- if (options.trendSummary) {
326
- const { direction, runCount } = options.trendSummary;
327
- lines.push(`Trend: ${direction} (${runCount} runs)`);
328
- }
329
- if (agg.belowTargetFiles.length > 0 && agg.targetsGlobal) {
330
- lines.push("");
331
- lines.push("Files below aspirational target:");
332
- const limit = options.coverageConsoleLimit;
333
- const shown = agg.belowTargetFiles.slice(0, limit);
334
- lines.push(...renderBelowTargetTable(shown, options.cwd));
335
- if (agg.belowTargetFiles.length > limit) lines.push(`… ${agg.belowTargetFiles.length - limit} more (use the test_coverage MCP tool for the full list)`);
336
- }
337
- return lines;
338
- };
339
- /**
340
- * Failures block: grouped by project, one entry per failed test with
341
- * inline error message and stack hint.
342
- *
343
- * @internal
344
- */
345
- const renderFailuresSection = (reports, cwd, ao) => {
346
- const lines = [];
347
- const reportsWithFailures = reports.filter((r) => r.summary.failed > 0 || r.unhandledErrors.length > 0);
348
- if (reportsWithFailures.length === 0) return lines;
349
- lines.push("Failures:");
350
- for (const report of reportsWithFailures) {
351
- const projectName = report.project ?? "default";
352
- lines.push(` ${projectName}`);
353
- for (const mod of report.failed) {
354
- const file = relativePath(mod.file, cwd);
355
- const failedTests = mod.tests.filter((t) => t.state === "failed");
356
- for (const test of failedTests) lines.push(...renderFailedTest(file, test, ao));
357
- if (mod.errors && mod.errors.length > 0) for (const err of mod.errors) {
358
- lines.push(` ${ansi("✗", "red", ao)} ${file} (module-level error)`);
359
- lines.push(` ${ansi(err.message, "red", ao)}`);
360
- }
361
- }
362
- for (const err of report.unhandledErrors) {
363
- lines.push(` ${ansi("✗", "red", ao)} unhandled error`);
364
- lines.push(` ${ansi(err.message, "red", ao)}`);
365
- }
366
- }
367
- return lines;
368
- };
369
- const renderFailedTest = (file, test, ao) => {
370
- const cross = ansi("✗", "red", ao);
371
- const cls = test.classification ? ansi(` [${test.classification}]`, "dim", ao) : "";
372
- const lines = [` ${cross} ${file} > ${test.fullName}${cls}`];
373
- if (test.errors && test.errors.length > 0) for (const err of test.errors) lines.push(` ${ansi(err.message, "dim", ao)}`);
374
- return lines;
375
- };
376
- /**
377
- * Total line: aggregated pass/fail/skip counts + max-project duration.
378
- *
379
- * @internal
380
- */
381
- const renderTotal = (reports, ao) => {
382
- let passed = 0;
383
- let failed = 0;
384
- let skipped = 0;
385
- let maxDuration = 0;
386
- for (const r of reports) {
387
- passed += r.summary.passed;
388
- failed += r.summary.failed;
389
- skipped += r.summary.skipped;
390
- if (r.summary.duration > maxDuration) maxDuration = r.summary.duration;
391
- }
392
- const parts = [];
393
- if (failed > 0) parts.push(ansi(`${failed} failed`, "red", ao));
394
- parts.push(`${passed} passed`);
395
- if (skipped > 0) parts.push(`${skipped} skipped`);
396
- return `Total: ${parts.join(", ")} (${formatDuration(maxDuration)})`;
397
- };
398
- /**
399
- * Re-run hints when failures exist.
400
- *
401
- * @internal
402
- */
403
- const renderNextSteps = (reports, options) => {
404
- const lines = [];
405
- const allFailedFiles = /* @__PURE__ */ new Set();
406
- for (const r of reports) for (const f of r.failedFiles) allFailedFiles.add(relativePath(f, options.cwd));
407
- if (allFailedFiles.size === 0) return lines;
408
- lines.push("Next steps:");
409
- for (const file of allFailedFiles) lines.push(` Re-run: pnpm vitest run ${file}`);
410
- if (options.mcp) {
411
- lines.push(" Use the test_history MCP tool to check failure trends");
412
- lines.push(" Use the test_errors MCP tool to search errors by type");
413
- }
414
- return lines;
415
- };
416
- /**
417
- * Header line for multi-project runs.
418
- *
419
- * @internal
420
- */
421
- const renderHeader = (reports) => {
422
- const lines = [];
423
- if (showProjectLabel(reports) && reports.length > 1) lines.push(`Projects (${reports.length}):`);
424
- else if (showProjectLabel(reports) && reports.length === 1) lines.push(`Project: ${reports[0]?.project ?? "default"}`);
425
- return lines;
426
- };
427
- /**
428
- * Public entry point: render a collection of `AgentReport` rows as
429
- * a single plain-text block for stdout.
430
- *
431
- * @public
432
- */
433
- const formatTerminal = (reports, options) => {
434
- if (reports.length === 0) return "";
435
- const ao = { noColor: options.noColor };
436
- const out = [];
437
- const header = renderHeader(reports);
438
- if (header.length > 0) out.push(...header);
439
- if (reports.length > 1) {
440
- const widths = reports.map((r) => (r.project ?? "default").length);
441
- const nameWidth = Math.max(0, ...widths);
442
- for (const r of reports) {
443
- out.push(renderProjectRow(r, ao, nameWidth));
444
- if (r.summary.failed > 0 && r.tagCounts) out.push(...renderTagFailureBreakdown(r.tagCounts));
445
- }
446
- out.push("");
447
- } else if (showProjectLabel(reports)) {
448
- const r = reports[0];
449
- if (r) {
450
- const tick = r.summary.failed === 0 ? ansi("✓", "green", ao) : ansi("✗", "red", ao);
451
- const counts = [];
452
- if (r.summary.failed > 0) counts.push(`${r.summary.failed} failed`);
453
- counts.push(`${r.summary.passed} passed`);
454
- if (r.summary.skipped > 0) counts.push(`${r.summary.skipped} skipped`);
455
- const baseRow = `${tick} ${counts.join(", ")} (${formatDuration(r.summary.duration)})`;
456
- const tagSummary = buildTagSummary(r.tagCounts);
457
- out.push(`${baseRow}${tagSummary}`);
458
- if (r.summary.failed > 0 && r.tagCounts) out.push(...renderTagFailureBreakdown(r.tagCounts));
459
- out.push("");
460
- }
461
- } else if (reports.length === 1) {
462
- const r = reports[0];
463
- if (r) {
464
- const tick = r.summary.failed === 0 ? ansi("✓", "green", ao) : ansi("✗", "red", ao);
465
- const counts = [];
466
- if (r.summary.failed > 0) counts.push(`${r.summary.failed} failed`);
467
- counts.push(`${r.summary.passed} passed`);
468
- if (r.summary.skipped > 0) counts.push(`${r.summary.skipped} skipped`);
469
- out.push(`${tick} ${counts.join(", ")} (${formatDuration(r.summary.duration)})`);
470
- out.push("");
471
- }
472
- }
473
- const failures = renderFailuresSection(reports, options.cwd, ao);
474
- if (failures.length > 0) {
475
- out.push(...failures);
476
- out.push("");
477
- }
478
- const coverage = renderCoverageSection(reports, options, ao);
479
- if (coverage.length > 0) {
480
- out.push(...coverage);
481
- out.push("");
482
- }
483
- out.push(renderTotal(reports, ao));
484
- const nextSteps = renderNextSteps(reports, options);
485
- if (nextSteps.length > 0) {
486
- out.push("");
487
- out.push(...nextSteps);
488
- }
489
- return out.join("\n");
490
- };
491
-
492
- //#endregion
493
- export { formatTerminal };