@vitest-agent/sdk 8.0.0 → 9.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/index.d.ts +140 -317
- package/index.js +5 -10
- package/package.json +1 -1
- package/schemas/5.0/run.json +95 -0
- package/schemas/RunDiagnostics.js +61 -0
- package/schemas/RunReportFile.js +3 -1
- package/tsdoc-metadata.json +1 -1
- package/utils/ansi.js +1 -35
- package/utils/run-diagnostics-actions.js +58 -0
- package/utils/vitest-config-files.js +24 -0
- package/version.js +1 -1
- package/utils/compress-lines.js +0 -44
- package/utils/detect-pm.js +0 -57
- package/utils/format-console.js +0 -222
- package/utils/format-fatal-error.js +0 -46
- package/utils/format-gfm.js +0 -160
- package/utils/normalize-workspace-key.js +0 -33
- package/utils/probe-host-metadata.js +0 -98
- package/utils/safe-filename.js +0 -33
package/schemas/5.0/run.json
CHANGED
|
@@ -781,6 +781,98 @@
|
|
|
781
781
|
"required": ["timestamp", "reason", "summary", "failed", "unhandledErrors", "failedFiles"],
|
|
782
782
|
"additionalProperties": true
|
|
783
783
|
},
|
|
784
|
+
"PerformanceHintKind": {
|
|
785
|
+
"type": "string",
|
|
786
|
+
"enum": ["environment", "import", "transform", "isolate"]
|
|
787
|
+
},
|
|
788
|
+
"PerformanceHint": {
|
|
789
|
+
"type": "object",
|
|
790
|
+
"properties": {
|
|
791
|
+
"kind": {
|
|
792
|
+
"$ref": "#/$defs/PerformanceHintKind"
|
|
793
|
+
},
|
|
794
|
+
"project": {
|
|
795
|
+
"type": "string"
|
|
796
|
+
},
|
|
797
|
+
"message": {
|
|
798
|
+
"type": "string"
|
|
799
|
+
},
|
|
800
|
+
"remedy": {
|
|
801
|
+
"type": "string"
|
|
802
|
+
},
|
|
803
|
+
"detail": {
|
|
804
|
+
"type": "string"
|
|
805
|
+
},
|
|
806
|
+
"learnMoreUrl": {
|
|
807
|
+
"type": "string"
|
|
808
|
+
},
|
|
809
|
+
"estimatedSavingMs": {
|
|
810
|
+
"type": "number"
|
|
811
|
+
},
|
|
812
|
+
"share": {
|
|
813
|
+
"type": "number"
|
|
814
|
+
}
|
|
815
|
+
},
|
|
816
|
+
"required": ["kind", "message", "remedy", "detail", "learnMoreUrl"],
|
|
817
|
+
"additionalProperties": true
|
|
818
|
+
},
|
|
819
|
+
"PostRunTiming": {
|
|
820
|
+
"type": "object",
|
|
821
|
+
"properties": {
|
|
822
|
+
"persistMs": {
|
|
823
|
+
"type": "number"
|
|
824
|
+
},
|
|
825
|
+
"coverageScanMs": {
|
|
826
|
+
"type": "number"
|
|
827
|
+
},
|
|
828
|
+
"totalMs": {
|
|
829
|
+
"type": "number"
|
|
830
|
+
},
|
|
831
|
+
"renderMs": {
|
|
832
|
+
"type": "number"
|
|
833
|
+
},
|
|
834
|
+
"vitestDurationMs": {
|
|
835
|
+
"type": "number"
|
|
836
|
+
}
|
|
837
|
+
},
|
|
838
|
+
"required": ["persistMs", "coverageScanMs", "totalMs"],
|
|
839
|
+
"additionalProperties": true
|
|
840
|
+
},
|
|
841
|
+
"PersistenceStatus": {
|
|
842
|
+
"type": "object",
|
|
843
|
+
"properties": {
|
|
844
|
+
"ok": {
|
|
845
|
+
"type": "boolean"
|
|
846
|
+
},
|
|
847
|
+
"reason": {
|
|
848
|
+
"type": "string"
|
|
849
|
+
},
|
|
850
|
+
"migrationFailed": {
|
|
851
|
+
"type": "boolean"
|
|
852
|
+
}
|
|
853
|
+
},
|
|
854
|
+
"required": ["ok"],
|
|
855
|
+
"additionalProperties": true
|
|
856
|
+
},
|
|
857
|
+
"RunDiagnostics": {
|
|
858
|
+
"type": "object",
|
|
859
|
+
"properties": {
|
|
860
|
+
"performanceHints": {
|
|
861
|
+
"type": "array",
|
|
862
|
+
"items": {
|
|
863
|
+
"$ref": "#/$defs/PerformanceHint"
|
|
864
|
+
}
|
|
865
|
+
},
|
|
866
|
+
"postRun": {
|
|
867
|
+
"$ref": "#/$defs/PostRunTiming"
|
|
868
|
+
},
|
|
869
|
+
"persistence": {
|
|
870
|
+
"$ref": "#/$defs/PersistenceStatus"
|
|
871
|
+
}
|
|
872
|
+
},
|
|
873
|
+
"required": ["performanceHints"],
|
|
874
|
+
"additionalProperties": true
|
|
875
|
+
},
|
|
784
876
|
"RunReportFile": {
|
|
785
877
|
"type": "object",
|
|
786
878
|
"properties": {
|
|
@@ -799,6 +891,9 @@
|
|
|
799
891
|
"items": {
|
|
800
892
|
"$ref": "#/$defs/AgentReport"
|
|
801
893
|
}
|
|
894
|
+
},
|
|
895
|
+
"runDiagnostics": {
|
|
896
|
+
"$ref": "#/$defs/RunDiagnostics"
|
|
802
897
|
}
|
|
803
898
|
},
|
|
804
899
|
"required": ["schemaVersion", "generatedAt", "reports"],
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { FiniteNumber } from "./Common.js";
|
|
2
|
+
import { Schema } from "effect";
|
|
3
|
+
|
|
4
|
+
// src/schemas/RunDiagnostics.ts
|
|
5
|
+
/**
|
|
6
|
+
* Which Vitest performance diagnostic a hint came from.
|
|
7
|
+
* @public
|
|
8
|
+
*/
|
|
9
|
+
const PerformanceHintKind = Schema.Literals([
|
|
10
|
+
"environment",
|
|
11
|
+
"import",
|
|
12
|
+
"transform",
|
|
13
|
+
"isolate"
|
|
14
|
+
]).annotate({ identifier: "PerformanceHintKind" });
|
|
15
|
+
/**
|
|
16
|
+
* One performance hint derived from Vitest's run diagnostics. `project` is
|
|
17
|
+
* omitted for a hint that applies to the whole run (for example `isolate`).
|
|
18
|
+
* @public
|
|
19
|
+
*/
|
|
20
|
+
const PerformanceHint = Schema.Struct({
|
|
21
|
+
kind: PerformanceHintKind,
|
|
22
|
+
project: Schema.optionalKey(Schema.String),
|
|
23
|
+
message: Schema.String,
|
|
24
|
+
remedy: Schema.String,
|
|
25
|
+
detail: Schema.String,
|
|
26
|
+
learnMoreUrl: Schema.String,
|
|
27
|
+
estimatedSavingMs: Schema.optionalKey(FiniteNumber),
|
|
28
|
+
share: Schema.optionalKey(FiniteNumber)
|
|
29
|
+
}).annotate({ identifier: "PerformanceHint" });
|
|
30
|
+
/**
|
|
31
|
+
* Wall-clock timing of the plugin's post-run work, in milliseconds.
|
|
32
|
+
* @public
|
|
33
|
+
*/
|
|
34
|
+
const PostRunTiming = Schema.Struct({
|
|
35
|
+
persistMs: FiniteNumber,
|
|
36
|
+
coverageScanMs: FiniteNumber,
|
|
37
|
+
totalMs: FiniteNumber,
|
|
38
|
+
renderMs: Schema.optionalKey(FiniteNumber),
|
|
39
|
+
vitestDurationMs: Schema.optionalKey(FiniteNumber)
|
|
40
|
+
}).annotate({ identifier: "PostRunTiming" });
|
|
41
|
+
/**
|
|
42
|
+
* Whether the run's results were persisted to the database.
|
|
43
|
+
* @public
|
|
44
|
+
*/
|
|
45
|
+
const PersistenceStatus = Schema.Struct({
|
|
46
|
+
ok: Schema.Boolean,
|
|
47
|
+
reason: Schema.optionalKey(Schema.String),
|
|
48
|
+
migrationFailed: Schema.optionalKey(Schema.Boolean)
|
|
49
|
+
}).annotate({ identifier: "PersistenceStatus" });
|
|
50
|
+
/**
|
|
51
|
+
* Run-level diagnostics: performance hints, post-run timing and persistence
|
|
52
|
+
* status (issue #544).
|
|
53
|
+
* @public
|
|
54
|
+
*/
|
|
55
|
+
const RunDiagnostics = Schema.Struct({
|
|
56
|
+
performanceHints: Schema.Array(PerformanceHint),
|
|
57
|
+
postRun: Schema.optionalKey(PostRunTiming),
|
|
58
|
+
persistence: Schema.optionalKey(PersistenceStatus)
|
|
59
|
+
}).annotate({ identifier: "RunDiagnostics" });
|
|
60
|
+
|
|
61
|
+
export { PerformanceHint, PerformanceHintKind, PersistenceStatus, PostRunTiming, RunDiagnostics };
|
package/schemas/RunReportFile.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AgentReport } from "./AgentReport.js";
|
|
2
|
+
import { RunDiagnostics } from "./RunDiagnostics.js";
|
|
2
3
|
import { Schema } from "effect";
|
|
3
4
|
|
|
4
5
|
// src/schemas/RunReportFile.ts
|
|
@@ -47,7 +48,8 @@ const RunReportFile = Schema.Struct({
|
|
|
47
48
|
$schema: Schema.optionalKey(Schema.String),
|
|
48
49
|
schemaVersion: Schema.Literal(1),
|
|
49
50
|
generatedAt: IsoInstant,
|
|
50
|
-
reports: Schema.Array(AgentReport)
|
|
51
|
+
reports: Schema.Array(AgentReport),
|
|
52
|
+
runDiagnostics: Schema.optionalKey(RunDiagnostics)
|
|
51
53
|
}).annotate({
|
|
52
54
|
identifier: "RunReportFile",
|
|
53
55
|
description: "The vitest-agent run report file (.vitest/vitest-agent/run.json): one AgentReport per Vitest project from the most recent run. Read reports[].summary for pass/fail counts and reports[].failed for the failing modules and their tests. schemaVersion is the envelope contract version; generatedAt is when the run finished."
|
package/tsdoc-metadata.json
CHANGED
package/utils/ansi.js
CHANGED
|
@@ -1,38 +1,4 @@
|
|
|
1
1
|
// src/utils/ansi.ts
|
|
2
|
-
const ANSI_CODES = {
|
|
3
|
-
reset: [0, 0],
|
|
4
|
-
bold: [1, 22],
|
|
5
|
-
dim: [2, 22],
|
|
6
|
-
red: [31, 39],
|
|
7
|
-
green: [32, 39],
|
|
8
|
-
yellow: [33, 39],
|
|
9
|
-
cyan: [36, 39]
|
|
10
|
-
};
|
|
11
|
-
/**
|
|
12
|
-
* Wrap text with an ANSI color/style escape code.
|
|
13
|
-
*
|
|
14
|
-
* No-ops when `options.noColor` is true, keeping output as valid plain
|
|
15
|
-
* markdown when captured by agents or piped to files.
|
|
16
|
-
*
|
|
17
|
-
* @privateRemarks
|
|
18
|
-
* Uses raw ANSI escape sequences (`\x1b[...m`) rather than a dependency
|
|
19
|
-
* like `chalk` or `picocolors` to keep the package zero-dependency. The
|
|
20
|
-
* `noColor` flag respects the `NO_COLOR` environment variable convention.
|
|
21
|
-
*
|
|
22
|
-
* @param text - Text to wrap
|
|
23
|
-
* @param color - ANSI code name: `reset`, `bold`, `dim`, `red`, `green`, `yellow`, `cyan`
|
|
24
|
-
* @param options - Control color output
|
|
25
|
-
* @returns Text with ANSI escape codes, or plain text if noColor
|
|
26
|
-
*
|
|
27
|
-
* @see {@link https://no-color.org/ | NO_COLOR convention}
|
|
28
|
-
* @internal
|
|
29
|
-
*/
|
|
30
|
-
function ansi(text, color, options) {
|
|
31
|
-
if (options?.noColor) return text;
|
|
32
|
-
const codes = ANSI_CODES[color];
|
|
33
|
-
if (!codes) return text;
|
|
34
|
-
return `\x1b[${codes[0]}m${text}\x1b[${codes[1]}m`;
|
|
35
|
-
}
|
|
36
2
|
/**
|
|
37
3
|
* Strip all ANSI escape codes from a string.
|
|
38
4
|
*
|
|
@@ -45,4 +11,4 @@ function stripAnsi(text) {
|
|
|
45
11
|
return text.replace(/\x1b\[[0-9;]*m/g, "");
|
|
46
12
|
}
|
|
47
13
|
|
|
48
|
-
export {
|
|
14
|
+
export { stripAnsi };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// src/utils/run-diagnostics-actions.ts
|
|
2
|
+
/**
|
|
3
|
+
* Post-run work at or above this many milliseconds is always "slow".
|
|
4
|
+
* @public
|
|
5
|
+
*/
|
|
6
|
+
const POST_RUN_SLOW_MS = 5e3;
|
|
7
|
+
/**
|
|
8
|
+
* Post-run work at or above this share of Vitest's own duration is "slow"
|
|
9
|
+
* (once it also clears {@link POST_RUN_SLOW_MIN_MS}).
|
|
10
|
+
* @public
|
|
11
|
+
*/
|
|
12
|
+
const POST_RUN_SLOW_SHARE = .1;
|
|
13
|
+
/**
|
|
14
|
+
* Minimum post-run milliseconds before the share rule applies.
|
|
15
|
+
* @public
|
|
16
|
+
*/
|
|
17
|
+
const POST_RUN_SLOW_MIN_MS = 1e3;
|
|
18
|
+
/**
|
|
19
|
+
* Whether the plugin's post-run work was slow enough to surface.
|
|
20
|
+
* @public
|
|
21
|
+
*/
|
|
22
|
+
function isPostRunSlow(timing) {
|
|
23
|
+
if (timing.totalMs >= 5e3) return true;
|
|
24
|
+
return timing.totalMs >= 1e3 && timing.vitestDurationMs !== void 0 && timing.totalMs >= .1 * timing.vitestDurationMs;
|
|
25
|
+
}
|
|
26
|
+
const fmt = (ms) => `${(ms / 1e3).toFixed(1)}s`;
|
|
27
|
+
/**
|
|
28
|
+
* Map run diagnostics onto suggested actions: persistence failure first, then
|
|
29
|
+
* slow post-run work, then performance hints in input order.
|
|
30
|
+
* @public
|
|
31
|
+
*/
|
|
32
|
+
function suggestedActionsFromRunDiagnostics(d) {
|
|
33
|
+
const out = [];
|
|
34
|
+
if (d.persistence !== void 0 && !d.persistence.ok) {
|
|
35
|
+
const { reason, migrationFailed } = d.persistence;
|
|
36
|
+
out.push({
|
|
37
|
+
severity: "warn",
|
|
38
|
+
title: "Results were not saved",
|
|
39
|
+
detail: migrationFailed ? `${reason ?? "unknown error"}. Run \`vitest-agent db reset\` to recreate the database.` : reason ?? "unknown error"
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
if (d.postRun !== void 0 && isPostRunSlow(d.postRun)) {
|
|
43
|
+
const { totalMs, persistMs, coverageScanMs } = d.postRun;
|
|
44
|
+
out.push({
|
|
45
|
+
severity: "warn",
|
|
46
|
+
title: `Post-run work took ${fmt(totalMs)} (persist ${fmt(persistMs)}, coverage scan ${fmt(coverageScanMs)})`,
|
|
47
|
+
detail: "Time vitest-agent spent after Vitest finished. Set persist: false where history is not needed (e.g. CI), or narrow coverage.include."
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
for (const hint of d.performanceHints) out.push({
|
|
51
|
+
severity: "info",
|
|
52
|
+
title: hint.project !== void 0 ? `${hint.project}: ${hint.message}` : hint.message,
|
|
53
|
+
detail: `${hint.remedy} — ${hint.detail} (${hint.learnMoreUrl})`
|
|
54
|
+
});
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export { POST_RUN_SLOW_MIN_MS, POST_RUN_SLOW_MS, POST_RUN_SLOW_SHARE, isPostRunSlow, suggestedActionsFromRunDiagnostics };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// src/utils/vitest-config-files.ts
|
|
2
|
+
/**
|
|
3
|
+
* The config filenames Vitest 5 itself loads, in the order it prefers them:
|
|
4
|
+
* `vitest.config.*` before `vite.config.*`, each across
|
|
5
|
+
* `ts` / `mts` / `cts` / `js` / `mjs` / `cjs`. Mirrors `configFiles` in
|
|
6
|
+
* Vitest's own `constants.ts`. `vitest.workspace.*` is absent on purpose:
|
|
7
|
+
* Vitest no longer reads a workspace file (projects live in the config's
|
|
8
|
+
* `test.projects`).
|
|
9
|
+
*
|
|
10
|
+
* The single source both front ends derive their config lookups from, so the
|
|
11
|
+
* CLI's `check-test-path` and the MCP `run_tests` root walk cannot drift.
|
|
12
|
+
*
|
|
13
|
+
* @public
|
|
14
|
+
*/
|
|
15
|
+
const VITEST_CONFIG_FILES = ["vitest.config", "vite.config"].flatMap((name) => [
|
|
16
|
+
"ts",
|
|
17
|
+
"mts",
|
|
18
|
+
"cts",
|
|
19
|
+
"js",
|
|
20
|
+
"mjs",
|
|
21
|
+
"cjs"
|
|
22
|
+
].map((ext) => `${name}.${ext}`));
|
|
23
|
+
|
|
24
|
+
export { VITEST_CONFIG_FILES };
|
package/version.js
CHANGED
package/utils/compress-lines.js
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
// src/utils/compress-lines.ts
|
|
2
|
-
/**
|
|
3
|
-
* Compress an array of line numbers into a compact range string.
|
|
4
|
-
*
|
|
5
|
-
* Consecutive line numbers are collapsed into `start-end` ranges,
|
|
6
|
-
* separated by commas. Duplicate values are removed and the input
|
|
7
|
-
* is sorted ascending before processing.
|
|
8
|
-
*
|
|
9
|
-
* @param lines - Array of line numbers to compress
|
|
10
|
-
* @returns Compressed range string, or empty string for empty input
|
|
11
|
-
*
|
|
12
|
-
* @example
|
|
13
|
-
* ```typescript
|
|
14
|
-
* import { compressLines } from "@vitest-agent/sdk/utils";
|
|
15
|
-
*
|
|
16
|
-
* compressLines([1, 2, 3, 5, 10, 11, 12]);
|
|
17
|
-
* // Returns: "1-3,5,10-12"
|
|
18
|
-
*
|
|
19
|
-
* compressLines([42]);
|
|
20
|
-
* // Returns: "42"
|
|
21
|
-
*
|
|
22
|
-
* compressLines([]);
|
|
23
|
-
* // Returns: ""
|
|
24
|
-
* ```
|
|
25
|
-
*
|
|
26
|
-
* @public
|
|
27
|
-
*/
|
|
28
|
-
function compressLines(lines) {
|
|
29
|
-
if (lines.length === 0) return "";
|
|
30
|
-
const sorted = [...new Set(lines)].sort((a, b) => a - b);
|
|
31
|
-
const ranges = [];
|
|
32
|
-
let start = sorted[0];
|
|
33
|
-
let end = sorted[0];
|
|
34
|
-
for (let i = 1; i < sorted.length; i++) if (sorted[i] === end + 1) end = sorted[i];
|
|
35
|
-
else {
|
|
36
|
-
ranges.push(start === end ? `${start}` : `${start}-${end}`);
|
|
37
|
-
start = sorted[i];
|
|
38
|
-
end = sorted[i];
|
|
39
|
-
}
|
|
40
|
-
ranges.push(start === end ? `${start}` : `${start}-${end}`);
|
|
41
|
-
return ranges.join(",");
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export { compressLines };
|
package/utils/detect-pm.js
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
// src/utils/detect-pm.ts
|
|
2
|
-
const LOCKFILE_MAP = [
|
|
3
|
-
["pnpm-lock.yaml", "pnpm"],
|
|
4
|
-
["package-lock.json", "npm"],
|
|
5
|
-
["yarn.lock", "yarn"],
|
|
6
|
-
["bun.lock", "bun"]
|
|
7
|
-
];
|
|
8
|
-
const RUN_COMMANDS = {
|
|
9
|
-
pnpm: "pnpm vitest run",
|
|
10
|
-
npm: "npx vitest run",
|
|
11
|
-
yarn: "yarn vitest run",
|
|
12
|
-
bun: "bun vitest run"
|
|
13
|
-
};
|
|
14
|
-
/**
|
|
15
|
-
* Detect the package manager used in a project by inspecting the root directory.
|
|
16
|
-
*
|
|
17
|
-
* @remarks
|
|
18
|
-
* Detection order:
|
|
19
|
-
* 1. The `packageManager` field in `package.json` (e.g., `"pnpm\@10.32.1"`)
|
|
20
|
-
* 2. Lockfile presence: `pnpm-lock.yaml`, `package-lock.json`, `yarn.lock`, `bun.lock`
|
|
21
|
-
*
|
|
22
|
-
* Returns `null` when no package manager can be determined.
|
|
23
|
-
*
|
|
24
|
-
* @param root - Absolute path to the project root directory
|
|
25
|
-
* @param fs - Filesystem adapter for reading files and checking existence
|
|
26
|
-
* @returns The detected package manager, or `null`
|
|
27
|
-
*
|
|
28
|
-
* @internal
|
|
29
|
-
*/
|
|
30
|
-
async function detectPackageManager(root, fs) {
|
|
31
|
-
try {
|
|
32
|
-
const raw = await fs.readFile(`${root}/package.json`);
|
|
33
|
-
const pkg = JSON.parse(raw);
|
|
34
|
-
if (pkg.packageManager) {
|
|
35
|
-
const name = pkg.packageManager.split("@")[0];
|
|
36
|
-
if (name in RUN_COMMANDS) return name;
|
|
37
|
-
}
|
|
38
|
-
} catch {}
|
|
39
|
-
for (const [lockfile, pm] of LOCKFILE_MAP) if (await fs.exists(`${root}/${lockfile}`)) return pm;
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Return the vitest run command for the given package manager.
|
|
44
|
-
*
|
|
45
|
-
* Falls back to `"npx vitest run"` when `pm` is `null`.
|
|
46
|
-
*
|
|
47
|
-
* @param pm - Detected package manager, or `null` for unknown
|
|
48
|
-
* @returns Shell command string for running vitest
|
|
49
|
-
*
|
|
50
|
-
* @internal
|
|
51
|
-
*/
|
|
52
|
-
function getRunCommand(pm) {
|
|
53
|
-
if (!pm) return "npx vitest run";
|
|
54
|
-
return RUN_COMMANDS[pm];
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export { detectPackageManager, getRunCommand };
|
package/utils/format-console.js
DELETED
|
@@ -1,222 +0,0 @@
|
|
|
1
|
-
import { ansi } from "./ansi.js";
|
|
2
|
-
import { formatScopedCoverageNote } from "./format-scoped-coverage-note.js";
|
|
3
|
-
|
|
4
|
-
// src/utils/format-console.ts
|
|
5
|
-
/**
|
|
6
|
-
* Return the metric with the lowest percentage from a `FileCoverageReport`.
|
|
7
|
-
*
|
|
8
|
-
* @remarks
|
|
9
|
-
* Used to surface the single worst coverage metric per file in console output,
|
|
10
|
-
* keeping the display compact. Labels are abbreviated: `"Stmts"`, `"Branch"`,
|
|
11
|
-
* `"Funcs"`, `"Lines"`.
|
|
12
|
-
*
|
|
13
|
-
* @param f - File coverage report to analyze
|
|
14
|
-
* @returns Object with `label` and `pct` of the worst metric
|
|
15
|
-
*
|
|
16
|
-
* @internal
|
|
17
|
-
*/
|
|
18
|
-
function getWorstMetric(f) {
|
|
19
|
-
return [
|
|
20
|
-
{
|
|
21
|
-
label: "Stmts",
|
|
22
|
-
pct: f.summary.statements
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
label: "Branch",
|
|
26
|
-
pct: f.summary.branches
|
|
27
|
-
},
|
|
28
|
-
{
|
|
29
|
-
label: "Funcs",
|
|
30
|
-
pct: f.summary.functions
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
label: "Lines",
|
|
34
|
-
pct: f.summary.lines
|
|
35
|
-
}
|
|
36
|
-
].reduce((worst, m) => m.pct < worst.pct ? m : worst);
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* Strip the working directory prefix from a file path to produce a
|
|
40
|
-
* project-relative path for display.
|
|
41
|
-
*
|
|
42
|
-
* @param filePath - Absolute or relative file path
|
|
43
|
-
* @param cwd - Working directory to strip
|
|
44
|
-
* @returns Project-relative path string
|
|
45
|
-
*
|
|
46
|
-
* @internal
|
|
47
|
-
*/
|
|
48
|
-
function relativePath(filePath, cwd) {
|
|
49
|
-
if (filePath.startsWith(cwd)) {
|
|
50
|
-
const rel = filePath.slice(cwd.length);
|
|
51
|
-
return rel.startsWith("/") ? rel.slice(1) : rel;
|
|
52
|
-
}
|
|
53
|
-
return filePath;
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Determine the output tier based on test results and coverage state.
|
|
57
|
-
*
|
|
58
|
-
* - `"red"` -- failures, unhandled errors, or files below threshold
|
|
59
|
-
* - `"yellow"` -- all tests pass but coverage targets exist (room to improve)
|
|
60
|
-
* - `"green"` -- all tests pass, all coverage targets met
|
|
61
|
-
*
|
|
62
|
-
* @internal
|
|
63
|
-
*/
|
|
64
|
-
function determineTier(report) {
|
|
65
|
-
if (report.summary.failed > 0 || report.unhandledErrors.length > 0) return "red";
|
|
66
|
-
if (report.coverage) {
|
|
67
|
-
if (report.coverage.lowCoverage.length > 0) return "red";
|
|
68
|
-
if (report.coverage.targets && report.coverage.belowTarget && report.coverage.belowTarget.length > 0) return "yellow";
|
|
69
|
-
}
|
|
70
|
-
return "green";
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* Format an {@link (AgentReport:variable)} as compact, actionable console markdown.
|
|
74
|
-
*
|
|
75
|
-
* @remarks
|
|
76
|
-
* The output is structured for LLM agent consumption with four sections:
|
|
77
|
-
*
|
|
78
|
-
* 1. **Compact header** with pass/fail counts and duration
|
|
79
|
-
* 2. **Failed test details** with error messages and diffs (the core value)
|
|
80
|
-
* 3. **Coverage gaps** showing only files below threshold, worst metric first
|
|
81
|
-
* 4. **Next steps** with re-run commands and cache file pointer
|
|
82
|
-
*
|
|
83
|
-
* When all tests pass, the output collapses to a single success line
|
|
84
|
-
* with an optional cache file reference.
|
|
85
|
-
*
|
|
86
|
-
* Returns an empty string when `consoleOutput` is `"silent"`.
|
|
87
|
-
*
|
|
88
|
-
* @param report - The report to format
|
|
89
|
-
* @param options - Formatting options controlling verbosity and color
|
|
90
|
-
* @returns Formatted markdown string, or empty string for silent mode
|
|
91
|
-
*
|
|
92
|
-
* @internal
|
|
93
|
-
*/
|
|
94
|
-
function formatConsoleMarkdown(report, options) {
|
|
95
|
-
const { consoleOutput, coverageConsoleLimit, noColor, cwd } = options;
|
|
96
|
-
if (consoleOutput === "silent") return "";
|
|
97
|
-
const ao = { noColor };
|
|
98
|
-
const lines = [];
|
|
99
|
-
const hasFailures = report.summary.failed > 0;
|
|
100
|
-
const duration = Math.round(report.summary.duration);
|
|
101
|
-
const projectLabel = report.project ? ` (${report.project})` : "";
|
|
102
|
-
if (hasFailures) lines.push(`## ${ansi("✗", "red", ao)} Vitest${projectLabel} \u2014 ${report.summary.failed} failed, ${report.summary.passed} passed (${duration}ms)`);
|
|
103
|
-
else lines.push(`## ${ansi("✓", "green", ao)} Vitest${projectLabel} \u2014 ${report.summary.passed} passed (${duration}ms)`);
|
|
104
|
-
lines.push("");
|
|
105
|
-
if (options.trendSummary && report.coverage) {
|
|
106
|
-
const { direction, runCount, firstMetric } = options.trendSummary;
|
|
107
|
-
const tier = determineTier(report);
|
|
108
|
-
if (tier === "green") {
|
|
109
|
-
const trendNote = `Coverage: all targets met | trending ${direction} over ${runCount} runs`;
|
|
110
|
-
lines[0] = `${lines[0]} — ${trendNote}`;
|
|
111
|
-
} else if (tier === "yellow") {
|
|
112
|
-
const belowTargetCount = report.coverage.belowTarget?.length ?? 0;
|
|
113
|
-
let summary = `Coverage: ${belowTargetCount} file${belowTargetCount !== 1 ? "s" : ""} below target`;
|
|
114
|
-
summary += ` | trending ${direction} over ${runCount} runs`;
|
|
115
|
-
if (firstMetric) {
|
|
116
|
-
summary += ` (${firstMetric.name}: ${firstMetric.from}% -> ${firstMetric.to}%`;
|
|
117
|
-
if (firstMetric.target) summary += `, target: ${firstMetric.target}%`;
|
|
118
|
-
summary += ")";
|
|
119
|
-
}
|
|
120
|
-
lines.push(summary);
|
|
121
|
-
lines.push("");
|
|
122
|
-
} else {
|
|
123
|
-
let summary = "Coverage:";
|
|
124
|
-
if (report.coverage.lowCoverage.length > 0) summary += ` ${report.coverage.lowCoverage.length} file${report.coverage.lowCoverage.length !== 1 ? "s" : ""} below threshold`;
|
|
125
|
-
summary += ` | trending ${direction} over ${runCount} runs`;
|
|
126
|
-
if (firstMetric) summary += ` (${firstMetric.name}: ${firstMetric.from}% -> ${firstMetric.to}%)`;
|
|
127
|
-
lines.push(summary);
|
|
128
|
-
lines.push("");
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
if (hasFailures) for (const mod of report.failed) {
|
|
132
|
-
const file = relativePath(mod.file, cwd);
|
|
133
|
-
lines.push(`### ${ansi("✗", "red", ao)} \`${file}\``);
|
|
134
|
-
lines.push("");
|
|
135
|
-
if (mod.errors && mod.errors.length > 0) for (const err of mod.errors) {
|
|
136
|
-
lines.push(ansi(err.message, "red", ao));
|
|
137
|
-
if (err.diff) {
|
|
138
|
-
lines.push("");
|
|
139
|
-
lines.push("```diff");
|
|
140
|
-
lines.push(err.diff);
|
|
141
|
-
lines.push("```");
|
|
142
|
-
}
|
|
143
|
-
lines.push("");
|
|
144
|
-
}
|
|
145
|
-
const failedTests = mod.tests.filter((t) => t.state === "failed");
|
|
146
|
-
for (const test of failedTests) {
|
|
147
|
-
const label = test.classification ? ` [${test.classification}]` : "";
|
|
148
|
-
lines.push(`- ${ansi("✗", "red", ao)} **${test.fullName}**${label}`);
|
|
149
|
-
if (test.errors && test.errors.length > 0) for (const err of test.errors) {
|
|
150
|
-
lines.push(` ${ansi(err.message, "dim", ao)}`);
|
|
151
|
-
if (err.diff) {
|
|
152
|
-
lines.push("");
|
|
153
|
-
lines.push(" ```diff");
|
|
154
|
-
for (const diffLine of err.diff.split("\n")) lines.push(` ${diffLine}`);
|
|
155
|
-
lines.push(" ```");
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
lines.push("");
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
if (report.coverage) {
|
|
162
|
-
const cov = report.coverage;
|
|
163
|
-
const globalThresholds = cov.thresholds.global;
|
|
164
|
-
if (cov.scoped) {
|
|
165
|
-
lines.push(formatScopedCoverageNote(cov.scopedFiles?.length ?? 0, cov.totalFiles));
|
|
166
|
-
lines.push("");
|
|
167
|
-
}
|
|
168
|
-
const filesToShow = cov.lowCoverage.slice(0, coverageConsoleLimit);
|
|
169
|
-
if (filesToShow.length > 0) {
|
|
170
|
-
lines.push(`### Coverage gaps`);
|
|
171
|
-
lines.push("");
|
|
172
|
-
for (const f of filesToShow) {
|
|
173
|
-
const file = relativePath(f.file, cwd);
|
|
174
|
-
const worst = getWorstMetric(f);
|
|
175
|
-
const thresholdValue = globalThresholds[worst.label === "Stmts" ? "statements" : worst.label === "Branch" ? "branches" : worst.label === "Funcs" ? "functions" : "lines"];
|
|
176
|
-
const thresholdSuffix = thresholdValue != null ? ` (threshold: ${thresholdValue}%)` : "";
|
|
177
|
-
lines.push(`- \`${file}\` \u2014 ${worst.label}: ${worst.pct}%${thresholdSuffix} \u2014 uncovered: ${f.uncoveredLines}`);
|
|
178
|
-
}
|
|
179
|
-
lines.push("");
|
|
180
|
-
}
|
|
181
|
-
if (cov.targets && cov.belowTarget && cov.belowTarget.length > 0) {
|
|
182
|
-
lines.push(`### Coverage improvements needed`);
|
|
183
|
-
lines.push("");
|
|
184
|
-
for (const f of cov.belowTarget.slice(0, coverageConsoleLimit)) {
|
|
185
|
-
const file = relativePath(f.file, cwd);
|
|
186
|
-
const worst = getWorstMetric(f);
|
|
187
|
-
const metricKey = worst.label === "Stmts" ? "statements" : worst.label === "Branch" ? "branches" : worst.label === "Funcs" ? "functions" : "lines";
|
|
188
|
-
const targetValue = cov.targets.global[metricKey];
|
|
189
|
-
const targetSuffix = targetValue != null ? ` (target: ${targetValue}%)` : "";
|
|
190
|
-
lines.push(`- \`${file}\` \u2014 ${worst.label}: ${worst.pct}%${targetSuffix} \u2014 uncovered: ${f.uncoveredLines}`);
|
|
191
|
-
}
|
|
192
|
-
lines.push("");
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
if (hasFailures || report.coverage && report.coverage.lowCoverage.length > 0) {
|
|
196
|
-
lines.push("### Next steps");
|
|
197
|
-
lines.push("");
|
|
198
|
-
if (hasFailures) {
|
|
199
|
-
const allFailedTests = report.failed.flatMap((m) => m.tests.filter((t) => t.state === "failed"));
|
|
200
|
-
const newFailures = allFailedTests.filter((t) => t.classification === "new-failure");
|
|
201
|
-
const persistent = allFailedTests.filter((t) => t.classification === "persistent");
|
|
202
|
-
const flaky = allFailedTests.filter((t) => t.classification === "flaky");
|
|
203
|
-
if (newFailures.length > 0) {
|
|
204
|
-
const files = [...new Set(report.failed.filter((m) => m.tests.some((t) => t.classification === "new-failure")).map((m) => relativePath(m.file, cwd)))];
|
|
205
|
-
lines.push(`- Fix ${newFailures.length} new failure${newFailures.length > 1 ? "s" : ""} in ${files.map((f) => `\`${f}\``).join(", ")} (likely caused by recent changes)`);
|
|
206
|
-
}
|
|
207
|
-
if (persistent.length > 0) lines.push(`- ${persistent.length} persistent failure${persistent.length > 1 ? "s" : ""} (pre-existing, may not be yours)`);
|
|
208
|
-
if (flaky.length > 0) lines.push(`- ${flaky.length} flaky test${flaky.length > 1 ? "s" : ""} (may pass on retry)`);
|
|
209
|
-
for (const file of report.failedFiles) lines.push(`- Re-run: \`vitest run ${relativePath(file, cwd)}\``);
|
|
210
|
-
if (options.mcp) {
|
|
211
|
-
lines.push("- Use `test_history` for failure trends");
|
|
212
|
-
lines.push("- Use `test_errors` to search errors by type");
|
|
213
|
-
lines.push("- Use `test_coverage` for coverage gap analysis");
|
|
214
|
-
lines.push("- Use `note({ action: \"create\" })` to record debugging findings");
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
lines.push("");
|
|
218
|
-
}
|
|
219
|
-
return lines.join("\n");
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
export { determineTier, formatConsoleMarkdown, getWorstMetric, relativePath };
|