@vitest-agent/sdk 2.0.16 → 2.2.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.
@@ -35,18 +35,35 @@ const extractSqlReason = (e) => {
35
35
  while (node && typeof node === "object" && !seen.has(node)) {
36
36
  seen.add(node);
37
37
  const n = node;
38
- if (typeof n.message === "string" && n.message.length > 0) best = n.message;
39
- if (typeof n.cause === "string") {
40
- if (n.cause.length > 0) best = n.cause;
38
+ let msg;
39
+ let cause;
40
+ try {
41
+ msg = n.message;
42
+ } catch {
43
+ msg = void 0;
44
+ }
45
+ try {
46
+ cause = n.cause;
47
+ } catch {
48
+ cause = void 0;
49
+ }
50
+ if (typeof msg === "string" && msg.length > 0) best = msg;
51
+ if (typeof cause === "string") {
52
+ if (cause.length > 0) best = cause;
41
53
  break;
42
54
  }
43
- node = n.cause;
55
+ node = cause;
44
56
  }
45
57
  if (best !== void 0) return best;
46
58
  if (e && typeof e === "object") try {
47
- return JSON.stringify(e);
59
+ const json = JSON.stringify(e);
60
+ if (json !== void 0) return json;
48
61
  } catch {}
49
- return String(e);
62
+ try {
63
+ return String(e);
64
+ } catch {
65
+ return "<unserializable error>";
66
+ }
50
67
  };
51
68
 
52
69
  //#endregion
package/index.d.ts CHANGED
@@ -375,6 +375,16 @@ declare const RenderState: Schema.Struct<{
375
375
  readonly detail: Schema.String;
376
376
  readonly targetTool: Schema.optional<Schema.String>;
377
377
  }>>;
378
+ /**
379
+ * Count of every collected module, passing modules included. Folded
380
+ * from `RunFinished.collectedModules`. Renderers prefer this over
381
+ * `moduleOrder.length` for "N modules all-passed" copy — a report
382
+ * replay only queues failing modules, so `moduleOrder` alone
383
+ * undercounts a green run to zero (issue #204). Optional — a run
384
+ * that never carried the field (older replay data, hand-built
385
+ * fixtures) falls back to `moduleOrder.length`.
386
+ */
387
+ readonly collectedModules: Schema.optional<Schema.Number>;
378
388
  }>;
379
389
  /** @public */
380
390
  type RenderState = typeof RenderState.Type;
@@ -519,6 +529,14 @@ declare const ReportSummary: Schema.Struct<{
519
529
  readonly failed: Schema.Number;
520
530
  readonly skipped: Schema.Number;
521
531
  readonly duration: Schema.Number;
532
+ /**
533
+ * Count of every collected module, passing modules included.
534
+ * `AgentReport.failed` only carries failing modules, so a green run
535
+ * has no other way to know how many files actually ran — without
536
+ * this a fully-passing replay reads as "0 modules all-passed"
537
+ * (issue #204). Optional to keep existing fixtures/reports valid.
538
+ */
539
+ readonly modules: Schema.optional<Schema.Number>;
522
540
  }>;
523
541
  /** @public */
524
542
  type ReportSummary = typeof ReportSummary.Type;
@@ -603,6 +621,14 @@ declare const AgentReport: Schema.Struct<{
603
621
  readonly failed: Schema.Number;
604
622
  readonly skipped: Schema.Number;
605
623
  readonly duration: Schema.Number;
624
+ /**
625
+ * Count of every collected module, passing modules included.
626
+ * `AgentReport.failed` only carries failing modules, so a green run
627
+ * has no other way to know how many files actually ran — without
628
+ * this a fully-passing replay reads as "0 modules all-passed"
629
+ * (issue #204). Optional to keep existing fixtures/reports valid.
630
+ */
631
+ readonly modules: Schema.optional<Schema.Number>;
606
632
  }>;
607
633
  readonly failed: Schema.$Array<Schema.Struct<{
608
634
  readonly file: Schema.String;
@@ -1090,6 +1116,14 @@ declare const RunEvent: Schema.Union<readonly [Schema.TaggedStruct<"RunStarted",
1090
1116
  readonly skipCount: Schema.Number;
1091
1117
  readonly durationMs: Schema.Number;
1092
1118
  readonly timeoutCount: Schema.optional<Schema.Number>;
1119
+ /**
1120
+ * Count of every collected module, passing modules included.
1121
+ * Carries `AgentReport.summary.modules` (or the live module
1122
+ * count) into the event stream so the reducer can fold the
1123
+ * true count into `RenderState` even when `moduleOrder` only
1124
+ * tracks failing modules (report replay). See issue #204.
1125
+ */
1126
+ readonly collectedModules: Schema.optional<Schema.Number>;
1093
1127
  }>]>;
1094
1128
  /** @public */
1095
1129
  type RunEvent = typeof RunEvent.Type;
@@ -4457,6 +4491,12 @@ interface VitestTestSuite {
4457
4491
  line: number;
4458
4492
  column: number;
4459
4493
  };
4494
+ /**
4495
+ * Errors attached to the suite entity itself — a `beforeAll`/`afterAll`
4496
+ * hook throw lands here, not on any individual `TestCase`. Optional
4497
+ * because older duck-typed callers may not supply it.
4498
+ */
4499
+ errors?(): Array<VitestModuleError>;
4460
4500
  }
4461
4501
  /**
4462
4502
  * Duck-typed Vitest TestModule from the Reporter v2 API.
@@ -4564,6 +4604,42 @@ declare function classifyTest(current: "passed" | "failed", priorRuns: ReadonlyA
4564
4604
  state: string;
4565
4605
  }>): TestClassification;
4566
4606
  //#endregion
4607
+ //#region src/utils/coerce-error-text.d.ts
4608
+ /**
4609
+ * Coerce an unknown error-field value (message, name, diff, stack, ...) into a
4610
+ * string that is safe to bind to a SQLite TEXT column or render to a terminal.
4611
+ *
4612
+ * Vitest failure values are typed as strings but are whatever the test threw:
4613
+ * `Effect.flip` on an unexpectedly-succeeding effect puts arbitrary success
4614
+ * values (routinely plain objects) into the error channel, and Effect's
4615
+ * `ConfigError.message` is a getter that throws when its cause lacks
4616
+ * `toString`. Every access here is exception-safe.
4617
+ *
4618
+ * - `undefined` / `null` → `undefined` (caller decides null vs sentinel)
4619
+ * - string → unchanged
4620
+ * - other primitives → `String(value)`
4621
+ * - objects → `JSON.stringify`, falling back to `String(value)`, falling back
4622
+ * to `"<unserializable>"` when both throw
4623
+ *
4624
+ * @public
4625
+ */
4626
+ declare const coerceErrorText: (value: unknown) => string | undefined;
4627
+ /**
4628
+ * Read a named field off an unknown error-like value and coerce it with
4629
+ * {@link coerceErrorText}, guarding the property access itself. A live getter
4630
+ * that throws (Effect's `ConfigError.message` when its cause lacks
4631
+ * `toString`) throws at the read site — `coerceErrorText(e.message)` never
4632
+ * reaches the helper's own exception handling. Route every read of an
4633
+ * untrusted error object's field through this helper instead.
4634
+ *
4635
+ * - `null` / non-object sources → `undefined`
4636
+ * - a getter that throws → `"<unreadable field>"`
4637
+ * - otherwise → `coerceErrorText(value)`
4638
+ *
4639
+ * @public
4640
+ */
4641
+ declare const coerceErrorField: (source: unknown, key: string) => string | undefined;
4642
+ //#endregion
4567
4643
  //#region src/utils/compress-lines.d.ts
4568
4644
  /**
4569
4645
  * Compress an array of line numbers into a compact range string.
@@ -5152,6 +5228,88 @@ declare const resolveWorkspaceKey: (projectDir: string) => Effect.Effect<string,
5152
5228
  */
5153
5229
  declare function safeFilename(name: string): string;
5154
5230
  //#endregion
5231
+ //#region src/utils/test-location.d.ts
5232
+ /** Directory holding source files whose tests may be co-located. @public */
5233
+ declare const SRC_DIR = "src";
5234
+ /** Canonical test directory name. @public */
5235
+ declare const TEST_DIR = "__test__";
5236
+ /** Support directories under `__test__/` that are never collected. @public */
5237
+ declare const TEST_HELPER_DIRS: readonly ["fixtures", "snapshots", "utils"];
5238
+ /** Glob suffix matching every discoverable test file. @public */
5239
+ declare const TEST_FILE_GLOB_SUFFIX = "*.{test,spec}.{ts,tsx,js,jsx}";
5240
+ /**
5241
+ * Directory names discovery never walks into, and therefore never collects
5242
+ * tests from. Keep in step with {@link TEST_FILE_GLOB_SUFFIX}'s extension set:
5243
+ * this is the directory half of the same "what can Vitest reach" question.
5244
+ *
5245
+ * `@vitest-agent/plugin`'s test-file walker and its cache-signature walk both
5246
+ * prune these before recursing (pruning first, rather than filtering results
5247
+ * after, is what keeps a symlinked `node_modules` from dragging in the whole
5248
+ * pnpm store), and {@link classifyTestPath} declines to render a verdict for
5249
+ * any path that crosses one.
5250
+ * @public
5251
+ */
5252
+ declare const NON_DISCOVERABLE_DIRS: ReadonlySet<string>;
5253
+ /**
5254
+ * True when `filePathOrName` ends in a discoverable test-file suffix — the
5255
+ * extension half of the layout rule, matching {@link TEST_FILE_GLOB_SUFFIX}.
5256
+ *
5257
+ * Says nothing about location: a `node_modules` copy of `foo.test.ts` passes
5258
+ * this and is still not discoverable. Pair it with {@link classifyTestPath}
5259
+ * when the answer has to account for where the file sits.
5260
+ * @public
5261
+ */
5262
+ declare function isTestFileName(filePathOrName: string): boolean;
5263
+ /**
5264
+ * Outcome of classifying a path against the test-layout rule.
5265
+ *
5266
+ * - `valid` — discoverable by Vitest.
5267
+ * - `excluded` — deliberately not discovered, and correct where it is.
5268
+ * - `invalid` — not a legal test location.
5269
+ * @public
5270
+ */
5271
+ type TestPathVerdict = "valid" | "excluded" | "invalid";
5272
+ /** Result of {@link classifyTestPath}. @public */
5273
+ interface TestPathClassification {
5274
+ readonly verdict: TestPathVerdict;
5275
+ readonly workspace: string | null;
5276
+ readonly suggestedPath: string | null;
5277
+ }
5278
+ /** Minimal workspace shape {@link classifyTestPath} needs to attribute a path. @public */
5279
+ interface WorkspaceLike {
5280
+ readonly name: string;
5281
+ readonly path: string;
5282
+ }
5283
+ /**
5284
+ * Returns the deepest supplied workspace containing `filePath`, or `null` when
5285
+ * none does. Attribution is by longest matching path, so a nested package wins
5286
+ * over the repository root that contains it.
5287
+ *
5288
+ * Exported because callers that need to reason about a path relative to its
5289
+ * owning package — walking down from the package root, for instance — must
5290
+ * agree with {@link classifyTestPath} about which package owns it.
5291
+ * @public
5292
+ */
5293
+ declare function findOwningWorkspace(workspaces: ReadonlyArray<WorkspaceLike>, filePath: string): WorkspaceLike | null;
5294
+ /**
5295
+ * Classifies `filePath` against the test-layout rule: a test file is
5296
+ * discoverable only at `<workspace>/src/**` or `<workspace>/__test__/**`,
5297
+ * excluding the helper directories under `__test__/`.
5298
+ *
5299
+ * Returns `null` when no supplied workspace contains the path, and when the
5300
+ * path crosses one of the {@link NON_DISCOVERABLE_DIRS} discovery never walks
5301
+ * into. Neither is a verdict — it means the rule has nothing to say, and
5302
+ * callers must fail open rather than treat it as invalid.
5303
+ *
5304
+ * The rule is pure, so one boundary the walker honors cannot be checked here:
5305
+ * a nested `package.json` marks an independent unit whose tests belong to a
5306
+ * different discovery pass, and detecting one needs a filesystem probe. A
5307
+ * caller with filesystem access should apply that check itself and fail open
5308
+ * when it fires — `@vitest-agent/cli`'s `agent check-test-path` does.
5309
+ * @public
5310
+ */
5311
+ declare function classifyTestPath(workspaces: ReadonlyArray<WorkspaceLike>, filePath: string): TestPathClassification | null;
5312
+ //#endregion
5155
5313
  //#region src/utils/validate-coverage-targets-shape.d.ts
5156
5314
  /** @public */
5157
5315
  interface CoverageTargetsShapeError {
@@ -5205,5 +5363,5 @@ declare function validateCoverageTargetsShape(input: unknown): CoverageTargetsSh
5205
5363
  */
5206
5364
  declare const CURRENT_SDK_VERSION: string;
5207
5365
  //#endregion
5208
- export { AcceptanceMetrics, ActionSeverity, ActorType, Agent, AgentConsoleMode, AgentContext, AgentId, AgentNotFoundError, AgentPluginOptions, AgentReport, AgentReporterOptions, AgentTreeNode, AnsiOptions, ArtifactKind, AssembledHistoryRecord, AssociateRunSessionInput, BehaviorAbandonedEvent, BehaviorAddedEvent, BehaviorCompletedEvent, BehaviorDetail, BehaviorNotFoundError, BehaviorRow, BehaviorScopedEventType, BehaviorScopedEventTypes, BehaviorStartedEvent, BehaviorStatus, BehaviorsReadyEvent, BlockedEvent, CURRENT_SDK_VERSION, CacheManifest, CacheManifestEntry, CandidateContext, CellOptions, ChangeKind, ChannelEvent, ChatId, CiConsoleMode, CitedArtifact, CitedArtifactRow, CommitChangesEntry, ConfigLive, ConsoleFormatOptions, ConsoleLeakEntry, ConsoleLeakFile, ConsoleLeakTask, ConsoleLeaks, ConsoleLogLevel, ConsoleMode, ConsoleOutputMode, ConsoleOutputs, ConversationId, ConversationNotFoundError, CoverageBaselines, FileCoverageReport as CoverageFile, FileCoverageReport, CoverageGap, CoverageInput, CoverageLevel, CoverageLevelName, CoverageMetric, CoverageRenderState, CoverageReport, CoverageTargets, CoverageTargetsMetrics, CoverageTargetsShapeError, CoverageTargetsShapeInfo, CoverageTargetsShapeResult, CoverageTargetsShapeWarning, CoverageTotals, CreateBehaviorInput, CreateGoalInput, CurrentTddPhase, DATABASE_FILENAME, DataReader, DataReaderLive, DataStore, DataStoreError, DataStoreLive, DenialReason, DetailLevel, DetailResolver, DetailResolverLive, DiscoveryError, DiscoveryRegistry, DiscoveryRegistryLive, DispatchInputs, EndTddTaskInput, Environment, EnvironmentDetector, EnvironmentDetectorLive, EnvironmentDetectorTest, Executor, ExecutorResolver, ExecutorResolverLive, FailureRecord, FailureSignatureDetail, FailureSignatureInput, FailureSignatureWriteInput, FileCoverageInput, FileEditPayload, FileSystemAdapter, FlakyTest, FormatSelector, FormatSelectorLive, type FormatTriageOptions, type FormatWrapupOptions, Formatter, FormatterContext, FunctionBoundary, GfmFormatter, GitCommandError, GoalAbandonedEvent, GoalAddedEvent, GoalCompletedEvent, GoalDetail, GoalNotFoundError, GoalRow, GoalStartedEvent, GoalStatus, GoalsReadyEvent, HistoryRecord, HistoryRow, HistoryTracker, HistoryTrackerLive, HistoryTrackerTest, HistoryTrackerTestState, HookFirePayload, HookStatus, HookType, HostKind, HostMetadataParseError, HostMetadataResult, HumanConsoleMode, HypothesisDetail, HypothesisInput, HypothesisPayload, IDEMPOTENCY_ROOT_SENTINEL, IdempotencyHit, IdempotencyInput, IdempotentResponseInput, IllegalStatusTransitionEntity, IllegalStatusTransitionError, JsonFormatter, KnownProject, KnownProjectInput, LoggerLive, ManifestRow, MapSessionInput, MapSessionOutput, MarkdownFormatter, MetricThresholds, ModuleInput, ModuleListEntry, ModuleRecord, ModuleReport, NoteInput, NotePayload, NoteRow, Osc8Options, OutputFormat, OutputPipelineLive, OutputRenderer, OutputRendererLive, PackageManager, PathResolutionError, PathResolutionLive, PatternThresholds, PerClientSessionMapReader, PerClientSessionMapReaderLive, PerClientSessionMapWriter, PerClientSessionMapWriterLive, PersistentFailure, Phase, PhaseTransitionContext, PhaseTransitionEvent, PhaseTransitionResult, ProbeMissError, ProjectDiscovery, ProjectDiscoveryLive, ProjectDiscoveryTest, ProjectIdentity, ProjectIdentityCandidate, ProjectIdentityCandidates, ProjectIdentityLive, ProjectIdentityNotResolvableError, ProjectIdentitySource, ProjectIdentityTest, ProjectKey, ProjectRunSummary, ProjectSummary, RegisterAgentInput, RegisterAgentResult, RegistrationConflictError, Remediation, RenderState, RenderTotals, RenderedOutput, ReportError, ReportSummary, ReporterKit, ReporterRenderInput, ResolveDataPathOptions, ResolveProjectIdentityOptions, ResolvedIdentity, ResolvedReporterConfig, ResolvedThresholds, RunChangedFile, RunContext, RunContext$, RunContextLive, RunContextService, RunContextTest, RunEvent, RunEventByTag, RunHealth, RunInvocationMethod, RunOutcome, RunShape, SessionCompleteEvent, SessionContextRow, SessionDetail, SessionInput, SessionNotFoundError, SessionOutcome, SettingsInput, SettingsListEntry, SettingsRow, SidecarTimeoutError, SilentFormatter, StackFrameInput, SuggestedActionRecord, SuiteInput, SuiteListEntry, TagCountEntry, TagInventoryRow, TddArtifactDetail, TddArtifactRow, TddPhaseDetail, TddTaskAlreadyEndedError, TddTaskDetail, TddTaskEndOutcome, TddTaskId, TddTaskInput, TddTaskNotFoundError, TddTaskSummary, TerminalFormatter, TestCaseInput, TestClassification, TestError, TestErrorInput, TestFileEntry, TestHistory, TestListEntry, TestOutcome, TestRecord, TestReport, TestRun, TestRunInput, TestRunReason, TestState, ToolCallPayload, ToolResultPayload, Transport, TrendEntry, TrendRecord, TrendSummary, TurnInput, TurnPayload, TurnSearchOptions, TurnSummary, UpdateBehaviorInput, UpdateGoalInput, UserPromptPayload, ValidateHypothesisInput, VitestAgentConfig, VitestAgentConfigFile, VitestAgentConfigFileService, VitestAgentReporter, VitestAgentReporterFactory, VitestModuleDiagnostic, VitestModuleError, VitestParsedStack, VitestTestCase, VitestTestDiagnostic, VitestTestError, VitestTestModule, VitestTestResult, VitestTestSuite, type WrapupKind, WriteCommitInput, WriteRunChangedFilesInput, WriteTddArtifactInput, WriteTddPhaseInput, WriteTddPhaseOutput, _resetMigrationCacheForTesting, ansi, assembleHistoryRecord, assembleManifest, buildAgentReport, buildConsoleLeaks, buildEnvPrefix, canonicalizeGitUrl, ciAnnotationsFormatter, classifyTest, collectConsoleLeakEntries, compressLines, computeFailureSignature, computeTrend, countSuiteFailures, deriveIdempotencyKey, detectPackageManager, detectVitestScripts, determineTier, ensureMigrated, extractSqlReason, findFunctionBoundary, formatConsoleMarkdown, formatFatalError, formatGfm, formatTriageEffect, formatWrapupEffect, getRecentDirection, getRunCommand, getWorstMetric, gitUrlToProjectKey, hashTargets, historyKey, initialRenderState, isTimeoutError, isVitestInvocation, migration as migration0001, normalizeAssertionShape, normalizeWorkspaceKey, osc8, probeHostMetadataFromEnv, migration$1 as registryMigration0001, relativePath, requiredArtifactForTransition, resolveCoverageInput, resolveDataPath, resolveLogFile, resolveLogLevel, resolveProjectIdentityFromCandidates, resolveProjectKeyFromCwd, resolveWorkspaceKey, rewriteBashCommand, safeFilename, migration$2 as sessionMapMigration0001, stripAnsi, transitionEnforcesBehaviorMatch, validateCoverageConfig, validateCoverageTargetsShape, validatePhaseTransition };
5366
+ export { AcceptanceMetrics, ActionSeverity, ActorType, Agent, AgentConsoleMode, AgentContext, AgentId, AgentNotFoundError, AgentPluginOptions, AgentReport, AgentReporterOptions, AgentTreeNode, AnsiOptions, ArtifactKind, AssembledHistoryRecord, AssociateRunSessionInput, BehaviorAbandonedEvent, BehaviorAddedEvent, BehaviorCompletedEvent, BehaviorDetail, BehaviorNotFoundError, BehaviorRow, BehaviorScopedEventType, BehaviorScopedEventTypes, BehaviorStartedEvent, BehaviorStatus, BehaviorsReadyEvent, BlockedEvent, CURRENT_SDK_VERSION, CacheManifest, CacheManifestEntry, CandidateContext, CellOptions, ChangeKind, ChannelEvent, ChatId, CiConsoleMode, CitedArtifact, CitedArtifactRow, CommitChangesEntry, ConfigLive, ConsoleFormatOptions, ConsoleLeakEntry, ConsoleLeakFile, ConsoleLeakTask, ConsoleLeaks, ConsoleLogLevel, ConsoleMode, ConsoleOutputMode, ConsoleOutputs, ConversationId, ConversationNotFoundError, CoverageBaselines, FileCoverageReport as CoverageFile, FileCoverageReport, CoverageGap, CoverageInput, CoverageLevel, CoverageLevelName, CoverageMetric, CoverageRenderState, CoverageReport, CoverageTargets, CoverageTargetsMetrics, CoverageTargetsShapeError, CoverageTargetsShapeInfo, CoverageTargetsShapeResult, CoverageTargetsShapeWarning, CoverageTotals, CreateBehaviorInput, CreateGoalInput, CurrentTddPhase, DATABASE_FILENAME, DataReader, DataReaderLive, DataStore, DataStoreError, DataStoreLive, DenialReason, DetailLevel, DetailResolver, DetailResolverLive, DiscoveryError, DiscoveryRegistry, DiscoveryRegistryLive, DispatchInputs, EndTddTaskInput, Environment, EnvironmentDetector, EnvironmentDetectorLive, EnvironmentDetectorTest, Executor, ExecutorResolver, ExecutorResolverLive, FailureRecord, FailureSignatureDetail, FailureSignatureInput, FailureSignatureWriteInput, FileCoverageInput, FileEditPayload, FileSystemAdapter, FlakyTest, FormatSelector, FormatSelectorLive, type FormatTriageOptions, type FormatWrapupOptions, Formatter, FormatterContext, FunctionBoundary, GfmFormatter, GitCommandError, GoalAbandonedEvent, GoalAddedEvent, GoalCompletedEvent, GoalDetail, GoalNotFoundError, GoalRow, GoalStartedEvent, GoalStatus, GoalsReadyEvent, HistoryRecord, HistoryRow, HistoryTracker, HistoryTrackerLive, HistoryTrackerTest, HistoryTrackerTestState, HookFirePayload, HookStatus, HookType, HostKind, HostMetadataParseError, HostMetadataResult, HumanConsoleMode, HypothesisDetail, HypothesisInput, HypothesisPayload, IDEMPOTENCY_ROOT_SENTINEL, IdempotencyHit, IdempotencyInput, IdempotentResponseInput, IllegalStatusTransitionEntity, IllegalStatusTransitionError, JsonFormatter, KnownProject, KnownProjectInput, LoggerLive, ManifestRow, MapSessionInput, MapSessionOutput, MarkdownFormatter, MetricThresholds, ModuleInput, ModuleListEntry, ModuleRecord, ModuleReport, NON_DISCOVERABLE_DIRS, NoteInput, NotePayload, NoteRow, Osc8Options, OutputFormat, OutputPipelineLive, OutputRenderer, OutputRendererLive, PackageManager, PathResolutionError, PathResolutionLive, PatternThresholds, PerClientSessionMapReader, PerClientSessionMapReaderLive, PerClientSessionMapWriter, PerClientSessionMapWriterLive, PersistentFailure, Phase, PhaseTransitionContext, PhaseTransitionEvent, PhaseTransitionResult, ProbeMissError, ProjectDiscovery, ProjectDiscoveryLive, ProjectDiscoveryTest, ProjectIdentity, ProjectIdentityCandidate, ProjectIdentityCandidates, ProjectIdentityLive, ProjectIdentityNotResolvableError, ProjectIdentitySource, ProjectIdentityTest, ProjectKey, ProjectRunSummary, ProjectSummary, RegisterAgentInput, RegisterAgentResult, RegistrationConflictError, Remediation, RenderState, RenderTotals, RenderedOutput, ReportError, ReportSummary, ReporterKit, ReporterRenderInput, ResolveDataPathOptions, ResolveProjectIdentityOptions, ResolvedIdentity, ResolvedReporterConfig, ResolvedThresholds, RunChangedFile, RunContext, RunContext$, RunContextLive, RunContextService, RunContextTest, RunEvent, RunEventByTag, RunHealth, RunInvocationMethod, RunOutcome, RunShape, SRC_DIR, SessionCompleteEvent, SessionContextRow, SessionDetail, SessionInput, SessionNotFoundError, SessionOutcome, SettingsInput, SettingsListEntry, SettingsRow, SidecarTimeoutError, SilentFormatter, StackFrameInput, SuggestedActionRecord, SuiteInput, SuiteListEntry, TEST_DIR, TEST_FILE_GLOB_SUFFIX, TEST_HELPER_DIRS, TagCountEntry, TagInventoryRow, TddArtifactDetail, TddArtifactRow, TddPhaseDetail, TddTaskAlreadyEndedError, TddTaskDetail, TddTaskEndOutcome, TddTaskId, TddTaskInput, TddTaskNotFoundError, TddTaskSummary, TerminalFormatter, TestCaseInput, TestClassification, TestError, TestErrorInput, TestFileEntry, TestHistory, TestListEntry, TestOutcome, TestPathClassification, TestPathVerdict, TestRecord, TestReport, TestRun, TestRunInput, TestRunReason, TestState, ToolCallPayload, ToolResultPayload, Transport, TrendEntry, TrendRecord, TrendSummary, TurnInput, TurnPayload, TurnSearchOptions, TurnSummary, UpdateBehaviorInput, UpdateGoalInput, UserPromptPayload, ValidateHypothesisInput, VitestAgentConfig, VitestAgentConfigFile, VitestAgentConfigFileService, VitestAgentReporter, VitestAgentReporterFactory, VitestModuleDiagnostic, VitestModuleError, VitestParsedStack, VitestTestCase, VitestTestDiagnostic, VitestTestError, VitestTestModule, VitestTestResult, VitestTestSuite, WorkspaceLike, type WrapupKind, WriteCommitInput, WriteRunChangedFilesInput, WriteTddArtifactInput, WriteTddPhaseInput, WriteTddPhaseOutput, _resetMigrationCacheForTesting, ansi, assembleHistoryRecord, assembleManifest, buildAgentReport, buildConsoleLeaks, buildEnvPrefix, canonicalizeGitUrl, ciAnnotationsFormatter, classifyTest, classifyTestPath, coerceErrorField, coerceErrorText, collectConsoleLeakEntries, compressLines, computeFailureSignature, computeTrend, countSuiteFailures, deriveIdempotencyKey, detectPackageManager, detectVitestScripts, determineTier, ensureMigrated, extractSqlReason, findFunctionBoundary, findOwningWorkspace, formatConsoleMarkdown, formatFatalError, formatGfm, formatTriageEffect, formatWrapupEffect, getRecentDirection, getRunCommand, getWorstMetric, gitUrlToProjectKey, hashTargets, historyKey, initialRenderState, isTestFileName, isTimeoutError, isVitestInvocation, migration as migration0001, normalizeAssertionShape, normalizeWorkspaceKey, osc8, probeHostMetadataFromEnv, migration$1 as registryMigration0001, relativePath, requiredArtifactForTransition, resolveCoverageInput, resolveDataPath, resolveLogFile, resolveLogLevel, resolveProjectIdentityFromCandidates, resolveProjectKeyFromCwd, resolveWorkspaceKey, rewriteBashCommand, safeFilename, migration$2 as sessionMapMigration0001, stripAnsi, transitionEnforcesBehaviorMatch, validateCoverageConfig, validateCoverageTargetsShape, validatePhaseTransition };
5209
5367
  //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -25,6 +25,7 @@ import { DataReaderLive } from "./layers/DataReaderLive.js";
25
25
  import { ActorType, AgentId, ChatId, ConversationId, HostKind, ProjectKey, TddTaskId } from "./schemas/Identity.js";
26
26
  import { Agent, IdempotencyHit } from "./schemas/Agent.js";
27
27
  import { DataStore } from "./services/DataStore.js";
28
+ import { coerceErrorField, coerceErrorText } from "./utils/coerce-error-text.js";
28
29
  import { DataStoreLive } from "./layers/DataStoreLive.js";
29
30
  import { DetailResolver } from "./services/DetailResolver.js";
30
31
  import { DetailResolverLive } from "./layers/DetailResolverLive.js";
@@ -48,6 +49,7 @@ import { PathResolutionLive } from "./layers/PathResolutionLive.js";
48
49
  import { PerClientSessionMapReader, PerClientSessionMapWriter } from "./services/PerClientSessionMap.js";
49
50
  import { PerClientSessionMapReaderLive, PerClientSessionMapWriterLive } from "./layers/PerClientSessionMapLive.js";
50
51
  import { ProjectDiscovery } from "./services/ProjectDiscovery.js";
52
+ import { NON_DISCOVERABLE_DIRS, SRC_DIR, TEST_DIR, TEST_FILE_GLOB_SUFFIX, TEST_HELPER_DIRS, classifyTestPath, findOwningWorkspace, isTestFileName } from "./utils/test-location.js";
51
53
  import { ProjectDiscoveryLive } from "./layers/ProjectDiscoveryLive.js";
52
54
  import { ProjectDiscoveryTest } from "./layers/ProjectDiscoveryTest.js";
53
55
  import { canonicalizeGitUrl, gitUrlToProjectKey } from "./utils/canonicalize-git-url.js";
@@ -115,7 +117,7 @@ import { requiredArtifactForTransition, transitionEnforcesBehaviorMatch, validat
115
117
  * substituted yet. Exported for version introspection by downstream tooling.
116
118
  * @public
117
119
  */
118
- const CURRENT_SDK_VERSION = "2.0.16";
120
+ const CURRENT_SDK_VERSION = "2.2.0";
119
121
 
120
122
  //#endregion
121
- export { ActionSeverity, ActorType, Agent, AgentConsoleMode, AgentContext, AgentId, AgentNotFoundError, AgentPluginOptions, AgentReport, AgentReporterOptions, BehaviorAbandonedEvent, BehaviorAddedEvent, BehaviorCompletedEvent, BehaviorDetail, BehaviorNotFoundError, BehaviorRow, BehaviorScopedEventTypes, BehaviorStartedEvent, BehaviorStatus, BehaviorsReadyEvent, BlockedEvent, CURRENT_SDK_VERSION, CacheManifest, CacheManifestEntry, ChannelEvent, ChatId, CiConsoleMode, ConfigLive, ConsoleLeakFile, ConsoleLeaks, ConsoleLogLevel, ConsoleMode, ConsoleOutputMode, ConsoleOutputs, ConversationId, ConversationNotFoundError, CoverageBaselines, FileCoverageReport as CoverageFile, CoverageGap, CoverageLevel, CoverageMetric, CoverageRenderState, CoverageReport, CoverageTargets, CoverageTargetsMetrics, CoverageTotals, DATABASE_FILENAME, DataReader, DataReaderLive, DataStore, DataStoreError, DataStoreLive, DetailLevel, DetailResolver, DetailResolverLive, DiscoveryError, DiscoveryRegistry, DiscoveryRegistryLive, Environment, EnvironmentDetector, EnvironmentDetectorLive, EnvironmentDetectorTest, Executor, ExecutorResolver, ExecutorResolverLive, FailureRecord, FileCoverageReport, FileEditPayload, FormatSelector, FormatSelectorLive, GfmFormatter, GitCommandError, GoalAbandonedEvent, GoalAddedEvent, GoalCompletedEvent, GoalDetail, GoalNotFoundError, GoalRow, GoalStartedEvent, GoalStatus, GoalsReadyEvent, HistoryRecord, HistoryTracker, HistoryTrackerLive, HistoryTrackerTest, HookFirePayload, HookStatus, HookType, HostKind, HostMetadataParseError, HumanConsoleMode, HypothesisPayload, IDEMPOTENCY_ROOT_SENTINEL, IdempotencyHit, IllegalStatusTransitionError, JsonFormatter, LoggerLive, MarkdownFormatter, MetricThresholds, ModuleRecord, ModuleReport, NotePayload, OutputFormat, OutputPipelineLive, OutputRenderer, OutputRendererLive, PackageManager, PathResolutionError, PathResolutionLive, PatternThresholds, PerClientSessionMapReader, PerClientSessionMapReaderLive, PerClientSessionMapWriter, PerClientSessionMapWriterLive, PhaseTransitionEvent, ProbeMissError, ProjectDiscovery, ProjectDiscoveryLive, ProjectDiscoveryTest, ProjectIdentity, ProjectIdentityLive, ProjectIdentityNotResolvableError, ProjectIdentityTest, ProjectKey, RegistrationConflictError, RenderState, RenderTotals, ReportError, ReportSummary, ResolvedThresholds, RunContext, RunContext$, RunContextLive, RunContextService, RunContextTest, RunEvent, SessionCompleteEvent, SessionNotFoundError, SessionOutcome, SidecarTimeoutError, SilentFormatter, SuggestedActionRecord, TagCountEntry, TddTaskAlreadyEndedError, TddTaskId, TddTaskNotFoundError, TerminalFormatter, TestClassification, TestHistory, TestRecord, TestReport, TestRun, TestRunReason, TestState, ToolCallPayload, ToolResultPayload, Transport, TrendEntry, TrendRecord, TurnPayload, UserPromptPayload, VitestAgentConfig, VitestAgentConfigFile, _resetMigrationCacheForTesting, ansi, assembleHistoryRecord, assembleManifest, buildAgentReport, buildConsoleLeaks, buildEnvPrefix, canonicalizeGitUrl, ciAnnotationsFormatter, classifyTest, collectConsoleLeakEntries, compressLines, computeFailureSignature, computeTrend, countSuiteFailures, deriveIdempotencyKey, detectPackageManager, detectVitestScripts, determineTier, ensureMigrated, extractSqlReason, findFunctionBoundary, formatConsoleMarkdown, formatFatalError, formatGfm, formatTriageEffect, formatWrapupEffect, getRecentDirection, getRunCommand, getWorstMetric, gitUrlToProjectKey, hashTargets, historyKey, initialRenderState, isTimeoutError, isVitestInvocation, migration as migration0001, normalizeAssertionShape, normalizeWorkspaceKey, osc8, probeHostMetadataFromEnv, migration$1 as registryMigration0001, relativePath, requiredArtifactForTransition, resolveCoverageInput, resolveDataPath, resolveLogFile, resolveLogLevel, resolveProjectIdentityFromCandidates, resolveProjectKeyFromCwd, resolveWorkspaceKey, rewriteBashCommand, safeFilename, migration$2 as sessionMapMigration0001, stripAnsi, transitionEnforcesBehaviorMatch, validateCoverageConfig, validateCoverageTargetsShape, validatePhaseTransition };
123
+ export { ActionSeverity, ActorType, Agent, AgentConsoleMode, AgentContext, AgentId, AgentNotFoundError, AgentPluginOptions, AgentReport, AgentReporterOptions, BehaviorAbandonedEvent, BehaviorAddedEvent, BehaviorCompletedEvent, BehaviorDetail, BehaviorNotFoundError, BehaviorRow, BehaviorScopedEventTypes, BehaviorStartedEvent, BehaviorStatus, BehaviorsReadyEvent, BlockedEvent, CURRENT_SDK_VERSION, CacheManifest, CacheManifestEntry, ChannelEvent, ChatId, CiConsoleMode, ConfigLive, ConsoleLeakFile, ConsoleLeaks, ConsoleLogLevel, ConsoleMode, ConsoleOutputMode, ConsoleOutputs, ConversationId, ConversationNotFoundError, CoverageBaselines, FileCoverageReport as CoverageFile, CoverageGap, CoverageLevel, CoverageMetric, CoverageRenderState, CoverageReport, CoverageTargets, CoverageTargetsMetrics, CoverageTotals, DATABASE_FILENAME, DataReader, DataReaderLive, DataStore, DataStoreError, DataStoreLive, DetailLevel, DetailResolver, DetailResolverLive, DiscoveryError, DiscoveryRegistry, DiscoveryRegistryLive, Environment, EnvironmentDetector, EnvironmentDetectorLive, EnvironmentDetectorTest, Executor, ExecutorResolver, ExecutorResolverLive, FailureRecord, FileCoverageReport, FileEditPayload, FormatSelector, FormatSelectorLive, GfmFormatter, GitCommandError, GoalAbandonedEvent, GoalAddedEvent, GoalCompletedEvent, GoalDetail, GoalNotFoundError, GoalRow, GoalStartedEvent, GoalStatus, GoalsReadyEvent, HistoryRecord, HistoryTracker, HistoryTrackerLive, HistoryTrackerTest, HookFirePayload, HookStatus, HookType, HostKind, HostMetadataParseError, HumanConsoleMode, HypothesisPayload, IDEMPOTENCY_ROOT_SENTINEL, IdempotencyHit, IllegalStatusTransitionError, JsonFormatter, LoggerLive, MarkdownFormatter, MetricThresholds, ModuleRecord, ModuleReport, NON_DISCOVERABLE_DIRS, NotePayload, OutputFormat, OutputPipelineLive, OutputRenderer, OutputRendererLive, PackageManager, PathResolutionError, PathResolutionLive, PatternThresholds, PerClientSessionMapReader, PerClientSessionMapReaderLive, PerClientSessionMapWriter, PerClientSessionMapWriterLive, PhaseTransitionEvent, ProbeMissError, ProjectDiscovery, ProjectDiscoveryLive, ProjectDiscoveryTest, ProjectIdentity, ProjectIdentityLive, ProjectIdentityNotResolvableError, ProjectIdentityTest, ProjectKey, RegistrationConflictError, RenderState, RenderTotals, ReportError, ReportSummary, ResolvedThresholds, RunContext, RunContext$, RunContextLive, RunContextService, RunContextTest, RunEvent, SRC_DIR, SessionCompleteEvent, SessionNotFoundError, SessionOutcome, SidecarTimeoutError, SilentFormatter, SuggestedActionRecord, TEST_DIR, TEST_FILE_GLOB_SUFFIX, TEST_HELPER_DIRS, TagCountEntry, TddTaskAlreadyEndedError, TddTaskId, TddTaskNotFoundError, TerminalFormatter, TestClassification, TestHistory, TestRecord, TestReport, TestRun, TestRunReason, TestState, ToolCallPayload, ToolResultPayload, Transport, TrendEntry, TrendRecord, TurnPayload, UserPromptPayload, VitestAgentConfig, VitestAgentConfigFile, _resetMigrationCacheForTesting, ansi, assembleHistoryRecord, assembleManifest, buildAgentReport, buildConsoleLeaks, buildEnvPrefix, canonicalizeGitUrl, ciAnnotationsFormatter, classifyTest, classifyTestPath, coerceErrorField, coerceErrorText, collectConsoleLeakEntries, compressLines, computeFailureSignature, computeTrend, countSuiteFailures, deriveIdempotencyKey, detectPackageManager, detectVitestScripts, determineTier, ensureMigrated, extractSqlReason, findFunctionBoundary, findOwningWorkspace, formatConsoleMarkdown, formatFatalError, formatGfm, formatTriageEffect, formatWrapupEffect, getRecentDirection, getRunCommand, getWorstMetric, gitUrlToProjectKey, hashTargets, historyKey, initialRenderState, isTestFileName, isTimeoutError, isVitestInvocation, migration as migration0001, normalizeAssertionShape, normalizeWorkspaceKey, osc8, probeHostMetadataFromEnv, migration$1 as registryMigration0001, relativePath, requiredArtifactForTransition, resolveCoverageInput, resolveDataPath, resolveLogFile, resolveLogLevel, resolveProjectIdentityFromCandidates, resolveProjectKeyFromCwd, resolveWorkspaceKey, rewriteBashCommand, safeFilename, migration$2 as sessionMapMigration0001, stripAnsi, transitionEnforcesBehaviorMatch, validateCoverageConfig, validateCoverageTargetsShape, validatePhaseTransition };
@@ -3,6 +3,7 @@ import { DataStoreError, extractSqlReason } from "../errors/DataStoreError.js";
3
3
  import { BehaviorNotFoundError, GoalNotFoundError, IllegalStatusTransitionError, TddTaskAlreadyEndedError, TddTaskNotFoundError } from "../errors/TddErrors.js";
4
4
  import { Agent, IdempotencyHit } from "../schemas/Agent.js";
5
5
  import { DataStore } from "../services/DataStore.js";
6
+ import { coerceErrorText } from "../utils/coerce-error-text.js";
6
7
  import { Effect, Layer, Option } from "effect";
7
8
  import { SqlClient } from "effect/unstable/sql/SqlClient";
8
9
  import { randomUUID } from "node:crypto";
@@ -134,16 +135,18 @@ const DataStoreLive = Layer.effect(DataStore, Effect.gen(function* () {
134
135
  count: errors.length
135
136
  }));
136
137
  for (const err of errors) {
137
- yield* sql`INSERT INTO test_errors (run_id, test_case_id, test_suite_id, module_id, scope, name, message, diff, actual, expected, stack, cause_error_id, signature_hash, ordinal) VALUES (${runId}, ${err.testCaseId ?? null}, ${err.testSuiteId ?? null}, ${err.moduleId ?? null}, ${err.scope}, ${err.name ?? null}, ${err.message}, ${err.diff ?? null}, ${err.actual ?? null}, ${err.expected ?? null}, ${err.stack ?? null}, ${err.causeErrorId ?? null}, ${err.signatureHash ?? null}, ${err.ordinal ?? 0})`;
138
+ const message = coerceErrorText(err.message) ?? "<missing message>";
139
+ yield* sql`INSERT INTO test_errors (run_id, test_case_id, test_suite_id, module_id, scope, name, message, diff, actual, expected, stack, cause_error_id, signature_hash, ordinal) VALUES (${runId}, ${err.testCaseId ?? null}, ${err.testSuiteId ?? null}, ${err.moduleId ?? null}, ${err.scope}, ${coerceErrorText(err.name) ?? null}, ${message}, ${coerceErrorText(err.diff) ?? null}, ${coerceErrorText(err.actual) ?? null}, ${coerceErrorText(err.expected) ?? null}, ${coerceErrorText(err.stack) ?? null}, ${err.causeErrorId ?? null}, ${err.signatureHash ?? null}, ${err.ordinal ?? 0})`;
140
+ const stackText = coerceErrorText(err.stack);
138
141
  if (err.frames && err.frames.length > 0) {
139
142
  const errorId = (yield* sql`SELECT last_insert_rowid() as id`)[0].id;
140
143
  for (const frame of err.frames) {
141
144
  const fileId = yield* ensureFile(frame.filePath);
142
145
  yield* sql`INSERT INTO stack_frames (error_id, ordinal, method, file_id, line, col, source_mapped_line, function_boundary_line) VALUES (${errorId}, ${frame.ordinal}, ${frame.method}, ${fileId}, ${frame.line}, ${frame.col}, ${frame.sourceMappedLine ?? null}, ${frame.functionBoundaryLine ?? null})`;
143
146
  }
144
- } else if (err.stack) {
147
+ } else if (stackText) {
145
148
  const errorId = (yield* sql`SELECT last_insert_rowid() as id`)[0].id;
146
- const frames = [...err.stack.matchAll(/at\s+(?:(.+?)\s+)?\(?(.+?):(\d+):(\d+)\)?/g)];
149
+ const frames = [...stackText.matchAll(/at\s+(?:(.+?)\s+)?\(?(.+?):(\d+):(\d+)\)?/g)];
147
150
  for (let frameOrdinal = 0; frameOrdinal < frames.length; frameOrdinal++) {
148
151
  const m = frames[frameOrdinal];
149
152
  const method = m[1] ?? null;
@@ -1,13 +1,12 @@
1
1
  import { DiscoveryError } from "../errors/DiscoveryError.js";
2
2
  import { ProjectDiscovery } from "../services/ProjectDiscovery.js";
3
+ import { NON_DISCOVERABLE_DIRS } from "../utils/test-location.js";
3
4
  import { Effect, FileSystem, Layer } from "effect";
4
5
 
5
6
  //#region src/layers/ProjectDiscoveryLive.ts
6
7
  const TEST_FILE_PATTERN = /\.(test|spec)\.(ts|tsx)$/;
7
8
  const SKIP_DIRS = /* @__PURE__ */ new Set([
8
- "node_modules",
9
- ".git",
10
- "dist",
9
+ ...NON_DISCOVERABLE_DIRS,
11
10
  "coverage",
12
11
  ".turbo",
13
12
  ".vite"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitest-agent/sdk",
3
- "version": "2.0.16",
3
+ "version": "2.2.0",
4
4
  "private": false,
5
5
  "description": "Shared Effect Schema, SQLite migrations, data layer, services, formatters, and utilities for vitest-agent packages.",
6
6
  "keywords": [
@@ -13,7 +13,15 @@ const ReportSummary = Schema.Struct({
13
13
  passed: Schema.Number,
14
14
  failed: Schema.Number,
15
15
  skipped: Schema.Number,
16
- duration: Schema.Number
16
+ duration: Schema.Number,
17
+ /**
18
+ * Count of every collected module, passing modules included.
19
+ * `AgentReport.failed` only carries failing modules, so a green run
20
+ * has no other way to know how many files actually ran — without
21
+ * this a fully-passing replay reads as "0 modules all-passed"
22
+ * (issue #204). Optional to keep existing fixtures/reports valid.
23
+ */
24
+ modules: Schema.optional(Schema.Number)
17
25
  }).annotate({ identifier: "ReportSummary" });
18
26
  /**
19
27
  * An individual test case result.
@@ -135,7 +135,17 @@ const RenderState = Schema.Struct({
135
135
  runCount: Schema.Number
136
136
  })),
137
137
  failures: Schema.Array(FailureRecord),
138
- suggestedActions: Schema.Array(SuggestedActionRecord)
138
+ suggestedActions: Schema.Array(SuggestedActionRecord),
139
+ /**
140
+ * Count of every collected module, passing modules included. Folded
141
+ * from `RunFinished.collectedModules`. Renderers prefer this over
142
+ * `moduleOrder.length` for "N modules all-passed" copy — a report
143
+ * replay only queues failing modules, so `moduleOrder` alone
144
+ * undercounts a green run to zero (issue #204). Optional — a run
145
+ * that never carried the field (older replay data, hand-built
146
+ * fixtures) falls back to `moduleOrder.length`.
147
+ */
148
+ collectedModules: Schema.optional(Schema.Number)
139
149
  }).annotate({ identifier: "RenderState" });
140
150
  /**
141
151
  * Initial state — what the reducer returns when no events have been
@@ -194,7 +194,15 @@ const RunEvent = Schema.Union([
194
194
  failCount: Schema.Number,
195
195
  skipCount: Schema.Number,
196
196
  durationMs: Schema.Number,
197
- timeoutCount: Schema.optional(Schema.Number)
197
+ timeoutCount: Schema.optional(Schema.Number),
198
+ /**
199
+ * Count of every collected module, passing modules included.
200
+ * Carries `AgentReport.summary.modules` (or the live module
201
+ * count) into the event stream so the reducer can fold the
202
+ * true count into `RenderState` even when `moduleOrder` only
203
+ * tracks failing modules (report replay). See issue #204.
204
+ */
205
+ collectedModules: Schema.optional(Schema.Number)
198
206
  })
199
207
  ]).annotate({ identifier: "RunEvent" });
200
208
 
package/testing.d.ts CHANGED
@@ -36,6 +36,14 @@ declare const AgentReport: Schema.Struct<{
36
36
  readonly failed: Schema.Number;
37
37
  readonly skipped: Schema.Number;
38
38
  readonly duration: Schema.Number;
39
+ /**
40
+ * Count of every collected module, passing modules included.
41
+ * `AgentReport.failed` only carries failing modules, so a green run
42
+ * has no other way to know how many files actually ran — without
43
+ * this a fully-passing replay reads as "0 modules all-passed"
44
+ * (issue #204). Optional to keep existing fixtures/reports valid.
45
+ */
46
+ readonly modules: Schema.optional<Schema.Number>;
39
47
  }>;
40
48
  readonly failed: Schema.$Array<Schema.Struct<{
41
49
  readonly file: Schema.String;
@@ -1,3 +1,5 @@
1
+ import { coerceErrorField } from "./coerce-error-text.js";
2
+
1
3
  //#region src/utils/build-report.ts
2
4
  /**
3
5
  * Format a single Vitest stack frame: strings pass through; ParsedStack
@@ -18,10 +20,23 @@ function formatStackFrame(frame) {
18
20
  function mapErrors(errors) {
19
21
  if (!errors || errors.length === 0) return void 0;
20
22
  return errors.map((e) => {
23
+ const diffText = coerceErrorField(e, "diff");
24
+ let stacks;
25
+ try {
26
+ stacks = e.stacks;
27
+ } catch {
28
+ stacks = void 0;
29
+ }
30
+ let stackText;
31
+ try {
32
+ stackText = stacks && stacks.length > 0 ? stacks.map(formatStackFrame).join("\n") : void 0;
33
+ } catch {
34
+ stackText = void 0;
35
+ }
21
36
  return {
22
- message: e.message,
23
- ..."diff" in e && e.diff != null ? { diff: e.diff } : {},
24
- ...e.stacks && e.stacks.length > 0 ? { stack: e.stacks.map(formatStackFrame).join("\n") } : {}
37
+ message: coerceErrorField(e, "message") ?? "<missing message>",
38
+ ...diffText !== void 0 ? { diff: diffText } : {},
39
+ ...stackText !== void 0 ? { stack: stackText } : {}
25
40
  };
26
41
  });
27
42
  }
@@ -68,6 +83,13 @@ function buildAgentReport(testModules, unhandledErrors, reason, options, project
68
83
  totalDuration += moduleDuration;
69
84
  const moduleState = normalizeState(testModule.state());
70
85
  const moduleErrors = mapErrors(testModule.errors());
86
+ const suiteErrors = [];
87
+ let anySuiteFailed = false;
88
+ for (const suite of testModule.children.allSuites()) {
89
+ if (normalizeState(suite.state()) === "failed") anySuiteFailed = true;
90
+ const mapped = mapErrors(suite.errors?.());
91
+ if (mapped) suiteErrors.push(...mapped);
92
+ }
71
93
  const allTests = [...testModule.children.allTests()];
72
94
  const testReports = [];
73
95
  let moduleHasFailure = false;
@@ -96,29 +118,33 @@ function buildAgentReport(testModules, unhandledErrors, reason, options, project
96
118
  testReports.push(testReport);
97
119
  }
98
120
  }
99
- if (moduleHasFailure || !moduleHasFailure && moduleState === "failed" && moduleErrors !== void 0) {
121
+ const combinedErrors = [...moduleErrors ?? [], ...suiteErrors];
122
+ const moduleLevelFailure = moduleState === "failed" || anySuiteFailed || combinedErrors.length > 0;
123
+ if (moduleHasFailure || moduleLevelFailure) {
100
124
  failedFiles.push(testModule.relativeModuleId);
101
125
  const moduleReport = {
102
126
  file: testModule.relativeModuleId,
103
- state: moduleState,
127
+ state: moduleLevelFailure ? "failed" : moduleState,
104
128
  duration: moduleDuration,
105
129
  tests: testReports,
106
- ...moduleErrors ? { errors: moduleErrors } : {}
130
+ ...combinedErrors.length > 0 ? { errors: combinedErrors } : {}
107
131
  };
108
132
  failedModules.push(moduleReport);
109
133
  }
110
134
  }
111
135
  const mappedUnhandled = mapErrors(unhandledErrors) ?? [];
136
+ const effectiveReason = reason === "passed" && (failedFiles.length > 0 || mappedUnhandled.length > 0) ? "failed" : reason;
112
137
  return {
113
138
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
114
139
  ...projectName != null ? { project: projectName } : {},
115
- reason,
140
+ reason: effectiveReason,
116
141
  summary: {
117
142
  total: totalCount,
118
143
  passed: passedCount,
119
144
  failed: failedCount,
120
145
  skipped: skippedCount,
121
- duration: totalDuration
146
+ duration: totalDuration,
147
+ modules: testModules.length
122
148
  },
123
149
  failed: failedModules,
124
150
  unhandledErrors: mappedUnhandled,
@@ -0,0 +1,60 @@
1
+ //#region src/utils/coerce-error-text.ts
2
+ /**
3
+ * Coerce an unknown error-field value (message, name, diff, stack, ...) into a
4
+ * string that is safe to bind to a SQLite TEXT column or render to a terminal.
5
+ *
6
+ * Vitest failure values are typed as strings but are whatever the test threw:
7
+ * `Effect.flip` on an unexpectedly-succeeding effect puts arbitrary success
8
+ * values (routinely plain objects) into the error channel, and Effect's
9
+ * `ConfigError.message` is a getter that throws when its cause lacks
10
+ * `toString`. Every access here is exception-safe.
11
+ *
12
+ * - `undefined` / `null` → `undefined` (caller decides null vs sentinel)
13
+ * - string → unchanged
14
+ * - other primitives → `String(value)`
15
+ * - objects → `JSON.stringify`, falling back to `String(value)`, falling back
16
+ * to `"<unserializable>"` when both throw
17
+ *
18
+ * @public
19
+ */
20
+ const coerceErrorText = (value) => {
21
+ if (value === void 0 || value === null) return void 0;
22
+ if (typeof value === "string") return value;
23
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
24
+ try {
25
+ return JSON.stringify(value) ?? "<unserializable>";
26
+ } catch {
27
+ try {
28
+ return String(value);
29
+ } catch {
30
+ return "<unserializable>";
31
+ }
32
+ }
33
+ };
34
+ /**
35
+ * Read a named field off an unknown error-like value and coerce it with
36
+ * {@link coerceErrorText}, guarding the property access itself. A live getter
37
+ * that throws (Effect's `ConfigError.message` when its cause lacks
38
+ * `toString`) throws at the read site — `coerceErrorText(e.message)` never
39
+ * reaches the helper's own exception handling. Route every read of an
40
+ * untrusted error object's field through this helper instead.
41
+ *
42
+ * - `null` / non-object sources → `undefined`
43
+ * - a getter that throws → `"<unreadable field>"`
44
+ * - otherwise → `coerceErrorText(value)`
45
+ *
46
+ * @public
47
+ */
48
+ const coerceErrorField = (source, key) => {
49
+ if (source === null || typeof source !== "object" && typeof source !== "function") return void 0;
50
+ let value;
51
+ try {
52
+ value = source[key];
53
+ } catch {
54
+ return "<unreadable field>";
55
+ }
56
+ return coerceErrorText(value);
57
+ };
58
+
59
+ //#endregion
60
+ export { coerceErrorField, coerceErrorText };
@@ -15,6 +15,7 @@ const typeTagOf = (literal) => {
15
15
  };
16
16
  /** @public */
17
17
  const normalizeAssertionShape = (message) => {
18
+ if (typeof message !== "string") return "";
18
19
  const match = message.match(ASSERTION_REGEX);
19
20
  if (match === null) return null;
20
21
  const matcher = match[1];
@@ -21,11 +21,23 @@ function formatFatalError(err) {
21
21
  const cause = err[FIBER_FAILURE_CAUSE_KEY];
22
22
  if (Cause.isCause(cause)) detail = Cause.pretty(cause);
23
23
  else detail = String(err);
24
- } else if (err instanceof Error) detail = err.stack ?? err.message;
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
+ }
25
33
  else if (err !== null && typeof err === "object") try {
26
34
  detail = JSON.stringify(err, null, 2);
27
35
  } catch {
28
- detail = String(err);
36
+ try {
37
+ detail = String(err);
38
+ } catch {
39
+ detail = "<unserializable error>";
40
+ }
29
41
  }
30
42
  else detail = String(err);
31
43
  return `${detail}\nPlease report at ${ISSUE_URL}`;
@@ -0,0 +1,114 @@
1
+ import { basename, join, relative, sep } from "node:path";
2
+
3
+ //#region src/utils/test-location.ts
4
+ /** Directory holding source files whose tests may be co-located. @public */
5
+ const SRC_DIR = "src";
6
+ /** Canonical test directory name. @public */
7
+ const TEST_DIR = "__test__";
8
+ /** Support directories under `__test__/` that are never collected. @public */
9
+ const TEST_HELPER_DIRS = [
10
+ "fixtures",
11
+ "snapshots",
12
+ "utils"
13
+ ];
14
+ /** Glob suffix matching every discoverable test file. @public */
15
+ const TEST_FILE_GLOB_SUFFIX = "*.{test,spec}.{ts,tsx,js,jsx}";
16
+ /**
17
+ * Directory names discovery never walks into, and therefore never collects
18
+ * tests from. Keep in step with {@link TEST_FILE_GLOB_SUFFIX}'s extension set:
19
+ * this is the directory half of the same "what can Vitest reach" question.
20
+ *
21
+ * `@vitest-agent/plugin`'s test-file walker and its cache-signature walk both
22
+ * prune these before recursing (pruning first, rather than filtering results
23
+ * after, is what keeps a symlinked `node_modules` from dragging in the whole
24
+ * pnpm store), and {@link classifyTestPath} declines to render a verdict for
25
+ * any path that crosses one.
26
+ * @public
27
+ */
28
+ const NON_DISCOVERABLE_DIRS = /* @__PURE__ */ new Set([
29
+ "node_modules",
30
+ ".git",
31
+ "dist"
32
+ ]);
33
+ /**
34
+ * Regex form of {@link TEST_FILE_GLOB_SUFFIX}. Anchored at the end of the
35
+ * string, so it matches a bare basename and a full path alike. Change both
36
+ * together — they are the same extension set in two notations.
37
+ */
38
+ const TEST_FILE_SUFFIX_RE = /\.(?:test|spec)\.(?:ts|tsx|js|jsx)$/;
39
+ /**
40
+ * True when `filePathOrName` ends in a discoverable test-file suffix — the
41
+ * extension half of the layout rule, matching {@link TEST_FILE_GLOB_SUFFIX}.
42
+ *
43
+ * Says nothing about location: a `node_modules` copy of `foo.test.ts` passes
44
+ * this and is still not discoverable. Pair it with {@link classifyTestPath}
45
+ * when the answer has to account for where the file sits.
46
+ * @public
47
+ */
48
+ function isTestFileName(filePathOrName) {
49
+ return TEST_FILE_SUFFIX_RE.test(filePathOrName);
50
+ }
51
+ /** True when `child` is `parent` or sits beneath it on a segment boundary. */
52
+ function contains(parent, child) {
53
+ return child === parent || child.startsWith(parent.endsWith(sep) ? parent : `${parent}${sep}`);
54
+ }
55
+ /**
56
+ * Returns the deepest supplied workspace containing `filePath`, or `null` when
57
+ * none does. Attribution is by longest matching path, so a nested package wins
58
+ * over the repository root that contains it.
59
+ *
60
+ * Exported because callers that need to reason about a path relative to its
61
+ * owning package — walking down from the package root, for instance — must
62
+ * agree with {@link classifyTestPath} about which package owns it.
63
+ * @public
64
+ */
65
+ function findOwningWorkspace(workspaces, filePath) {
66
+ let owner = null;
67
+ for (const ws of workspaces) {
68
+ if (!contains(ws.path, filePath)) continue;
69
+ if (owner === null || ws.path.length > owner.path.length) owner = ws;
70
+ }
71
+ return owner;
72
+ }
73
+ /**
74
+ * Classifies `filePath` against the test-layout rule: a test file is
75
+ * discoverable only at `<workspace>/src/**` or `<workspace>/__test__/**`,
76
+ * excluding the helper directories under `__test__/`.
77
+ *
78
+ * Returns `null` when no supplied workspace contains the path, and when the
79
+ * path crosses one of the {@link NON_DISCOVERABLE_DIRS} discovery never walks
80
+ * into. Neither is a verdict — it means the rule has nothing to say, and
81
+ * callers must fail open rather than treat it as invalid.
82
+ *
83
+ * The rule is pure, so one boundary the walker honors cannot be checked here:
84
+ * a nested `package.json` marks an independent unit whose tests belong to a
85
+ * different discovery pass, and detecting one needs a filesystem probe. A
86
+ * caller with filesystem access should apply that check itself and fail open
87
+ * when it fires — `@vitest-agent/cli`'s `agent check-test-path` does.
88
+ * @public
89
+ */
90
+ function classifyTestPath(workspaces, filePath) {
91
+ const owner = findOwningWorkspace(workspaces, filePath);
92
+ if (owner === null) return null;
93
+ const segments = relative(owner.path, filePath).split(sep);
94
+ if (segments.some((s) => NON_DISCOVERABLE_DIRS.has(s))) return null;
95
+ const [head] = segments;
96
+ if (head === "src") return {
97
+ verdict: "valid",
98
+ workspace: owner.name,
99
+ suggestedPath: null
100
+ };
101
+ if (head === "__test__") return {
102
+ verdict: segments.slice(1, -1).some((s) => TEST_HELPER_DIRS.includes(s)) ? "excluded" : "valid",
103
+ workspace: owner.name,
104
+ suggestedPath: null
105
+ };
106
+ return {
107
+ verdict: "invalid",
108
+ workspace: owner.name,
109
+ suggestedPath: join(owner.path, TEST_DIR, basename(filePath))
110
+ };
111
+ }
112
+
113
+ //#endregion
114
+ export { NON_DISCOVERABLE_DIRS, SRC_DIR, TEST_DIR, TEST_FILE_GLOB_SUFFIX, TEST_HELPER_DIRS, classifyTestPath, findOwningWorkspace, isTestFileName };