@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.
@@ -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 };
@@ -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 };
@@ -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 (!agg.thresholdsMet) {
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
- if (ctx.cited_artifact.test_case_id === null) return {
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 has no specific test (test_case_id is null), so it cannot be bound to this phase. Run a specific failing test via run_tests so the resulting artifact carries a test_case_id, then cite that 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.cited_artifact.test_case_created_turn_at !== null && ctx.cited_artifact.test_case_created_turn_at < ctx.phase_started_at) return {
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 test was authored before this phase started. Write a new failing test inside the current phase."
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",