@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
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { Cause } from "effect";
|
|
2
|
-
|
|
3
|
-
// src/utils/format-fatal-error.ts
|
|
4
|
-
const ISSUE_URL = "https://github.com/spencerbeggs/vitest-agent/issues";
|
|
5
|
-
const FIBER_FAILURE_CAUSE_KEY = Symbol.for("effect/Runtime/FiberFailure/Cause");
|
|
6
|
-
/**
|
|
7
|
-
* Format an unknown error into a human-readable string with issue URL.
|
|
8
|
-
*
|
|
9
|
-
* Extraction order:
|
|
10
|
-
* 1. Effect Cause — use `Cause.pretty()` for full formatting
|
|
11
|
-
* 2. Effect FiberFailure — extract Cause and use `Cause.pretty()`
|
|
12
|
-
* 3. Plain Error — extract `.message` and `.stack`
|
|
13
|
-
* 4. Unknown — `String(err)`
|
|
14
|
-
*
|
|
15
|
-
* @public
|
|
16
|
-
*/
|
|
17
|
-
function formatFatalError(err) {
|
|
18
|
-
let detail;
|
|
19
|
-
if (Cause.isCause(err)) detail = Cause.pretty(err);
|
|
20
|
-
else if (err != null && typeof err === "object" && FIBER_FAILURE_CAUSE_KEY in err) {
|
|
21
|
-
const cause = err[FIBER_FAILURE_CAUSE_KEY];
|
|
22
|
-
if (Cause.isCause(cause)) detail = Cause.pretty(cause);
|
|
23
|
-
else detail = String(err);
|
|
24
|
-
} else if (err instanceof Error) try {
|
|
25
|
-
detail = err.stack ?? err.message;
|
|
26
|
-
} catch {
|
|
27
|
-
let ctorName = "Error";
|
|
28
|
-
try {
|
|
29
|
-
ctorName = err.constructor?.name ?? "Error";
|
|
30
|
-
} catch {}
|
|
31
|
-
detail = `<unserializable ${ctorName}>`;
|
|
32
|
-
}
|
|
33
|
-
else if (err !== null && typeof err === "object") try {
|
|
34
|
-
detail = JSON.stringify(err, null, 2);
|
|
35
|
-
} catch {
|
|
36
|
-
try {
|
|
37
|
-
detail = String(err);
|
|
38
|
-
} catch {
|
|
39
|
-
detail = "<unserializable error>";
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
else detail = String(err);
|
|
43
|
-
return `${detail}\nPlease report at ${ISSUE_URL}`;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export { formatFatalError };
|
package/utils/format-gfm.js
DELETED
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
// src/utils/format-gfm.ts
|
|
2
|
-
/**
|
|
3
|
-
* Format a number as a percentage string.
|
|
4
|
-
*
|
|
5
|
-
* @internal
|
|
6
|
-
*/
|
|
7
|
-
function pct(n) {
|
|
8
|
-
return `${n}%`;
|
|
9
|
-
}
|
|
10
|
-
/**
|
|
11
|
-
* Find the worst (lowest) coverage metric for a file.
|
|
12
|
-
*
|
|
13
|
-
* @internal
|
|
14
|
-
*/
|
|
15
|
-
function worstCoverage(f) {
|
|
16
|
-
return [
|
|
17
|
-
{
|
|
18
|
-
label: "Stmts",
|
|
19
|
-
pct: f.summary.statements
|
|
20
|
-
},
|
|
21
|
-
{
|
|
22
|
-
label: "Branch",
|
|
23
|
-
pct: f.summary.branches
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
label: "Funcs",
|
|
27
|
-
pct: f.summary.functions
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
label: "Lines",
|
|
31
|
-
pct: f.summary.lines
|
|
32
|
-
}
|
|
33
|
-
].reduce((worst, m) => m.pct < worst.pct ? m : worst);
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Format the body of a single project's GFM output.
|
|
37
|
-
*
|
|
38
|
-
* @internal
|
|
39
|
-
*/
|
|
40
|
-
function formatProjectBody(report) {
|
|
41
|
-
const lines = [];
|
|
42
|
-
if (report.failed.length > 0) {
|
|
43
|
-
lines.push("## Failed Tests");
|
|
44
|
-
lines.push("");
|
|
45
|
-
for (const mod of report.failed) {
|
|
46
|
-
lines.push(`### \`${mod.file}\``);
|
|
47
|
-
lines.push("");
|
|
48
|
-
if (mod.errors && mod.errors.length > 0) for (const err of mod.errors) {
|
|
49
|
-
lines.push(`> ${err.message}`);
|
|
50
|
-
if (err.diff) {
|
|
51
|
-
lines.push("");
|
|
52
|
-
lines.push("```diff");
|
|
53
|
-
lines.push(err.diff);
|
|
54
|
-
lines.push("```");
|
|
55
|
-
}
|
|
56
|
-
lines.push("");
|
|
57
|
-
}
|
|
58
|
-
const failedTests = mod.tests.filter((t) => t.state === "failed");
|
|
59
|
-
for (const test of failedTests) {
|
|
60
|
-
lines.push(`**${test.fullName}**`);
|
|
61
|
-
lines.push("");
|
|
62
|
-
if (test.errors && test.errors.length > 0) for (const err of test.errors) {
|
|
63
|
-
lines.push(`> ${err.message}`);
|
|
64
|
-
if (err.diff) {
|
|
65
|
-
lines.push("");
|
|
66
|
-
lines.push("```diff");
|
|
67
|
-
lines.push(err.diff);
|
|
68
|
-
lines.push("```");
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
lines.push("");
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
if (report.coverage) {
|
|
76
|
-
const cov = report.coverage;
|
|
77
|
-
lines.push("## Coverage");
|
|
78
|
-
lines.push("");
|
|
79
|
-
lines.push("| Metric | Coverage |");
|
|
80
|
-
lines.push("| --- | --- |");
|
|
81
|
-
lines.push(`| Statements | ${pct(cov.totals.statements)} |`);
|
|
82
|
-
lines.push(`| Branches | ${pct(cov.totals.branches)} |`);
|
|
83
|
-
lines.push(`| Functions | ${pct(cov.totals.functions)} |`);
|
|
84
|
-
lines.push(`| Lines | ${pct(cov.totals.lines)} |`);
|
|
85
|
-
lines.push("");
|
|
86
|
-
if (cov.lowCoverage.length > 0) {
|
|
87
|
-
const thresholdParts = [];
|
|
88
|
-
const g = cov.thresholds.global;
|
|
89
|
-
if (g.lines !== void 0) thresholdParts.push(`lines: ${g.lines}%`);
|
|
90
|
-
if (g.functions !== void 0) thresholdParts.push(`functions: ${g.functions}%`);
|
|
91
|
-
if (g.branches !== void 0) thresholdParts.push(`branches: ${g.branches}%`);
|
|
92
|
-
if (g.statements !== void 0) thresholdParts.push(`statements: ${g.statements}%`);
|
|
93
|
-
const thresholdDisplay = thresholdParts.length > 0 ? thresholdParts.join(", ") : "configured thresholds";
|
|
94
|
-
lines.push("> [!WARNING]");
|
|
95
|
-
lines.push(`> ${cov.lowCoverage.length} file(s) below coverage threshold (${thresholdDisplay}):`);
|
|
96
|
-
lines.push(">");
|
|
97
|
-
for (const f of cov.lowCoverage) {
|
|
98
|
-
const worst = worstCoverage(f);
|
|
99
|
-
lines.push(`> - \`${f.file}\` — ${worst.label} ${pct(worst.pct)}`);
|
|
100
|
-
}
|
|
101
|
-
lines.push("");
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
return lines;
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* Format one or more {@link (AgentReport:variable) | AgentReports} as a GitHub-Flavored
|
|
108
|
-
* Markdown string for writing to `GITHUB_STEP_SUMMARY`.
|
|
109
|
-
*
|
|
110
|
-
* @remarks
|
|
111
|
-
* GFM-specific features used in the output:
|
|
112
|
-
*
|
|
113
|
-
* - **Summary table** with pass/fail counts across all projects
|
|
114
|
-
* - **Collapsible `<details>` blocks** for per-project results in monorepo setups
|
|
115
|
-
* - **Tables** for coverage metrics
|
|
116
|
-
* - **Diff-fenced code blocks** for expected/received comparison output
|
|
117
|
-
* - **GitHub Alert callouts** (`[!WARNING]`) for coverage threshold violations
|
|
118
|
-
*
|
|
119
|
-
* For a single report, the project body is rendered directly without
|
|
120
|
-
* a `<details>` wrapper. For multiple reports (monorepo), each project
|
|
121
|
-
* gets its own collapsible section with a status icon in the summary line.
|
|
122
|
-
*
|
|
123
|
-
* @param reports - One or more agent reports to format
|
|
124
|
-
* @returns GFM-formatted string ready for appending to `GITHUB_STEP_SUMMARY`
|
|
125
|
-
*
|
|
126
|
-
* @public
|
|
127
|
-
*/
|
|
128
|
-
function formatGfm(reports) {
|
|
129
|
-
const lines = [];
|
|
130
|
-
const headerIcon = reports.some((r) => r.summary.failed > 0) ? "❌" : "✅";
|
|
131
|
-
lines.push(`# ${headerIcon} Vitest Results`);
|
|
132
|
-
lines.push("");
|
|
133
|
-
const totalPassed = reports.reduce((acc, r) => acc + r.summary.passed, 0);
|
|
134
|
-
const totalFailed = reports.reduce((acc, r) => acc + r.summary.failed, 0);
|
|
135
|
-
const totalTests = reports.reduce((acc, r) => acc + r.summary.total, 0);
|
|
136
|
-
lines.push("| | Count |");
|
|
137
|
-
lines.push("| --- | --- |");
|
|
138
|
-
lines.push(`| Passed | **${totalPassed}** |`);
|
|
139
|
-
lines.push(`| Failed | **${totalFailed}** |`);
|
|
140
|
-
lines.push(`| Total | **${totalTests}** |`);
|
|
141
|
-
lines.push("");
|
|
142
|
-
if (reports.length > 1) for (const report of reports) {
|
|
143
|
-
const projectLabel = report.project ?? "default";
|
|
144
|
-
const statusIcon = report.summary.failed > 0 ? "❌" : "✅";
|
|
145
|
-
lines.push(`<details>`);
|
|
146
|
-
lines.push(`<summary>${statusIcon} ${projectLabel}</summary>`);
|
|
147
|
-
lines.push("");
|
|
148
|
-
const bodyLines = formatProjectBody(report);
|
|
149
|
-
lines.push(...bodyLines);
|
|
150
|
-
lines.push("</details>");
|
|
151
|
-
lines.push("");
|
|
152
|
-
}
|
|
153
|
-
else if (reports.length === 1) {
|
|
154
|
-
const bodyLines = formatProjectBody(reports[0]);
|
|
155
|
-
lines.push(...bodyLines);
|
|
156
|
-
}
|
|
157
|
-
return lines.join("\n");
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export { formatGfm };
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
// src/utils/normalize-workspace-key.ts
|
|
2
|
-
/**
|
|
3
|
-
* Normalize a workspace name into a filesystem-safe directory segment.
|
|
4
|
-
*
|
|
5
|
-
* The workspace key drives the per-project subdirectory under the XDG data
|
|
6
|
-
* directory (e.g. `$XDG_DATA_HOME/vitest-agent/<key>/data.db`). The
|
|
7
|
-
* key must be deterministic for a given input and safe to use as a single
|
|
8
|
-
* path segment on macOS, Linux, and Windows.
|
|
9
|
-
*
|
|
10
|
-
* Rules (applied in order):
|
|
11
|
-
*
|
|
12
|
-
* 1. Replace `/` with `__` so scoped names like `@org/pkg` collapse to
|
|
13
|
-
* `@org__pkg` rather than introducing a subdirectory boundary. Scoped
|
|
14
|
-
* npm names contain at most one `/` (between scope and name), and
|
|
15
|
-
* leading-`@` requires the scope/name form, so `@org__pkg` is not a
|
|
16
|
-
* valid unscoped npm name -- the `__` token is unambiguous in
|
|
17
|
-
* practice.
|
|
18
|
-
* 2. Replace any character outside `[A-Za-z0-9._@-]` with `_`. This catches
|
|
19
|
-
* Windows-reserved characters (`\`, `:`, `*`, `?`, `"`, `<`, `>`, `|`),
|
|
20
|
-
* control characters, whitespace, and anything else that varies in
|
|
21
|
-
* interpretation across filesystems.
|
|
22
|
-
* 3. Collapse runs of underscores produced by the substitution above into a
|
|
23
|
-
* single `_` so the output stays compact.
|
|
24
|
-
*
|
|
25
|
-
* @param name - The raw `name` field from a workspace's `package.json`.
|
|
26
|
-
* @returns A filesystem-safe single path segment.
|
|
27
|
-
* @public
|
|
28
|
-
*/
|
|
29
|
-
function normalizeWorkspaceKey(name) {
|
|
30
|
-
return name.replaceAll("/", "__").replace(/[^A-Za-z0-9._@-]/g, "_").replace(/_{3,}/g, "__");
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export { normalizeWorkspaceKey };
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
// src/utils/probe-host-metadata.ts
|
|
2
|
-
const isUseful = (value) => value !== void 0 && value.length > 0;
|
|
3
|
-
/**
|
|
4
|
-
* Walk the priority chain of env-var probes. Returns the first match
|
|
5
|
-
* along with any decorating metadata (`term_program`, `ci_provider`,
|
|
6
|
-
* etc.) for that source. When no probe matches, `source`/`value`/
|
|
7
|
-
* `metadata` are all `null`.
|
|
8
|
-
*
|
|
9
|
-
* The probe order matches the plan's table exactly:
|
|
10
|
-
*
|
|
11
|
-
* 1. `TMUX_PANE` (tmux reattach-stable)
|
|
12
|
-
* 2. `WT_SESSION` (Windows Terminal pane)
|
|
13
|
-
* 3. `WEZTERM_PANE` (WezTerm pane)
|
|
14
|
-
* 4. `KITTY_WINDOW_ID` (kitty window)
|
|
15
|
-
* 5. `TERM_SESSION_ID` (iTerm2, Terminal.app)
|
|
16
|
-
* 6. `VSCODE_INJECTION` + `process.ppid` (handled by the Live layer
|
|
17
|
-
* since it needs `process.ppid`, not env)
|
|
18
|
-
* 7. `GITHUB_RUN_ID` + `GITHUB_RUN_ATTEMPT`
|
|
19
|
-
* 8. `BUILDKITE_JOB_ID` / `CIRCLE_BUILD_NUM` / `GITLAB_CI_JOB_ID`
|
|
20
|
-
* 9. `process.ppid` walk (handled by the Live layer)
|
|
21
|
-
* @public
|
|
22
|
-
*/
|
|
23
|
-
const probeHostMetadataFromEnv = (env) => {
|
|
24
|
-
const decoration = (extra) => {
|
|
25
|
-
const baseDecoration = {};
|
|
26
|
-
if (isUseful(env.TERM_PROGRAM)) baseDecoration.term_program = env.TERM_PROGRAM;
|
|
27
|
-
if (isUseful(env.TERM_PROGRAM_VERSION)) baseDecoration.term_program_version = env.TERM_PROGRAM_VERSION;
|
|
28
|
-
return {
|
|
29
|
-
...baseDecoration,
|
|
30
|
-
...extra
|
|
31
|
-
};
|
|
32
|
-
};
|
|
33
|
-
if (isUseful(env.TMUX_PANE)) return {
|
|
34
|
-
source: "TMUX_PANE",
|
|
35
|
-
value: env.TMUX_PANE,
|
|
36
|
-
metadata: decoration(isUseful(env.TMUX) ? { tmux_socket: env.TMUX } : {})
|
|
37
|
-
};
|
|
38
|
-
if (isUseful(env.WT_SESSION)) return {
|
|
39
|
-
source: "WT_SESSION",
|
|
40
|
-
value: env.WT_SESSION,
|
|
41
|
-
metadata: decoration({})
|
|
42
|
-
};
|
|
43
|
-
if (isUseful(env.WEZTERM_PANE)) return {
|
|
44
|
-
source: "WEZTERM_PANE",
|
|
45
|
-
value: env.WEZTERM_PANE,
|
|
46
|
-
metadata: decoration({})
|
|
47
|
-
};
|
|
48
|
-
if (isUseful(env.KITTY_WINDOW_ID)) return {
|
|
49
|
-
source: "KITTY_WINDOW_ID",
|
|
50
|
-
value: env.KITTY_WINDOW_ID,
|
|
51
|
-
metadata: decoration({})
|
|
52
|
-
};
|
|
53
|
-
if (isUseful(env.TERM_SESSION_ID)) return {
|
|
54
|
-
source: "TERM_SESSION_ID",
|
|
55
|
-
value: env.TERM_SESSION_ID,
|
|
56
|
-
metadata: decoration({})
|
|
57
|
-
};
|
|
58
|
-
if (isUseful(env.GITHUB_RUN_ID)) return {
|
|
59
|
-
source: "GITHUB_RUN_ID",
|
|
60
|
-
value: env.GITHUB_RUN_ID,
|
|
61
|
-
metadata: decoration({
|
|
62
|
-
ci: true,
|
|
63
|
-
ci_provider: "github",
|
|
64
|
-
...isUseful(env.GITHUB_RUN_ATTEMPT) ? { github_run_attempt: env.GITHUB_RUN_ATTEMPT } : {}
|
|
65
|
-
})
|
|
66
|
-
};
|
|
67
|
-
if (isUseful(env.BUILDKITE_JOB_ID)) return {
|
|
68
|
-
source: "BUILDKITE_JOB_ID",
|
|
69
|
-
value: env.BUILDKITE_JOB_ID,
|
|
70
|
-
metadata: decoration({
|
|
71
|
-
ci: true,
|
|
72
|
-
ci_provider: "buildkite"
|
|
73
|
-
})
|
|
74
|
-
};
|
|
75
|
-
if (isUseful(env.CIRCLE_BUILD_NUM)) return {
|
|
76
|
-
source: "CIRCLE_BUILD_NUM",
|
|
77
|
-
value: env.CIRCLE_BUILD_NUM,
|
|
78
|
-
metadata: decoration({
|
|
79
|
-
ci: true,
|
|
80
|
-
ci_provider: "circle"
|
|
81
|
-
})
|
|
82
|
-
};
|
|
83
|
-
if (isUseful(env.GITLAB_CI_JOB_ID)) return {
|
|
84
|
-
source: "GITLAB_CI_JOB_ID",
|
|
85
|
-
value: env.GITLAB_CI_JOB_ID,
|
|
86
|
-
metadata: decoration({
|
|
87
|
-
ci: true,
|
|
88
|
-
ci_provider: "gitlab"
|
|
89
|
-
})
|
|
90
|
-
};
|
|
91
|
-
return {
|
|
92
|
-
source: null,
|
|
93
|
-
value: null,
|
|
94
|
-
metadata: null
|
|
95
|
-
};
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
export { probeHostMetadataFromEnv };
|
package/utils/safe-filename.js
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
// src/utils/safe-filename.ts
|
|
2
|
-
/**
|
|
3
|
-
* Convert a project name to a filesystem-safe filename.
|
|
4
|
-
*
|
|
5
|
-
* Replaces `/` and `:` characters with `__` (double underscore).
|
|
6
|
-
* Returns `"default"` for empty strings, which is used as the
|
|
7
|
-
* fallback project name for single-repo configurations.
|
|
8
|
-
*
|
|
9
|
-
* @param name - Project name to sanitize
|
|
10
|
-
* @returns Filesystem-safe filename string
|
|
11
|
-
*
|
|
12
|
-
* @example
|
|
13
|
-
* ```typescript
|
|
14
|
-
* import { safeFilename } from "@vitest-agent/sdk/utils";
|
|
15
|
-
*
|
|
16
|
-
* safeFilename("\@savvy-web/my-lib:unit");
|
|
17
|
-
* // Returns: "\@savvy-web__my-lib__unit"
|
|
18
|
-
*
|
|
19
|
-
* safeFilename("core");
|
|
20
|
-
* // Returns: "core"
|
|
21
|
-
*
|
|
22
|
-
* safeFilename("");
|
|
23
|
-
* // Returns: "default"
|
|
24
|
-
* ```
|
|
25
|
-
*
|
|
26
|
-
* @public
|
|
27
|
-
*/
|
|
28
|
-
function safeFilename(name) {
|
|
29
|
-
if (!name) return "default";
|
|
30
|
-
return name.replace(/[/:]/g, "__");
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export { safeFilename };
|