@vitest-agent/sdk 2.4.13 → 2.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -10,6 +10,7 @@ import { formatGfm } from "./utils/format-gfm.js";
10
10
  import { GfmFormatter } from "./formatters/gfm.js";
11
11
  import { JsonFormatter } from "./formatters/json.js";
12
12
  import { ansi, stripAnsi } from "./utils/ansi.js";
13
+ import { formatScopedCoverageNote } from "./utils/format-scoped-coverage-note.js";
13
14
  import { determineTier, formatConsoleMarkdown, getWorstMetric, relativePath } from "./utils/format-console.js";
14
15
  import { MarkdownFormatter } from "./formatters/markdown.js";
15
16
  import { SilentFormatter } from "./formatters/silent.js";
@@ -65,7 +66,7 @@ import migration from "./migrations/0001_initial.js";
65
66
  import migration$1 from "./migrations/registry_0001_initial.js";
66
67
  import migration$2 from "./migrations/session_map_0001_initial.js";
67
68
  import { AgentConsoleMode, CiConsoleMode, ConsoleMode, ConsoleOutputMode, DetailLevel, Environment, Executor, HumanConsoleMode, OutputFormat, PackageManager, ReportError, TestClassification, TestRunReason, TestState } from "./schemas/Common.js";
68
- import { ConsoleLeakFile, ConsoleLeaks } from "./schemas/ConsoleLeaks.js";
69
+ import { ConsoleLeakFile, ConsoleLeaks, ConsoleLeaksFromFailingTests } from "./schemas/ConsoleLeaks.js";
69
70
  import { MetricThresholds, PatternThresholds, ResolvedThresholds } from "./schemas/Thresholds.js";
70
71
  import { CoverageReport, CoverageTotals, FileCoverageReport } from "./schemas/Coverage.js";
71
72
  import { AgentReport, ModuleReport, ReportSummary, TagCountEntry, TestReport } from "./schemas/AgentReport.js";
@@ -94,6 +95,7 @@ import { assembleHistoryRecord, assembleManifest } from "./sql/assemblers.js";
94
95
  import { buildAgentReport, countSuiteFailures } from "./utils/build-report.js";
95
96
  import { computeTrend, getRecentDirection, hashTargets } from "./utils/compute-trend.js";
96
97
  import { buildConsoleLeaks, collectConsoleLeakEntries } from "./utils/console-leaks.js";
98
+ import { detectNonDefaultDiscoverStrategy } from "./utils/detect-non-default-discover-strategy.js";
97
99
  import { detectPackageManager, getRunCommand } from "./utils/detect-pm.js";
98
100
  import { isTimeoutError } from "./utils/detect-timeout.js";
99
101
  import { _resetMigrationCacheForTesting, ensureMigrated } from "./utils/ensure-migrated.js";
@@ -117,7 +119,7 @@ import { requiredArtifactForTransition, transitionEnforcesBehaviorMatch, validat
117
119
  * substituted yet. Exported for version introspection by downstream tooling.
118
120
  * @public
119
121
  */
120
- const CURRENT_SDK_VERSION = "2.4.13";
122
+ const CURRENT_SDK_VERSION = "2.5.1";
121
123
 
122
124
  //#endregion
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 };
125
+ 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, ConsoleLeaksFromFailingTests, 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, detectNonDefaultDiscoverStrategy, detectPackageManager, detectVitestScripts, determineTier, ensureMigrated, extractSqlReason, findFunctionBoundary, findOwningWorkspace, formatConsoleMarkdown, formatFatalError, formatGfm, formatScopedCoverageNote, 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 };
@@ -118,7 +118,7 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
118
118
  lines: 0
119
119
  };
120
120
  const baselineRows = yield* sql`SELECT metric, value FROM coverage_baselines
121
- WHERE project = '__global__' AND pattern = ''`;
121
+ WHERE project = '__global__' AND kind = 'baseline' AND pattern = ''`;
122
122
  const thresholds = {};
123
123
  for (const b of baselineRows) if (b.metric === "lines") thresholds.lines = b.value;
124
124
  else if (b.metric === "functions") thresholds.functions = b.value;
@@ -253,11 +253,21 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
253
253
  table: "test_history",
254
254
  reason: extractSqlReason(e)
255
255
  })));
256
- const getBaselines = (project) => Effect.gen(function* () {
257
- yield* Effect.logDebug("getBaselines").pipe(Effect.annotateLogs({ project }));
256
+ /**
257
+ * Shared read for the `coverage_baselines` table's per-`kind` rows
258
+ * (`baseline` / `threshold` / `target`). Returns `Option.none()` when
259
+ * no rows of that kind were ever persisted, so a caller can tell
260
+ * "never configured" apart from "configured with zero metrics"
261
+ * (issue #237).
262
+ */
263
+ const getCoveragePolicy = (kind, project) => Effect.gen(function* () {
264
+ yield* Effect.logDebug("getCoveragePolicy").pipe(Effect.annotateLogs({
265
+ kind,
266
+ project
267
+ }));
258
268
  const rows = yield* sql`SELECT metric, value, pattern, updated_at
259
269
  FROM coverage_baselines
260
- WHERE project = ${project}
270
+ WHERE project = ${project} AND kind = ${kind}
261
271
  ORDER BY pattern, metric`;
262
272
  if (rows.length === 0) return Option.none();
263
273
  const global = {};
@@ -284,6 +294,7 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
284
294
  table: "coverage_baselines",
285
295
  reason: extractSqlReason(e)
286
296
  })));
297
+ const getBaselines = (project) => getCoveragePolicy("baseline", project);
287
298
  const getTrends = (project, limit) => Effect.gen(function* () {
288
299
  yield* Effect.logDebug("getTrends").pipe(Effect.annotateLogs({
289
300
  project,
@@ -454,7 +465,7 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
454
465
  ORDER BY timestamp DESC LIMIT 1`;
455
466
  if (runs.length === 0) return Option.none();
456
467
  const runId = runs[0].id;
457
- const fileCoverageRows = yield* sql`SELECT f.path as file_path, fc.statements, fc.branches, fc.functions, fc.lines, fc.uncovered_lines
468
+ const fileCoverageRows = yield* sql`SELECT f.path as file_path, fc.statements, fc.branches, fc.functions, fc.lines, fc.uncovered_lines, fc.tier
458
469
  FROM file_coverage fc JOIN files f ON f.id = fc.file_id
459
470
  WHERE fc.run_id = ${runId}`;
460
471
  const trendRows = yield* sql`SELECT statements, branches, functions, lines
@@ -478,13 +489,20 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
478
489
  lines: fileCoverageRows.reduce((sum, r) => sum + r.lines, 0) / count
479
490
  };
480
491
  }
481
- const baselinesOpt = yield* getBaselines("__global__");
492
+ const thresholdsOpt = yield* getCoveragePolicy("threshold", "__global__");
493
+ const targetsOpt = yield* getCoveragePolicy("target", "__global__");
494
+ const baselinesOpt = yield* getCoveragePolicy("baseline", "__global__");
495
+ const thresholds = Option.getOrElse(thresholdsOpt, () => ({
496
+ updatedAt: "",
497
+ global: {},
498
+ patterns: []
499
+ }));
482
500
  const baselines = Option.getOrElse(baselinesOpt, () => ({
483
501
  updatedAt: "",
484
502
  global: {},
485
503
  patterns: []
486
504
  }));
487
- const lowCoverage = fileCoverageRows.map((r) => ({
505
+ const toFileCoverageReport = (r) => ({
488
506
  file: r.file_path,
489
507
  summary: {
490
508
  statements: r.statements,
@@ -493,16 +511,30 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
493
511
  lines: r.lines
494
512
  },
495
513
  uncoveredLines: r.uncovered_lines ?? ""
496
- }));
514
+ });
515
+ const lowCoverage = fileCoverageRows.filter((r) => r.tier === "below_threshold").map(toFileCoverageReport);
516
+ const belowTarget = fileCoverageRows.filter((r) => r.tier === "below_target").map(toFileCoverageReport);
497
517
  const report = {
498
518
  totals,
499
519
  thresholds: {
520
+ global: thresholds.global,
521
+ patterns: thresholds.patterns
522
+ },
523
+ ...Option.isSome(targetsOpt) ? { targets: {
524
+ global: targetsOpt.value.global,
525
+ patterns: targetsOpt.value.patterns
526
+ } } : {},
527
+ baselines: {
500
528
  global: baselines.global,
501
529
  patterns: baselines.patterns
502
530
  },
503
531
  scoped: false,
504
532
  lowCoverage,
505
- lowCoverageFiles: lowCoverage.map((f) => f.file)
533
+ lowCoverageFiles: lowCoverage.map((f) => f.file),
534
+ ...belowTarget.length > 0 ? {
535
+ belowTarget,
536
+ belowTargetFiles: belowTarget.map((f) => f.file)
537
+ } : {}
506
538
  };
507
539
  return Option.some(report);
508
540
  }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
@@ -895,11 +927,12 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
895
927
  triageWasNonEmpty: r.triage_was_non_empty === 1,
896
928
  startedAt: r.started_at,
897
929
  endedAt: r.ended_at,
898
- endReason: r.end_reason
930
+ endReason: r.end_reason,
931
+ conversationId: r.conversation_id
899
932
  });
900
933
  const getSessionById = (id) => Effect.gen(function* () {
901
934
  yield* Effect.logDebug("getSessionById").pipe(Effect.annotateLogs({ id }));
902
- const rows = yield* sql`SELECT id, chat_id, project, cwd, agent_kind, agent_type, parent_session_id, triage_was_non_empty, started_at, ended_at, end_reason FROM sessions WHERE id = ${id} LIMIT 1`;
935
+ const rows = yield* sql`SELECT id, chat_id, project, cwd, agent_kind, agent_type, parent_session_id, triage_was_non_empty, started_at, ended_at, end_reason, conversation_id FROM sessions WHERE id = ${id} LIMIT 1`;
903
936
  if (rows.length === 0) return Option.none();
904
937
  return Option.some(sessionRowToDetail(rows[0]));
905
938
  }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
@@ -909,7 +942,7 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
909
942
  })));
910
943
  const getSessionByChatId = (chatId) => Effect.gen(function* () {
911
944
  yield* Effect.logDebug("getSessionByChatId").pipe(Effect.annotateLogs({ chatId }));
912
- const rows = yield* sql`SELECT id, chat_id, project, cwd, agent_kind, agent_type, parent_session_id, triage_was_non_empty, started_at, ended_at, end_reason FROM sessions WHERE chat_id = ${chatId} LIMIT 1`;
945
+ const rows = yield* sql`SELECT id, chat_id, project, cwd, agent_kind, agent_type, parent_session_id, triage_was_non_empty, started_at, ended_at, end_reason, conversation_id FROM sessions WHERE chat_id = ${chatId} LIMIT 1`;
913
946
  if (rows.length === 0) return Option.none();
914
947
  return Option.some(sessionRowToDetail(rows[0]));
915
948
  }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
@@ -920,7 +953,7 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
920
953
  const findSessionsByChatPrefix = (prefix) => Effect.gen(function* () {
921
954
  yield* Effect.logDebug("findSessionsByChatPrefix").pipe(Effect.annotateLogs({ prefix }));
922
955
  const pattern = `${prefix}%`;
923
- return (yield* sql`SELECT id, chat_id, project, cwd, agent_kind, agent_type, parent_session_id, triage_was_non_empty, started_at, ended_at, end_reason FROM sessions WHERE chat_id LIKE ${pattern} ESCAPE '\\' ORDER BY started_at DESC`).map(sessionRowToDetail);
956
+ return (yield* sql`SELECT id, chat_id, project, cwd, agent_kind, agent_type, parent_session_id, triage_was_non_empty, started_at, ended_at, end_reason, conversation_id FROM sessions WHERE chat_id LIKE ${pattern} ESCAPE '\\' ORDER BY started_at DESC`).map(sessionRowToDetail);
924
957
  }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
925
958
  operation: "read",
926
959
  table: "sessions",
@@ -928,7 +961,7 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
928
961
  })));
929
962
  const findActiveSubagentSession = (parentSessionId) => Effect.gen(function* () {
930
963
  yield* Effect.logDebug("findActiveSubagentSession").pipe(Effect.annotateLogs({ parentSessionId }));
931
- const rows = yield* sql`SELECT id, chat_id, project, cwd, agent_kind, agent_type, parent_session_id, triage_was_non_empty, started_at, ended_at, end_reason FROM sessions WHERE parent_session_id = ${parentSessionId} AND agent_kind = 'subagent' AND ended_at IS NULL ORDER BY started_at DESC LIMIT 1`;
964
+ const rows = yield* sql`SELECT id, chat_id, project, cwd, agent_kind, agent_type, parent_session_id, triage_was_non_empty, started_at, ended_at, end_reason, conversation_id FROM sessions WHERE parent_session_id = ${parentSessionId} AND agent_kind = 'subagent' AND ended_at IS NULL ORDER BY started_at DESC LIMIT 1`;
932
965
  if (rows.length === 0) return Option.none();
933
966
  return Option.some(sessionRowToDetail(rows[0]));
934
967
  }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
@@ -938,7 +971,7 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
938
971
  })));
939
972
  const getSessionByTddTaskId = (tddTaskId) => Effect.gen(function* () {
940
973
  yield* Effect.logDebug("getSessionByTddTaskId").pipe(Effect.annotateLogs({ tddTaskId }));
941
- const rows = yield* sql`SELECT id, chat_id, project, cwd, agent_kind, agent_type, parent_session_id, triage_was_non_empty, started_at, ended_at, end_reason FROM sessions WHERE id = (SELECT session_id FROM tdd_tasks WHERE id = ${tddTaskId}) LIMIT 1`;
974
+ const rows = yield* sql`SELECT id, chat_id, project, cwd, agent_kind, agent_type, parent_session_id, triage_was_non_empty, started_at, ended_at, end_reason, conversation_id FROM sessions WHERE id = (SELECT session_id FROM tdd_tasks WHERE id = ${tddTaskId}) LIMIT 1`;
942
975
  if (rows.length === 0) return Option.none();
943
976
  return Option.some(sessionRowToDetail(rows[0]));
944
977
  }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
@@ -1078,7 +1111,7 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
1078
1111
  const agentKind = options.agentKind ?? null;
1079
1112
  return (yield* sql`
1080
1113
  SELECT id, chat_id, project, cwd, agent_kind, agent_type,
1081
- parent_session_id, triage_was_non_empty, started_at, ended_at, end_reason
1114
+ parent_session_id, triage_was_non_empty, started_at, ended_at, end_reason, conversation_id
1082
1115
  FROM sessions
1083
1116
  WHERE (${project} IS NULL OR project = ${project})
1084
1117
  AND (${agentKind} IS NULL OR agent_kind = ${agentKind})
@@ -1364,7 +1397,8 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
1364
1397
  END AS test_case_authored_in_session,
1365
1398
  a.test_run_id,
1366
1399
  a.test_first_failure_run_id,
1367
- p.behavior_id
1400
+ p.behavior_id,
1401
+ a.suite
1368
1402
  FROM tdd_artifacts a
1369
1403
  JOIN tdd_phases p ON p.id = a.phase_id
1370
1404
  JOIN tdd_tasks ts ON ts.id = p.tdd_task_id
@@ -1384,7 +1418,8 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
1384
1418
  test_case_authored_in_session: r.test_case_authored_in_session === 1,
1385
1419
  test_run_id: r.test_run_id,
1386
1420
  test_first_failure_run_id: r.test_first_failure_run_id,
1387
- behavior_id: r.behavior_id
1421
+ behavior_id: r.behavior_id,
1422
+ suite: r.suite
1388
1423
  });
1389
1424
  }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
1390
1425
  operation: "read",
@@ -1427,7 +1462,8 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
1427
1462
  const listTddTasksForSession = (sessionId, options) => Effect.gen(function* () {
1428
1463
  yield* Effect.logDebug("listTddTasksForSession").pipe(Effect.annotateLogs({
1429
1464
  sessionId,
1430
- walkParents: options?.walkParents ?? false
1465
+ walkParents: options?.walkParents ?? false,
1466
+ walkConversation: options?.walkConversation ?? false
1431
1467
  }));
1432
1468
  const sessionIds = [sessionId];
1433
1469
  if (options?.walkParents === true) {
@@ -1441,9 +1477,23 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
1441
1477
  currentId = next;
1442
1478
  }
1443
1479
  }
1480
+ if (options?.walkConversation === true) {
1481
+ const conversationId = (yield* sql`
1482
+ SELECT conversation_id FROM sessions WHERE id = ${sessionId} LIMIT 1
1483
+ `)[0]?.conversation_id ?? null;
1484
+ if (conversationId !== null) {
1485
+ const conversationSessionRows = yield* sql`
1486
+ SELECT id FROM sessions WHERE conversation_id = ${conversationId}
1487
+ `;
1488
+ for (const row of conversationSessionRows) if (!sessionIds.includes(row.id)) sessionIds.push(row.id);
1489
+ }
1490
+ }
1444
1491
  return (yield* sql`
1445
- SELECT id, session_id, goal, started_at, ended_at, outcome FROM tdd_tasks
1446
- WHERE session_id IN ${sql.in(sessionIds)} ORDER BY started_at DESC
1492
+ SELECT t.id, t.session_id, t.goal, t.started_at, t.ended_at, t.outcome, s.agent_kind
1493
+ FROM tdd_tasks t
1494
+ JOIN sessions s ON s.id = t.session_id
1495
+ WHERE t.session_id IN ${sql.in(sessionIds)}
1496
+ ORDER BY (CASE WHEN s.agent_kind = 'main' THEN 0 ELSE 1 END), t.started_at DESC
1447
1497
  `).map((r) => ({
1448
1498
  id: r.id,
1449
1499
  sessionId: r.session_id,
@@ -1457,6 +1507,31 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
1457
1507
  table: "tdd_tasks",
1458
1508
  reason: extractSqlReason(e)
1459
1509
  })));
1510
+ const countRecentArtifactsInOtherSessionsOfConversation = (input) => Effect.gen(function* () {
1511
+ yield* Effect.logDebug("countRecentArtifactsInOtherSessionsOfConversation").pipe(Effect.annotateLogs(input));
1512
+ const owner = (yield* sql`
1513
+ SELECT t.session_id, s.conversation_id
1514
+ FROM tdd_tasks t
1515
+ JOIN sessions s ON s.id = t.session_id
1516
+ WHERE t.id = ${input.tddTaskId}
1517
+ LIMIT 1
1518
+ `)[0];
1519
+ if (owner === void 0 || owner.conversation_id === null) return 0;
1520
+ return (yield* sql`
1521
+ SELECT COUNT(*) AS n
1522
+ FROM tdd_artifacts a
1523
+ JOIN tdd_phases p ON p.id = a.phase_id
1524
+ JOIN tdd_tasks t ON t.id = p.tdd_task_id
1525
+ JOIN sessions s ON s.id = t.session_id
1526
+ WHERE s.conversation_id = ${owner.conversation_id}
1527
+ AND s.id != ${owner.session_id}
1528
+ AND a.recorded_at >= ${input.sinceIso}
1529
+ `)[0]?.n ?? 0;
1530
+ }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
1531
+ operation: "read",
1532
+ table: "tdd_artifacts",
1533
+ reason: extractSqlReason(e)
1534
+ })));
1460
1535
  const listTddArtifactsForTask = (input) => Effect.gen(function* () {
1461
1536
  yield* Effect.logDebug("listTddArtifactsForTask").pipe(Effect.annotateLogs(input));
1462
1537
  const limit = input.limit ?? 50;
@@ -1471,7 +1546,8 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
1471
1546
  a.test_case_id,
1472
1547
  a.test_run_id,
1473
1548
  a.test_first_failure_run_id,
1474
- a.recorded_at
1549
+ a.recorded_at,
1550
+ a.suite
1475
1551
  FROM tdd_artifacts a
1476
1552
  JOIN tdd_phases p ON p.id = a.phase_id
1477
1553
  WHERE p.tdd_task_id = ${input.tddTaskId}
@@ -1490,7 +1566,8 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
1490
1566
  testCaseId: r.test_case_id,
1491
1567
  testRunId: r.test_run_id,
1492
1568
  testFirstFailureRunId: r.test_first_failure_run_id,
1493
- recordedAt: r.recorded_at
1569
+ recordedAt: r.recorded_at,
1570
+ suite: r.suite
1494
1571
  }));
1495
1572
  }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
1496
1573
  operation: "read",
@@ -1715,6 +1792,7 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
1715
1792
  getTddArtifactWithContext,
1716
1793
  getCommitChanges,
1717
1794
  listTddTasksForSession,
1795
+ countRecentArtifactsInOtherSessionsOfConversation,
1718
1796
  listTddArtifactsForTask,
1719
1797
  listHypotheses,
1720
1798
  findIdempotentResponse,
@@ -201,17 +201,50 @@ const DataStoreLive = Layer.effect(DataStore, Effect.gen(function* () {
201
201
  ];
202
202
  for (const metric of metrics) {
203
203
  const value = g[metric];
204
- if (value !== void 0 && Number.isFinite(value)) yield* sql`INSERT INTO coverage_baselines (project, metric, value, pattern, updated_at) VALUES ('__global__', ${metric}, ${value}, '', ${updatedAt}) ON CONFLICT (project, metric, pattern) DO UPDATE SET value = ${value}, updated_at = ${updatedAt}`;
204
+ if (value !== void 0 && Number.isFinite(value)) yield* sql`INSERT INTO coverage_baselines (project, kind, metric, value, pattern, updated_at) VALUES ('__global__', 'baseline', ${metric}, ${value}, '', ${updatedAt}) ON CONFLICT (project, kind, metric, pattern) DO UPDATE SET value = ${value}, updated_at = ${updatedAt}`;
205
205
  }
206
206
  for (const [pattern, thresholds] of patterns) for (const metric of metrics) {
207
207
  const value = thresholds[metric];
208
- if (value !== void 0 && Number.isFinite(value)) yield* sql`INSERT INTO coverage_baselines (project, metric, value, pattern, updated_at) VALUES ('__global__', ${metric}, ${value}, ${pattern}, ${updatedAt}) ON CONFLICT (project, metric, pattern) DO UPDATE SET value = ${value}, updated_at = ${updatedAt}`;
208
+ if (value !== void 0 && Number.isFinite(value)) yield* sql`INSERT INTO coverage_baselines (project, kind, metric, value, pattern, updated_at) VALUES ('__global__', 'baseline', ${metric}, ${value}, ${pattern}, ${updatedAt}) ON CONFLICT (project, kind, metric, pattern) DO UPDATE SET value = ${value}, updated_at = ${updatedAt}`;
209
209
  }
210
210
  }).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => new DataStoreError({
211
211
  operation: "write",
212
212
  table: "coverage_baselines",
213
213
  reason: extractSqlReason(e)
214
214
  })));
215
+ /**
216
+ * Shared upsert for the `coverage_baselines` table's non-baseline
217
+ * `kind` rows (`threshold` / `target`). Distinct from `writeBaselines`
218
+ * so the ratcheted high-water mark never collides with the resolved,
219
+ * enforced Vitest thresholds or the aspirational coverageTargets
220
+ * (issue #237).
221
+ */
222
+ const writeCoveragePolicy = (kind, policy) => sql.withTransaction(Effect.gen(function* () {
223
+ yield* Effect.logDebug("writeCoveragePolicy").pipe(Effect.annotateLogs({ kind }));
224
+ const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
225
+ const { global: g, patterns = [] } = policy;
226
+ const metrics = [
227
+ "lines",
228
+ "functions",
229
+ "branches",
230
+ "statements"
231
+ ];
232
+ yield* sql`DELETE FROM coverage_baselines WHERE project = '__global__' AND kind = ${kind}`;
233
+ for (const metric of metrics) {
234
+ const value = g[metric];
235
+ if (value !== void 0 && Number.isFinite(value)) yield* sql`INSERT INTO coverage_baselines (project, kind, metric, value, pattern, updated_at) VALUES ('__global__', ${kind}, ${metric}, ${value}, '', ${updatedAt})`;
236
+ }
237
+ for (const [pattern, thresholds] of patterns) for (const metric of metrics) {
238
+ const value = thresholds[metric];
239
+ if (value !== void 0 && Number.isFinite(value)) yield* sql`INSERT INTO coverage_baselines (project, kind, metric, value, pattern, updated_at) VALUES ('__global__', ${kind}, ${metric}, ${value}, ${pattern}, ${updatedAt})`;
240
+ }
241
+ })).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => new DataStoreError({
242
+ operation: "write",
243
+ table: "coverage_baselines",
244
+ reason: extractSqlReason(e)
245
+ })));
246
+ const writeThresholds = (thresholds) => writeCoveragePolicy("threshold", thresholds);
247
+ const writeTargets = (targets) => writeCoveragePolicy("target", targets);
215
248
  const writeTrends = (project, runId, entry) => Effect.gen(function* () {
216
249
  yield* Effect.logDebug("writeTrends").pipe(Effect.annotateLogs({
217
250
  project,
@@ -311,7 +344,7 @@ const DataStoreLive = Layer.effect(DataStore, Effect.gen(function* () {
311
344
  })));
312
345
  const writeSession = (input) => Effect.gen(function* () {
313
346
  yield* Effect.logDebug("writeSession").pipe(Effect.annotateLogs({ chatId: input.chatId }));
314
- yield* sql`INSERT INTO sessions (chat_id, project, cwd, agent_kind, agent_type, parent_session_id, triage_was_non_empty, started_at) VALUES (${input.chatId}, ${input.project}, ${input.cwd}, ${input.agentKind}, ${input.agentType ?? null}, ${input.parentSessionId ?? null}, ${boolToInt(input.triageWasNonEmpty) ?? 0}, ${input.startedAt})`;
347
+ yield* sql`INSERT INTO sessions (chat_id, project, cwd, agent_kind, agent_type, parent_session_id, triage_was_non_empty, started_at, conversation_id) VALUES (${input.chatId}, ${input.project}, ${input.cwd}, ${input.agentKind}, ${input.agentType ?? null}, ${input.parentSessionId ?? null}, ${boolToInt(input.triageWasNonEmpty) ?? 0}, ${input.startedAt}, ${input.conversationId ?? null})`;
315
348
  return (yield* sql`SELECT id FROM sessions WHERE chat_id = ${input.chatId}`)[0].id;
316
349
  }).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => new DataStoreError({
317
350
  operation: "write",
@@ -320,13 +353,24 @@ const DataStoreLive = Layer.effect(DataStore, Effect.gen(function* () {
320
353
  })));
321
354
  const upsertSession = (input) => Effect.gen(function* () {
322
355
  yield* Effect.logDebug("upsertSession").pipe(Effect.annotateLogs({ chatId: input.chatId }));
323
- yield* sql`INSERT INTO sessions (chat_id, project, cwd, agent_kind, agent_type, parent_session_id, triage_was_non_empty, started_at) VALUES (${input.chatId}, ${input.project}, ${input.cwd}, ${input.agentKind}, ${input.agentType ?? null}, ${input.parentSessionId ?? null}, ${boolToInt(input.triageWasNonEmpty) ?? 0}, ${input.startedAt}) ON CONFLICT(chat_id) DO NOTHING`;
356
+ yield* sql`INSERT INTO sessions (chat_id, project, cwd, agent_kind, agent_type, parent_session_id, triage_was_non_empty, started_at, conversation_id) VALUES (${input.chatId}, ${input.project}, ${input.cwd}, ${input.agentKind}, ${input.agentType ?? null}, ${input.parentSessionId ?? null}, ${boolToInt(input.triageWasNonEmpty) ?? 0}, ${input.startedAt}, ${input.conversationId ?? null}) ON CONFLICT(chat_id) DO NOTHING`;
324
357
  return (yield* sql`SELECT id FROM sessions WHERE chat_id = ${input.chatId}`)[0].id;
325
358
  }).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => new DataStoreError({
326
359
  operation: "write",
327
360
  table: "sessions",
328
361
  reason: extractSqlReason(e)
329
362
  })));
363
+ const setSessionConversationIdIfNull = (input) => Effect.gen(function* () {
364
+ yield* Effect.logDebug("setSessionConversationIdIfNull").pipe(Effect.annotateLogs(input));
365
+ yield* sql`
366
+ UPDATE sessions SET conversation_id = ${input.conversationId}
367
+ WHERE id = ${input.sessionId} AND conversation_id IS NULL
368
+ `;
369
+ }).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => new DataStoreError({
370
+ operation: "write",
371
+ table: "sessions",
372
+ reason: extractSqlReason(e)
373
+ })));
330
374
  const writeTurn = (input) => sql.withTransaction(Effect.gen(function* () {
331
375
  yield* Effect.logDebug("writeTurn").pipe(Effect.annotateLogs({
332
376
  sessionId: input.sessionId,
@@ -875,7 +919,7 @@ const DataStoreLive = Layer.effect(DataStore, Effect.gen(function* () {
875
919
  return (yield* sql`
876
920
  INSERT INTO tdd_artifacts
877
921
  (phase_id, artifact_kind, file_id, test_case_id, test_run_id,
878
- test_first_failure_run_id, diff_excerpt, recorded_at)
922
+ test_first_failure_run_id, diff_excerpt, recorded_at, suite)
879
923
  VALUES
880
924
  (
881
925
  ${input.phaseId},
@@ -885,7 +929,8 @@ const DataStoreLive = Layer.effect(DataStore, Effect.gen(function* () {
885
929
  ${input.testRunId ?? null},
886
930
  ${input.testFirstFailureRunId ?? null},
887
931
  ${truncatedDiff},
888
- ${input.recordedAt}
932
+ ${input.recordedAt},
933
+ ${input.suite ?? "vitest"}
889
934
  )
890
935
  RETURNING id
891
936
  `)[0].id;
@@ -1151,6 +1196,8 @@ const DataStoreLive = Layer.effect(DataStore, Effect.gen(function* () {
1151
1196
  writeCoverage,
1152
1197
  writeHistory,
1153
1198
  writeBaselines,
1199
+ writeThresholds,
1200
+ writeTargets,
1154
1201
  writeTrends,
1155
1202
  writeSourceMap,
1156
1203
  writeNote,
@@ -1158,6 +1205,7 @@ const DataStoreLive = Layer.effect(DataStore, Effect.gen(function* () {
1158
1205
  deleteNote,
1159
1206
  writeSession,
1160
1207
  upsertSession,
1208
+ setSessionConversationIdIfNull,
1161
1209
  writeTurn,
1162
1210
  writeFailureSignature,
1163
1211
  endSession,
@@ -340,13 +340,16 @@ const migration = Effect.gen(function* () {
340
340
  CREATE TABLE coverage_baselines (
341
341
  id INTEGER PRIMARY KEY AUTOINCREMENT,
342
342
  project TEXT NOT NULL,
343
+ kind TEXT NOT NULL DEFAULT 'baseline' CHECK (kind IN (
344
+ 'baseline', 'threshold', 'target'
345
+ )),
343
346
  metric TEXT NOT NULL CHECK (metric IN (
344
347
  'lines', 'functions', 'branches', 'statements'
345
348
  )),
346
349
  value REAL NOT NULL,
347
350
  pattern TEXT NOT NULL DEFAULT '',
348
351
  updated_at TEXT NOT NULL,
349
- UNIQUE(project, metric, pattern)
352
+ UNIQUE(project, kind, metric, pattern)
350
353
  )
351
354
  `;
352
355
  yield* sql`
@@ -691,6 +694,7 @@ const migration = Effect.gen(function* () {
691
694
  test_first_failure_run_id INTEGER REFERENCES test_runs(id) ON DELETE SET NULL,
692
695
  diff_excerpt TEXT,
693
696
  recorded_at TEXT NOT NULL,
697
+ suite TEXT NOT NULL DEFAULT 'vitest' CHECK (suite IN ('vitest', 'bats')),
694
698
  UNIQUE (phase_id, artifact_kind, file_id, test_run_id)
695
699
  )
696
700
  `;
@@ -764,7 +768,7 @@ const migration = Effect.gen(function* () {
764
768
  CREATE TRIGGER trg_sessions_conv_id_immutable
765
769
  AFTER UPDATE ON sessions
766
770
  FOR EACH ROW
767
- WHEN OLD.conversation_id IS NOT NEW.conversation_id
771
+ WHEN OLD.conversation_id IS NOT NULL AND OLD.conversation_id IS NOT NEW.conversation_id
768
772
  BEGIN
769
773
  SELECT RAISE(ABORT, 'sessions.conversation_id is immutable');
770
774
  END
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitest-agent/sdk",
3
- "version": "2.4.13",
3
+ "version": "2.5.1",
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": [
@@ -45,19 +45,19 @@
45
45
  "./package.json": "./package.json"
46
46
  },
47
47
  "dependencies": {
48
- "@effect/platform-node": "4.0.0-rc.109",
49
- "@effect/sql-sqlite-node": "4.0.0-rc.109",
50
- "@effected/config-file": "^0.5.2",
51
- "@effected/jsonc": "^0.8.1",
52
- "@effected/toml": "^0.5.0",
53
- "@effected/walker": "^0.5.0",
54
- "@effected/workspaces": "^0.18.3",
55
- "@effected/xdg": "^0.3.0",
56
- "@effected/yaml": "^0.12.0",
48
+ "@effect/platform-node": "4.0.0-rc.112",
49
+ "@effect/sql-sqlite-node": "4.0.0-rc.112",
50
+ "@effected/config-file": "^0.6.0",
51
+ "@effected/jsonc": "^0.9.0",
52
+ "@effected/toml": "^0.6.0",
53
+ "@effected/walker": "^0.6.0",
54
+ "@effected/workspaces": "^0.20.0",
55
+ "@effected/xdg": "^0.4.0",
56
+ "@effected/yaml": "^0.13.0",
57
57
  "acorn": "^8.18.0",
58
58
  "acorn-typescript": "^1.4.13",
59
59
  "ajv": "^8.20.0",
60
- "effect": "4.0.0-rc.109",
60
+ "effect": "4.0.0-rc.112",
61
61
  "std-env": "^4.2.0"
62
62
  },
63
63
  "engines": {
@@ -14,15 +14,32 @@ const ConsoleLeakFile = Schema.Struct({
14
14
  sample: Schema.optional(Schema.String)
15
15
  }).annotate({ identifier: "ConsoleLeakFile" });
16
16
  /**
17
+ * Summary of console output logged inside tests that did NOT pass. Kept
18
+ * separate from `total`/`byFile` so it can never be mistaken for a leak in a
19
+ * passing test — assertion-failure logging that routes through `console.*`
20
+ * (common with logger-backed assertion libraries) would otherwise camouflage
21
+ * genuine leaks on every red run.
22
+ * @public
23
+ */
24
+ const ConsoleLeaksFromFailingTests = Schema.Struct({
25
+ total: Schema.Number,
26
+ files: Schema.Number
27
+ }).annotate({ identifier: "ConsoleLeaksFromFailingTests" });
28
+ /**
17
29
  * Aggregate stray-console-output signal for a single test run. Omitted from
18
- * a report entirely when the run produced no user console output.
30
+ * a report entirely when the run produced no user console output at all
31
+ * (neither non-failing nor failing-test writes). `total`/`byFile` count only
32
+ * output from tests that did not fail; `fromFailingTests`, when present,
33
+ * summarizes the output Vitest captured from failing tests so it stays
34
+ * visible without polluting the actionable leak signal (issue #263).
19
35
  * @public
20
36
  */
21
37
  const ConsoleLeaks = Schema.Struct({
22
38
  total: Schema.Number,
23
39
  byFile: Schema.Array(ConsoleLeakFile),
24
- truncated: Schema.optional(Schema.Boolean)
40
+ truncated: Schema.optional(Schema.Boolean),
41
+ fromFailingTests: Schema.optional(ConsoleLeaksFromFailingTests)
25
42
  }).annotate({ identifier: "ConsoleLeaks" });
26
43
 
27
44
  //#endregion
28
- export { ConsoleLeakFile, ConsoleLeaks };
45
+ export { ConsoleLeakFile, ConsoleLeaks, ConsoleLeaksFromFailingTests };
@@ -41,6 +41,7 @@ const CoverageReport = Schema.Struct({
41
41
  })),
42
42
  scoped: Schema.Boolean.pipe(Schema.withDecodingDefaultKey(Effect.succeed(false))),
43
43
  scopedFiles: Schema.optional(Schema.Array(Schema.String)),
44
+ totalFiles: Schema.optional(Schema.Number),
44
45
  lowCoverage: Schema.Array(FileCoverageReport),
45
46
  lowCoverageFiles: Schema.Array(Schema.String),
46
47
  belowTarget: Schema.optional(Schema.Array(FileCoverageReport)),
@@ -76,7 +76,10 @@ const CoverageRenderState = Schema.Struct({
76
76
  metric: CoverageMetric,
77
77
  expected: Schema.Number,
78
78
  actual: Schema.Number
79
- }))
79
+ })),
80
+ scoped: Schema.optional(Schema.Boolean),
81
+ scopedFiles: Schema.optional(Schema.Number),
82
+ totalFiles: Schema.optional(Schema.Number)
80
83
  }).annotate({ identifier: "CoverageRenderState" });
81
84
  /**
82
85
  * A `SuggestedAction` event, denormalized into the queue the renderer reads.
@@ -145,7 +148,14 @@ const RenderState = Schema.Struct({
145
148
  * that never carried the field (older replay data, hand-built
146
149
  * fixtures) falls back to `moduleOrder.length`.
147
150
  */
148
- collectedModules: Schema.optional(Schema.Number)
151
+ collectedModules: Schema.optional(Schema.Number),
152
+ /**
153
+ * Process-level errors with no owning module, folded from
154
+ * `RunFinished.unhandledErrors`. A run that never carried the field
155
+ * (older replay data, hand-built fixtures) defaults to `[]`. See
156
+ * issue #240.
157
+ */
158
+ unhandledErrors: Schema.Array(ReportError)
149
159
  }).annotate({ identifier: "RenderState" });
150
160
  /**
151
161
  * Initial state — what the reducer returns when no events have been
@@ -171,7 +181,8 @@ const initialRenderState = {
171
181
  coverage: null,
172
182
  trend: null,
173
183
  failures: [],
174
- suggestedActions: []
184
+ suggestedActions: [],
185
+ unhandledErrors: []
175
186
  };
176
187
 
177
188
  //#endregion
@@ -169,7 +169,10 @@ const RunEvent = Schema.Union([
169
169
  Schema.TaggedStruct("CoverageReady", {
170
170
  metrics: CoverageTotals,
171
171
  thresholds: MetricThresholds,
172
- gaps: Schema.Array(CoverageGap)
172
+ gaps: Schema.Array(CoverageGap),
173
+ scoped: Schema.optional(Schema.Boolean),
174
+ scopedFiles: Schema.optional(Schema.Number),
175
+ totalFiles: Schema.optional(Schema.Number)
173
176
  }),
174
177
  Schema.TaggedStruct("ThresholdViolation", {
175
178
  metric: CoverageMetric,
@@ -202,7 +205,14 @@ const RunEvent = Schema.Union([
202
205
  * true count into `RenderState` even when `moduleOrder` only
203
206
  * tracks failing modules (report replay). See issue #204.
204
207
  */
205
- collectedModules: Schema.optional(Schema.Number)
208
+ collectedModules: Schema.optional(Schema.Number),
209
+ /**
210
+ * Process-level errors with no owning module (an unhandled
211
+ * rejection, a worker crash) — Vitest's `unhandledErrors`
212
+ * argument to `onTestRunEnd`. Optional so hand-built fixtures
213
+ * and older replay data keep decoding. See issue #240.
214
+ */
215
+ unhandledErrors: Schema.optional(Schema.Array(ReportError))
206
216
  })
207
217
  ]).annotate({ identifier: "RunEvent" });
208
218