@vitest-agent/sdk 2.0.16 → 2.1.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.
@@ -5205,5 +5281,5 @@ declare function validateCoverageTargetsShape(input: unknown): CoverageTargetsSh
5205
5281
  */
5206
5282
  declare const CURRENT_SDK_VERSION: string;
5207
5283
  //#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 };
5284
+ 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, coerceErrorField, coerceErrorText, 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 };
5209
5285
  //# 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";
@@ -115,7 +116,7 @@ import { requiredArtifactForTransition, transitionEnforcesBehaviorMatch, validat
115
116
  * substituted yet. Exported for version introspection by downstream tooling.
116
117
  * @public
117
118
  */
118
- const CURRENT_SDK_VERSION = "2.0.16";
119
+ const CURRENT_SDK_VERSION = "2.1.0";
119
120
 
120
121
  //#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 };
122
+ 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, coerceErrorField, coerceErrorText, 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 };
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitest-agent/sdk",
3
- "version": "2.0.16",
3
+ "version": "2.1.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}`;