@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 +2 -2
- package/index.d.ts +25 -89
- package/index.js +8 -14
- package/package.json +2 -2
- package/version.js +1 -1
- package/formatters/ci-annotations.js +0 -61
- package/formatters/gfm.js +0 -20
- package/formatters/json.js +0 -18
- package/formatters/markdown.js +0 -40
- package/formatters/silent.js +0 -9
- package/formatters/terminal.js +0 -58
- package/utils/format-terminal.js +0 -493
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
|
|
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
|
|
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,
|
|
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": "
|
|
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,
|
|
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
|
@@ -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 };
|
package/formatters/json.js
DELETED
|
@@ -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 };
|
package/formatters/markdown.js
DELETED
|
@@ -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 };
|
package/formatters/silent.js
DELETED
package/formatters/terminal.js
DELETED
|
@@ -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 };
|
package/utils/format-terminal.js
DELETED
|
@@ -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 };
|