@vitest-agent/sdk 2.4.13 → 2.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dispatch.d.ts +4 -4
- package/index.d.ts +676 -458
- package/index.js +5 -3
- package/layers/DataReaderLive.js +101 -23
- package/layers/DataStoreLive.js +54 -6
- package/migrations/0001_initial.js +6 -2
- package/package.json +11 -11
- package/schemas/ConsoleLeaks.js +20 -3
- package/schemas/Coverage.js +1 -0
- package/schemas/RenderState.js +14 -3
- package/schemas/RunEvent.js +12 -2
- package/testing.d.ts +169 -52
- package/utils/console-leaks.js +42 -15
- package/utils/detect-non-default-discover-strategy.js +38 -0
- package/utils/format-console.js +5 -0
- package/utils/format-scoped-coverage-note.js +25 -0
- package/utils/format-terminal.js +15 -2
- package/utils/validate-phase-transition.js +20 -6
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
//#region src/utils/detect-non-default-discover-strategy.ts
|
|
2
|
+
/**
|
|
3
|
+
* Cheaply strips `//` line comments and `/* *\/` block comments from `source`.
|
|
4
|
+
*
|
|
5
|
+
* Not a full lexer — a marker string that happens to sit inside a string
|
|
6
|
+
* literal containing comment-like syntax could slip through uncaught. That
|
|
7
|
+
* false positive is acceptable: {@link detectNonDefaultDiscoverStrategy} is
|
|
8
|
+
* only ever used to decide whether to fail open, and failing open is the
|
|
9
|
+
* safe direction.
|
|
10
|
+
*/
|
|
11
|
+
function stripComments(source) {
|
|
12
|
+
return source.replace(/\/\*[^*]*\*+(?:[^/*][^*]*\*+)*\//g, "").replace(/\/\/.*$/gm, "");
|
|
13
|
+
}
|
|
14
|
+
const DISCOVER_STRATEGY_OPTION_RE = /\bdiscoverStrategy\s*:/;
|
|
15
|
+
const ADD_PROJECT_MARKER = ".addProject(";
|
|
16
|
+
const EXTENDS_DEFAULT_STRATEGY_RE = /\bextends\s+DefaultDiscoverStrategy\b/;
|
|
17
|
+
const IMPLEMENTS_STRATEGY_RE = /\bimplements\s+DiscoverStrategy\b/;
|
|
18
|
+
/**
|
|
19
|
+
* Lexically detects whether a Vitest/Vite config source text appears to
|
|
20
|
+
* configure a non-default `DiscoverStrategy` — a custom `discoverStrategy`
|
|
21
|
+
* option (including `discoverStrategy: false`), an `AgentPlugin.discover()`
|
|
22
|
+
* `.addProject(...)` chain, or a class extending `DefaultDiscoverStrategy` /
|
|
23
|
+
* implementing `DiscoverStrategy`.
|
|
24
|
+
*
|
|
25
|
+
* Pure and comment-tolerant (best-effort): a marker mentioned only inside a
|
|
26
|
+
* comment may still register as a false positive if it isn't caught by the
|
|
27
|
+
* comment-stripping pass. That is intentional — this function only ever
|
|
28
|
+
* decides whether to fail open, so a false positive is harmless while a
|
|
29
|
+
* false negative would produce a confidently wrong deny.
|
|
30
|
+
* @public
|
|
31
|
+
*/
|
|
32
|
+
function detectNonDefaultDiscoverStrategy(source) {
|
|
33
|
+
const stripped = stripComments(source);
|
|
34
|
+
return DISCOVER_STRATEGY_OPTION_RE.test(stripped) || stripped.includes(ADD_PROJECT_MARKER) || EXTENDS_DEFAULT_STRATEGY_RE.test(stripped) || IMPLEMENTS_STRATEGY_RE.test(stripped);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
//#endregion
|
|
38
|
+
export { detectNonDefaultDiscoverStrategy };
|
package/utils/format-console.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ansi } from "./ansi.js";
|
|
2
|
+
import { formatScopedCoverageNote } from "./format-scoped-coverage-note.js";
|
|
2
3
|
|
|
3
4
|
//#region src/utils/format-console.ts
|
|
4
5
|
/**
|
|
@@ -161,6 +162,10 @@ function formatConsoleMarkdown(report, options) {
|
|
|
161
162
|
if (report.coverage) {
|
|
162
163
|
const cov = report.coverage;
|
|
163
164
|
const globalThresholds = cov.thresholds.global;
|
|
165
|
+
if (cov.scoped) {
|
|
166
|
+
lines.push(formatScopedCoverageNote(cov.scopedFiles?.length ?? 0, cov.totalFiles));
|
|
167
|
+
lines.push("");
|
|
168
|
+
}
|
|
164
169
|
const filesToShow = cov.lowCoverage.slice(0, coverageConsoleLimit);
|
|
165
170
|
if (filesToShow.length > 0) {
|
|
166
171
|
lines.push(`### Coverage gaps`);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//#region src/utils/format-scoped-coverage-note.ts
|
|
2
|
+
/**
|
|
3
|
+
* Format the informational note shown when coverage thresholds were
|
|
4
|
+
* skipped because a run only exercised a subset of the project's test
|
|
5
|
+
* files (issue #160). Vitest's coverage provider enforces
|
|
6
|
+
* `coverage.thresholds` against the whole-project denominator
|
|
7
|
+
* regardless of how many files actually ran, so callers that detect a
|
|
8
|
+
* scoped/partial run suppress the native threshold check and surface
|
|
9
|
+
* this note instead so the agent understands why no pass/fail verdict
|
|
10
|
+
* was rendered for coverage.
|
|
11
|
+
*
|
|
12
|
+
* @param testedFileCount - number of test files that actually ran
|
|
13
|
+
* @param totalFileCount - total number of test files in the project, when
|
|
14
|
+
* known. Some callers (formatters working from a persisted
|
|
15
|
+
* `CoverageReport`, which tracks only the tested-file list) cannot supply
|
|
16
|
+
* this; the note degrades to `"(N test files)"` when omitted.
|
|
17
|
+
* @returns the note text, e.g. `"Coverage thresholds skipped: partial run (2 of 47 test files)"`
|
|
18
|
+
* @public
|
|
19
|
+
*/
|
|
20
|
+
function formatScopedCoverageNote(testedFileCount, totalFileCount) {
|
|
21
|
+
return `Coverage thresholds skipped: partial run (${totalFileCount !== void 0 ? `${testedFileCount} of ${totalFileCount}` : `${testedFileCount}`} test files)`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
//#endregion
|
|
25
|
+
export { formatScopedCoverageNote };
|
package/utils/format-terminal.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ansi } from "./ansi.js";
|
|
2
|
+
import { formatScopedCoverageNote } from "./format-scoped-coverage-note.js";
|
|
2
3
|
import { relativePath } from "./format-console.js";
|
|
3
4
|
import { compressLines } from "./compress-lines.js";
|
|
4
5
|
|
|
@@ -257,6 +258,9 @@ const aggregateCoverage = (reports) => {
|
|
|
257
258
|
let hasCoverage = false;
|
|
258
259
|
let thresholdsGlobal;
|
|
259
260
|
let targetsGlobal;
|
|
261
|
+
let scoped = false;
|
|
262
|
+
let scopedFileCount = 0;
|
|
263
|
+
let totalFiles;
|
|
260
264
|
const belowTargetByFile = /* @__PURE__ */ new Map();
|
|
261
265
|
const belowThresholdByFile = /* @__PURE__ */ new Map();
|
|
262
266
|
for (const r of reports) {
|
|
@@ -265,6 +269,11 @@ const aggregateCoverage = (reports) => {
|
|
|
265
269
|
hasCoverage = true;
|
|
266
270
|
thresholdsGlobal ??= cov.thresholds.global;
|
|
267
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
|
+
}
|
|
268
277
|
if (cov.lowCoverage) {
|
|
269
278
|
for (const f of cov.lowCoverage) if (!belowThresholdByFile.has(f.file)) belowThresholdByFile.set(f.file, f);
|
|
270
279
|
}
|
|
@@ -279,8 +288,11 @@ const aggregateCoverage = (reports) => {
|
|
|
279
288
|
thresholdsMet: belowThresholdCount === 0,
|
|
280
289
|
belowThresholdCount,
|
|
281
290
|
belowTargetFiles,
|
|
291
|
+
scoped,
|
|
292
|
+
scopedFileCount,
|
|
282
293
|
...thresholdsGlobal !== void 0 ? { thresholdsGlobal } : {},
|
|
283
|
-
...targetsGlobal !== void 0 ? { targetsGlobal } : {}
|
|
294
|
+
...targetsGlobal !== void 0 ? { targetsGlobal } : {},
|
|
295
|
+
...totalFiles !== void 0 ? { totalFiles } : {}
|
|
284
296
|
};
|
|
285
297
|
};
|
|
286
298
|
/**
|
|
@@ -294,7 +306,8 @@ const renderCoverageSection = (reports, options, ao) => {
|
|
|
294
306
|
const lines = [];
|
|
295
307
|
const thresholdSpec = agg.thresholdsGlobal ? formatTargetSpec(agg.thresholdsGlobal) : "";
|
|
296
308
|
const targetSpec = agg.targetsGlobal ? formatTargetSpec(agg.targetsGlobal) : "";
|
|
297
|
-
if (
|
|
309
|
+
if (agg.scoped) lines.push(formatScopedCoverageNote(agg.scopedFileCount, agg.totalFiles));
|
|
310
|
+
else if (!agg.thresholdsMet) {
|
|
298
311
|
const cross = ansi("✗", "red", ao);
|
|
299
312
|
const fileWord = agg.belowThresholdCount === 1 ? "file" : "files";
|
|
300
313
|
lines.push(`Coverage: ${cross} ${agg.belowThresholdCount} ${fileWord} below minimum thresholds${thresholdSpec ? ` (${thresholdSpec})` : ""}`);
|
|
@@ -55,6 +55,19 @@ const validatePhaseTransition = (ctx) => {
|
|
|
55
55
|
humanHint: `Cannot transition from '${ctx.current_phase}' directly to 'green'. The red phase must be entered explicitly first (${ctx.current_phase}→red), then a failing test written and run, then red→green requested with a test_failed_run artifact.`
|
|
56
56
|
}
|
|
57
57
|
};
|
|
58
|
+
if (ctx.requested_phase === "refactor" && ctx.current_phase !== "green" && ctx.current_phase !== "green.fake-it") {
|
|
59
|
+
const canEnterGreen = ctx.current_phase === "red" || ctx.current_phase === "red.triangulate";
|
|
60
|
+
return {
|
|
61
|
+
accepted: false,
|
|
62
|
+
phase: ctx.current_phase,
|
|
63
|
+
denialReason: "refactor_without_passing_run",
|
|
64
|
+
remediation: {
|
|
65
|
+
suggestedTool: "tdd_phase_transition_request",
|
|
66
|
+
suggestedArgs: { requestedPhase: canEnterGreen ? "green" : "red" },
|
|
67
|
+
humanHint: canEnterGreen ? `Cannot transition from '${ctx.current_phase}' directly to 'refactor'. The green phase must be entered first (${ctx.current_phase}→green with a test_failed_run artifact), then green→refactor requested with a test_passed_run artifact.` : `Cannot transition from '${ctx.current_phase}' directly to 'refactor'. Enter red first (${ctx.current_phase}→red), write and run a failing test, request red→green with a test_failed_run artifact, then green→refactor with a test_passed_run artifact.`
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
58
71
|
const isTriangulateGreen = ctx.current_phase === "red.triangulate" && ctx.requested_phase === "green";
|
|
59
72
|
const expected = requiredArtifactForTransition(ctx.current_phase, ctx.requested_phase);
|
|
60
73
|
if (expected === null) return {
|
|
@@ -71,28 +84,29 @@ const validatePhaseTransition = (ctx) => {
|
|
|
71
84
|
humanHint: expected.humanHint
|
|
72
85
|
}
|
|
73
86
|
};
|
|
74
|
-
|
|
87
|
+
const isBatsRunLevel = ctx.cited_artifact.test_case_id === null && (ctx.cited_artifact.suite ?? "vitest") === "bats";
|
|
88
|
+
if (ctx.cited_artifact.test_case_id === null && !isBatsRunLevel) return {
|
|
75
89
|
accepted: false,
|
|
76
90
|
phase: ctx.current_phase,
|
|
77
91
|
denialReason: "missing_artifact_evidence",
|
|
78
92
|
remediation: {
|
|
79
93
|
suggestedTool: "run_tests",
|
|
80
94
|
suggestedArgs: {},
|
|
81
|
-
humanHint: "The cited artifact
|
|
95
|
+
humanHint: "The cited artifact is run-level (test_case_id is null) and its suite is 'vitest', so it cannot be bound to this phase — only bats-suite run-level artifacts are accepted without a specific test. Run a specific failing test via run_tests so the resulting artifact carries a test_case_id, then cite that artifact."
|
|
82
96
|
}
|
|
83
97
|
};
|
|
84
|
-
if (expected.kind === "test_failed_run") {
|
|
85
|
-
if (!isTriangulateGreen && ctx.
|
|
98
|
+
if (expected.kind === "test_failed_run" || isBatsRunLevel && expected.kind === "test_passed_run") {
|
|
99
|
+
if (!isTriangulateGreen && ctx.current_phase_id !== null && ctx.cited_artifact.phase_id !== ctx.current_phase_id) return {
|
|
86
100
|
accepted: false,
|
|
87
101
|
phase: ctx.current_phase,
|
|
88
102
|
denialReason: "evidence_not_in_phase_window",
|
|
89
103
|
remediation: {
|
|
90
104
|
suggestedTool: "run_tests",
|
|
91
105
|
suggestedArgs: {},
|
|
92
|
-
humanHint: "The cited
|
|
106
|
+
humanHint: "The cited artifact was recorded in a different phase than the one currently open. Run the failing test again inside the current phase so a fresh test_failed_run artifact is bound to it."
|
|
93
107
|
}
|
|
94
108
|
};
|
|
95
|
-
if (!ctx.cited_artifact.test_case_authored_in_session) return {
|
|
109
|
+
if (!isBatsRunLevel && !ctx.cited_artifact.test_case_authored_in_session) return {
|
|
96
110
|
accepted: false,
|
|
97
111
|
phase: ctx.current_phase,
|
|
98
112
|
denialReason: "evidence_not_in_phase_window",
|