@vitest-agent/sdk 2.5.0 → 3.0.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.
package/index.js CHANGED
@@ -25,7 +25,7 @@ import { HistoryTracker, historyKey } from "./services/HistoryTracker.js";
25
25
  import { DataReaderLive } from "./layers/DataReaderLive.js";
26
26
  import { ActorType, AgentId, ChatId, ConversationId, HostKind, ProjectKey, TddTaskId } from "./schemas/Identity.js";
27
27
  import { Agent, IdempotencyHit } from "./schemas/Agent.js";
28
- import { DataStore } from "./services/DataStore.js";
28
+ import { DataStore, INLINE_ATTACHMENT_BODY_CAP_BYTES } from "./services/DataStore.js";
29
29
  import { coerceErrorField, coerceErrorText } from "./utils/coerce-error-text.js";
30
30
  import { DataStoreLive } from "./layers/DataStoreLive.js";
31
31
  import { DetailResolver } from "./services/DetailResolver.js";
@@ -63,11 +63,13 @@ import { RunContextLive, RunContextTest } from "./layers/RunContextLive.js";
63
63
  import { formatTriageEffect } from "./lib/format-triage.js";
64
64
  import { formatWrapupEffect } from "./lib/format-wrapup.js";
65
65
  import migration from "./migrations/0001_initial.js";
66
- import migration$1 from "./migrations/registry_0001_initial.js";
67
- import migration$2 from "./migrations/session_map_0001_initial.js";
66
+ import migration$1 from "./migrations/0002_test_artifacts.js";
67
+ import { PROJECT_MIGRATIONS } from "./migrations/index.js";
68
+ import migration$2 from "./migrations/registry_0001_initial.js";
69
+ import migration$3 from "./migrations/session_map_0001_initial.js";
68
70
  import { AgentConsoleMode, CiConsoleMode, ConsoleMode, ConsoleOutputMode, DetailLevel, Environment, Executor, HumanConsoleMode, OutputFormat, PackageManager, ReportError, TestClassification, TestRunReason, TestState } from "./schemas/Common.js";
69
71
  import { ConsoleLeakFile, ConsoleLeaks, ConsoleLeaksFromFailingTests } from "./schemas/ConsoleLeaks.js";
70
- import { MetricThresholds, PatternThresholds, ResolvedThresholds } from "./schemas/Thresholds.js";
72
+ import { MetricThresholds, PatternMetricThresholds, PatternThresholds, PerFileThresholds, ResolvedThresholds } from "./schemas/Thresholds.js";
71
73
  import { CoverageReport, CoverageTotals, FileCoverageReport } from "./schemas/Coverage.js";
72
74
  import { AgentReport, ModuleReport, ReportSummary, TagCountEntry, TestReport } from "./schemas/AgentReport.js";
73
75
  import { CoverageBaselines } from "./schemas/Baselines.js";
@@ -77,9 +79,11 @@ import { CoverageLevel, resolveCoverageInput, validateCoverageConfig } from "./s
77
79
  import { CoverageTargets, CoverageTargetsMetrics } from "./schemas/CoverageTargets.js";
78
80
  import { HistoryRecord, TestHistory, TestRun } from "./schemas/History.js";
79
81
  import { Transport } from "./schemas/Transport.js";
80
- import { AgentPluginOptions, AgentReporterOptions, ConsoleOutputs } from "./schemas/Options.js";
82
+ import { AgentPluginOptions, AgentReporterOptions, ConsoleOutputs, ReportOption, ReportSettings } from "./schemas/Options.js";
83
+ import { TestAnnotation, TestArtifact, TestArtifactLocation, TestAttachment } from "./schemas/TestArtifacts.js";
81
84
  import { ActionSeverity, ConsoleLogLevel, CoverageGap, CoverageMetric, HookStatus, HookType, RunEvent } from "./schemas/RunEvent.js";
82
85
  import { CoverageRenderState, FailureRecord, ModuleRecord, RenderState, RenderTotals, SuggestedActionRecord, TestRecord, initialRenderState } from "./schemas/RenderState.js";
86
+ import { RUN_REPORT_FILE_SCHEMA_URL, RunReportFile } from "./schemas/RunReportFile.js";
83
87
  import { BehaviorDetail, BehaviorRow, BehaviorStatus, GoalDetail, GoalRow, GoalStatus } from "./schemas/Tdd.js";
84
88
  import { TrendEntry, TrendRecord } from "./schemas/Trends.js";
85
89
  import { FileEditPayload } from "./schemas/turns/FileEditPayload.js";
@@ -119,7 +123,7 @@ import { requiredArtifactForTransition, transitionEnforcesBehaviorMatch, validat
119
123
  * substituted yet. Exported for version introspection by downstream tooling.
120
124
  * @public
121
125
  */
122
- const CURRENT_SDK_VERSION = "2.5.0";
126
+ const CURRENT_SDK_VERSION = "3.0.0";
123
127
 
124
128
  //#endregion
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 };
129
+ 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, INLINE_ATTACHMENT_BODY_CAP_BYTES, IdempotencyHit, IllegalStatusTransitionError, JsonFormatter, LoggerLive, MarkdownFormatter, MetricThresholds, ModuleRecord, ModuleReport, NON_DISCOVERABLE_DIRS, NotePayload, OutputFormat, OutputPipelineLive, OutputRenderer, OutputRendererLive, PROJECT_MIGRATIONS, PackageManager, PathResolutionError, PathResolutionLive, PatternMetricThresholds, PatternThresholds, PerClientSessionMapReader, PerClientSessionMapReaderLive, PerClientSessionMapWriter, PerClientSessionMapWriterLive, PerFileThresholds, PhaseTransitionEvent, ProbeMissError, ProjectDiscovery, ProjectDiscoveryLive, ProjectDiscoveryTest, ProjectIdentity, ProjectIdentityLive, ProjectIdentityNotResolvableError, ProjectIdentityTest, ProjectKey, RUN_REPORT_FILE_SCHEMA_URL, RegistrationConflictError, RenderState, RenderTotals, ReportError, ReportOption, ReportSettings, ReportSummary, ResolvedThresholds, RunContext, RunContext$, RunContextLive, RunContextService, RunContextTest, RunEvent, RunReportFile, SRC_DIR, SessionCompleteEvent, SessionNotFoundError, SessionOutcome, SidecarTimeoutError, SilentFormatter, SuggestedActionRecord, TEST_DIR, TEST_FILE_GLOB_SUFFIX, TEST_HELPER_DIRS, TagCountEntry, TddTaskAlreadyEndedError, TddTaskId, TddTaskNotFoundError, TerminalFormatter, TestAnnotation, TestArtifact, TestArtifactLocation, TestAttachment, 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, migration$1 as migration0002, normalizeAssertionShape, normalizeWorkspaceKey, osc8, probeHostMetadataFromEnv, migration$2 as registryMigration0001, relativePath, requiredArtifactForTransition, resolveCoverageInput, resolveDataPath, resolveLogFile, resolveLogLevel, resolveProjectIdentityFromCandidates, resolveProjectKeyFromCwd, resolveWorkspaceKey, rewriteBashCommand, safeFilename, migration$3 as sessionMapMigration0001, stripAnsi, transitionEnforcesBehaviorMatch, validateCoverageConfig, validateCoverageTargetsShape, validatePhaseTransition };
@@ -602,6 +602,94 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
602
602
  table: "test_errors",
603
603
  reason: extractSqlReason(e)
604
604
  })));
605
+ const latestRunId = (project) => Effect.gen(function* () {
606
+ const runs = yield* sql`SELECT id FROM test_runs WHERE project = ${project} ORDER BY timestamp DESC LIMIT 1`;
607
+ return runs.length === 0 ? null : runs[0].id;
608
+ });
609
+ const attachmentsFor = (column, ownerId) => Effect.gen(function* () {
610
+ return (column === "annotation_id" ? yield* sql`SELECT content_type, path, body, body_encoding, byte_size FROM attachments WHERE annotation_id = ${ownerId} ORDER BY id` : yield* sql`SELECT content_type, path, body, body_encoding, byte_size FROM attachments WHERE artifact_id = ${ownerId} ORDER BY id`).map((r) => ({
611
+ ...r.content_type !== null && { contentType: r.content_type },
612
+ ...r.path !== null && { path: r.path },
613
+ ...r.body !== null && { body: r.body },
614
+ ...r.body_encoding !== null && { bodyEncoding: r.body_encoding },
615
+ byteSize: r.byte_size
616
+ }));
617
+ });
618
+ const getAnnotationsForTest = (project, fullName, options) => Effect.gen(function* () {
619
+ yield* Effect.logDebug("getAnnotationsForTest").pipe(Effect.annotateLogs({
620
+ project,
621
+ fullName
622
+ }));
623
+ const runId = yield* latestRunId(project);
624
+ if (runId === null) return [];
625
+ const modulePath = options?.modulePath ?? null;
626
+ const rows = yield* sql`SELECT ta.id, ta.type, ta.message, ta.location_line, ta.location_column, f.path AS location_path
627
+ FROM test_annotations ta
628
+ JOIN test_cases tc ON tc.id = ta.test_case_id
629
+ JOIN test_modules tm ON tm.id = tc.module_id
630
+ LEFT JOIN files f ON f.id = ta.location_file_id
631
+ WHERE tm.run_id = ${runId} AND tc.full_name = ${fullName}
632
+ AND (${modulePath} IS NULL OR tm.relative_module_id = ${modulePath})
633
+ ORDER BY ta.id`;
634
+ const out = [];
635
+ for (const r of rows) {
636
+ const attachments = yield* attachmentsFor("annotation_id", r.id);
637
+ out.push({
638
+ id: r.id,
639
+ type: r.type,
640
+ message: r.message,
641
+ ...r.location_path !== null && r.location_line !== null && r.location_column !== null && { location: {
642
+ file: r.location_path,
643
+ line: r.location_line,
644
+ column: r.location_column
645
+ } },
646
+ attachments
647
+ });
648
+ }
649
+ return out;
650
+ }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
651
+ operation: "read",
652
+ table: "test_annotations",
653
+ reason: extractSqlReason(e)
654
+ })));
655
+ const getArtifactsForTest = (project, fullName, options) => Effect.gen(function* () {
656
+ yield* Effect.logDebug("getArtifactsForTest").pipe(Effect.annotateLogs({
657
+ project,
658
+ fullName
659
+ }));
660
+ const runId = yield* latestRunId(project);
661
+ if (runId === null) return [];
662
+ const modulePath = options?.modulePath ?? null;
663
+ const rows = yield* sql`SELECT ta.id, ta.type, ta.message, ta.data, ta.location_line, ta.location_column, f.path AS location_path
664
+ FROM test_artifacts ta
665
+ JOIN test_cases tc ON tc.id = ta.test_case_id
666
+ JOIN test_modules tm ON tm.id = tc.module_id
667
+ LEFT JOIN files f ON f.id = ta.location_file_id
668
+ WHERE tm.run_id = ${runId} AND tc.full_name = ${fullName}
669
+ AND (${modulePath} IS NULL OR tm.relative_module_id = ${modulePath})
670
+ ORDER BY ta.id`;
671
+ const out = [];
672
+ for (const r of rows) {
673
+ const attachments = yield* attachmentsFor("artifact_id", r.id);
674
+ out.push({
675
+ id: r.id,
676
+ type: r.type,
677
+ message: r.message,
678
+ data: r.data,
679
+ ...r.location_path !== null && r.location_line !== null && r.location_column !== null && { location: {
680
+ file: r.location_path,
681
+ line: r.location_line,
682
+ column: r.location_column
683
+ } },
684
+ attachments
685
+ });
686
+ }
687
+ return out;
688
+ }).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
689
+ operation: "read",
690
+ table: "test_artifacts",
691
+ reason: extractSqlReason(e)
692
+ })));
605
693
  const getNotes = (scope, project, testFullName) => Effect.gen(function* () {
606
694
  yield* Effect.logDebug("getNotes").pipe(Effect.annotateLogs({
607
695
  scope: scope ?? null,
@@ -1759,6 +1847,8 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
1759
1847
  getCoverage,
1760
1848
  getTestsForFile,
1761
1849
  getErrors,
1850
+ getAnnotationsForTest,
1851
+ getArtifactsForTest,
1762
1852
  getNotes,
1763
1853
  getNoteById,
1764
1854
  searchNotes,
@@ -2,10 +2,11 @@ import { AgentNotFoundError, RegistrationConflictError } from "../errors/AgentEr
2
2
  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
- import { DataStore } from "../services/DataStore.js";
5
+ import { DataStore, INLINE_ATTACHMENT_BODY_CAP_BYTES } from "../services/DataStore.js";
6
6
  import { coerceErrorText } from "../utils/coerce-error-text.js";
7
7
  import { Effect, Layer, Option } from "effect";
8
8
  import { SqlClient } from "effect/unstable/sql/SqlClient";
9
+ import { Buffer } from "node:buffer";
9
10
  import { randomUUID } from "node:crypto";
10
11
 
11
12
  //#region src/layers/DataStoreLive.ts
@@ -163,6 +164,49 @@ const DataStoreLive = Layer.effect(DataStore, Effect.gen(function* () {
163
164
  table: "test_errors",
164
165
  reason: extractSqlReason(e)
165
166
  })));
167
+ const writeAttachments = (owner, attachments) => Effect.gen(function* () {
168
+ for (const att of attachments) {
169
+ const artifactId = "artifactId" in owner ? owner.artifactId : null;
170
+ const annotationId = "annotationId" in owner ? owner.annotationId : null;
171
+ const storedBytes = att.body === void 0 ? 0 : Buffer.byteLength(att.body, "utf8");
172
+ const inline = att.body !== void 0 && Math.max(storedBytes, att.byteSize) <= 65536;
173
+ const body = inline ? att.body : null;
174
+ const bodyEncoding = inline ? att.bodyEncoding ?? null : null;
175
+ yield* sql`INSERT INTO attachments (artifact_id, annotation_id, content_type, path, body, body_encoding, byte_size) VALUES (${artifactId}, ${annotationId}, ${att.contentType ?? null}, ${att.path ?? null}, ${body}, ${bodyEncoding}, ${att.byteSize})`;
176
+ }
177
+ });
178
+ const writeAnnotations = (runId, annotations) => Effect.gen(function* () {
179
+ yield* Effect.logDebug("writeAnnotations").pipe(Effect.annotateLogs({
180
+ runId,
181
+ count: annotations.length
182
+ }));
183
+ for (const anno of annotations) {
184
+ const locationFileId = anno.locationFile !== void 0 ? yield* ensureFile(anno.locationFile) : null;
185
+ yield* sql`INSERT INTO test_annotations (test_case_id, type, message, location_file_id, location_line, location_column) VALUES (${anno.testCaseId}, ${anno.type}, ${anno.message}, ${locationFileId}, ${anno.locationLine ?? null}, ${anno.locationColumn ?? null})`;
186
+ const idRows = yield* sql`SELECT last_insert_rowid() as id`;
187
+ yield* writeAttachments({ annotationId: idRows[0].id }, anno.attachments ?? []);
188
+ }
189
+ }).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => new DataStoreError({
190
+ operation: "write",
191
+ table: "test_annotations",
192
+ reason: extractSqlReason(e)
193
+ })));
194
+ const writeArtifacts = (runId, artifacts) => Effect.gen(function* () {
195
+ yield* Effect.logDebug("writeArtifacts").pipe(Effect.annotateLogs({
196
+ runId,
197
+ count: artifacts.length
198
+ }));
199
+ for (const art of artifacts) {
200
+ const locationFileId = art.locationFile !== void 0 ? yield* ensureFile(art.locationFile) : null;
201
+ yield* sql`INSERT INTO test_artifacts (test_case_id, type, message, data, location_file_id, location_line, location_column) VALUES (${art.testCaseId}, ${art.type}, ${art.message ?? null}, ${art.data ?? null}, ${locationFileId}, ${art.locationLine ?? null}, ${art.locationColumn ?? null})`;
202
+ const idRows = yield* sql`SELECT last_insert_rowid() as id`;
203
+ yield* writeAttachments({ artifactId: idRows[0].id }, art.attachments ?? []);
204
+ }
205
+ }).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => new DataStoreError({
206
+ operation: "write",
207
+ table: "test_artifacts",
208
+ reason: extractSqlReason(e)
209
+ })));
166
210
  const writeCoverage = (runId, coverage) => Effect.gen(function* () {
167
211
  yield* Effect.logDebug("writeCoverage").pipe(Effect.annotateLogs({
168
212
  runId,
@@ -183,7 +227,7 @@ const DataStoreLive = Layer.effect(DataStore, Effect.gen(function* () {
183
227
  modulePath,
184
228
  runId
185
229
  }));
186
- yield* sql`INSERT INTO test_history (run_id, project, module_path, full_name, timestamp, state, duration, flaky, retry_count, error_message) VALUES (${runId}, ${project}, ${modulePath}, ${fullName}, ${timestamp}, ${state}, ${duration}, ${flaky ? 1 : 0}, ${retryCount}, ${errorMessage})`;
230
+ yield* sql`INSERT INTO test_history (run_id, project, module_path, full_name, timestamp, state, duration, flaky, retry_count, error_message) VALUES (${runId}, ${project}, ${modulePath}, ${fullName}, ${timestamp}, ${state}, ${duration}, ${flaky ? 1 : 0}, ${retryCount}, ${errorMessage}) ON CONFLICT(project, module_path, full_name, timestamp) DO UPDATE SET run_id = excluded.run_id, state = excluded.state, duration = excluded.duration, flaky = excluded.flaky, retry_count = excluded.retry_count, error_message = excluded.error_message`;
187
231
  yield* sql`DELETE FROM test_history WHERE id NOT IN (SELECT id FROM test_history WHERE project = ${project} AND module_path = ${modulePath} AND full_name = ${fullName} ORDER BY timestamp DESC LIMIT 10) AND project = ${project} AND module_path = ${modulePath} AND full_name = ${fullName}`;
188
232
  }).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => new DataStoreError({
189
233
  operation: "write",
@@ -1193,6 +1237,8 @@ const DataStoreLive = Layer.effect(DataStore, Effect.gen(function* () {
1193
1237
  writeSuites,
1194
1238
  writeTestCases,
1195
1239
  writeErrors,
1240
+ writeAnnotations,
1241
+ writeArtifacts,
1196
1242
  writeCoverage,
1197
1243
  writeHistory,
1198
1244
  writeBaselines,
@@ -0,0 +1,30 @@
1
+ import { Effect } from "effect";
2
+ import { SqlClient } from "effect/unstable/sql/SqlClient";
3
+
4
+ //#region src/migrations/0002_test_artifacts.ts
5
+ /** @internal */
6
+ const migration = Effect.gen(function* () {
7
+ const sql = yield* SqlClient;
8
+ yield* sql`DROP INDEX IF EXISTS idx_test_annotations_case`;
9
+ yield* sql`DROP INDEX IF EXISTS idx_test_annotations_type`;
10
+ yield* sql`DROP TABLE IF EXISTS test_annotations`;
11
+ yield* sql`
12
+ CREATE TABLE test_annotations (
13
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
14
+ test_case_id INTEGER NOT NULL REFERENCES test_cases(id) ON DELETE CASCADE,
15
+ type TEXT NOT NULL,
16
+ message TEXT NOT NULL,
17
+ location_file_id INTEGER REFERENCES files(id),
18
+ location_line INTEGER,
19
+ location_column INTEGER
20
+ )
21
+ `;
22
+ yield* sql`CREATE INDEX idx_test_annotations_case ON test_annotations(test_case_id)`;
23
+ yield* sql`CREATE INDEX idx_test_annotations_type ON test_annotations(type)`;
24
+ yield* sql`ALTER TABLE test_artifacts ADD COLUMN data TEXT`;
25
+ yield* sql`ALTER TABLE attachments ADD COLUMN byte_size INTEGER`;
26
+ yield* sql`ALTER TABLE attachments ADD COLUMN body_encoding TEXT`;
27
+ });
28
+
29
+ //#endregion
30
+ export { migration as default };
@@ -0,0 +1,26 @@
1
+ import migration from "./0001_initial.js";
2
+ import migration$1 from "./0002_test_artifacts.js";
3
+
4
+ //#region src/migrations/index.ts
5
+ /**
6
+ * Every migration that must run against a per-project `data.db`, keyed by
7
+ * migration id in application order.
8
+ *
9
+ * This is the single source of truth for the project database's migration
10
+ * set: `ensureMigrated`, the sdk testing layer, `ReporterLive`, `McpLive`,
11
+ * `CliLive` and `SidecarLive` all pass it straight to
12
+ * `SqliteMigrator.fromRecord`, so adding a migration file and registering it
13
+ * here is enough to reach every process that opens a project database.
14
+ *
15
+ * The session-map and discovery-registry databases are separate schemas with
16
+ * their own migrations and are deliberately not listed here.
17
+ *
18
+ * @public
19
+ */
20
+ const PROJECT_MIGRATIONS = {
21
+ "0001_initial": migration,
22
+ "0002_test_artifacts": migration$1
23
+ };
24
+
25
+ //#endregion
26
+ export { PROJECT_MIGRATIONS };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitest-agent/sdk",
3
- "version": "2.5.0",
3
+ "version": "3.0.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": [
@@ -42,22 +42,23 @@
42
42
  "import": "./testing.js",
43
43
  "default": "./testing.js"
44
44
  },
45
+ "./schemas/*.json": "./schemas/*.json",
45
46
  "./package.json": "./package.json"
46
47
  },
47
48
  "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.19.0",
55
- "@effected/xdg": "^0.3.0",
56
- "@effected/yaml": "^0.12.0",
49
+ "@effect/platform-node": "4.0.0-rc.112",
50
+ "@effect/sql-sqlite-node": "4.0.0-rc.112",
51
+ "@effected/config-file": "^0.7.0",
52
+ "@effected/jsonc": "^0.9.0",
53
+ "@effected/toml": "^0.6.0",
54
+ "@effected/walker": "^0.7.0",
55
+ "@effected/workspaces": "^0.20.1",
56
+ "@effected/xdg": "^0.4.1",
57
+ "@effected/yaml": "^0.14.0",
57
58
  "acorn": "^8.18.0",
58
59
  "acorn-typescript": "^1.4.13",
59
60
  "ajv": "^8.20.0",
60
- "effect": "4.0.0-rc.109",
61
+ "effect": "4.0.0-rc.112",
61
62
  "std-env": "^4.2.0"
62
63
  },
63
64
  "engines": {
@@ -1,3 +1,4 @@
1
+ import { PerFileThresholds } from "./Thresholds.js";
1
2
  import { Schema } from "effect";
2
3
 
3
4
  //#region src/schemas/CoverageTargets.ts
@@ -8,7 +9,8 @@ import { Schema } from "effect";
8
9
  const PositivePercent = Schema.Number.check(Schema.isGreaterThan(0));
9
10
  /**
10
11
  * Per-metric leaf shape for coverageTargets entries. Allows optional
11
- * numeric targets per metric and the `100: true` shortcut.
12
+ * numeric targets per metric, the `100: true` shortcut, and — mirroring
13
+ * Vitest 5 — a per-glob `perFile` setting.
12
14
  * @public
13
15
  */
14
16
  const CoverageTargetsMetrics = Schema.Struct({
@@ -16,7 +18,8 @@ const CoverageTargetsMetrics = Schema.Struct({
16
18
  functions: Schema.optional(PositivePercent),
17
19
  branches: Schema.optional(PositivePercent),
18
20
  statements: Schema.optional(PositivePercent),
19
- 100: Schema.optional(Schema.Literal(true))
21
+ 100: Schema.optional(Schema.Literal(true)),
22
+ perFile: Schema.optional(PerFileThresholds)
20
23
  }).annotate({ identifier: "CoverageTargetsMetrics" });
21
24
  /**
22
25
  * Typed schema for the coverageTargets option.
@@ -24,8 +27,10 @@ const CoverageTargetsMetrics = Schema.Struct({
24
27
  * Mirrors Vitest's coverage.thresholds shape — per-metric positive numbers,
25
28
  * the `100: true` shortcut (only valid at key `"100"`), and glob-pattern
26
29
  * entries with metric objects. Positive numbers only; negatives and zeros
27
- * are rejected. `perFile` is not allowed here — inherit it from
28
- * `coverage.thresholds.perFile`.
30
+ * are rejected. A top-level `perFile` is not allowed here — set it on
31
+ * `coverage.thresholds.perFile`. A `perFile` inside a glob-pattern entry
32
+ * IS allowed, mirroring Vitest 5, where a glob entry no longer inherits
33
+ * the top-level setting.
29
34
  *
30
35
  * A decode-time refinement rejects `true` at any key other than `"100"`
31
36
  * so `{ statements: true }` fails parse rather than silently flowing
@@ -25,18 +25,34 @@ const ConsoleOutputs = Schema.Struct({
25
25
  ci: Schema.optional(CiConsoleMode)
26
26
  }).annotate({ identifier: "ConsoleOutputs" });
27
27
  /**
28
+ * Report-file settings. An object overrides the scope directory name
29
+ * used under `.vitest/`; the default scope is `vitest-agent`.
30
+ *
31
+ * @public
32
+ */
33
+ const ReportSettings = Schema.Struct({ scope: Schema.optional(Schema.String.check(Schema.makeFilter((scope) => scope.includes("/") || scope.includes("\\") || scope === ".." || scope === "." ? `report.scope ${scope} must be a single directory name — it is resolved directly under .vitest/ and cannot nest or escape.` : void 0))) }).annotate({ identifier: "ReportSettings" });
34
+ /**
35
+ * Report-file settings. `false` disables `.vitest/<scope>/` writes
36
+ * entirely; an object overrides the scope directory name.
37
+ *
38
+ * Default: on for the `agent` and `ci` executors, off for `human`.
39
+ * @public
40
+ */
41
+ const ReportOption = Schema.Union([Schema.Literal(false), ReportSettings]).annotate({ identifier: "ReportOption" });
42
+ /**
28
43
  * The 2.0 user-facing options for `AgentPlugin`.
29
44
  *
30
- * Five fields total. Two are function-typed (`reporter`, `onRunEvent`)
45
+ * Six fields total. Two are function-typed (`reporter`, `onRunEvent`)
31
46
  * and live on the `AgentPluginConstructorOptions` companion interface in
32
47
  * `@vitest-agent/plugin` — Effect Schema cannot encode functions cleanly,
33
- * so the schema-decodable struct carries the three data-shaped fields.
48
+ * so the schema-decodable struct carries the four data-shaped fields.
34
49
  * @public
35
50
  */
36
51
  const AgentPluginOptions = Schema.Struct({
37
52
  console: Schema.optional(ConsoleOutputs),
38
53
  coverageTargets: Schema.optional(CoverageTargets),
39
- transport: Schema.optional(Transport)
54
+ transport: Schema.optional(Transport),
55
+ report: Schema.optional(ReportOption)
40
56
  }).annotate({ identifier: "AgentPluginOptions" });
41
57
  /**
42
58
  * Reporter-implementation options.
@@ -54,4 +70,4 @@ const AgentReporterOptions = Schema.Struct({
54
70
  projectFilter: Schema.optional(Schema.String) }).annotate({ identifier: "AgentReporterOptions" });
55
71
 
56
72
  //#endregion
57
- export { AgentPluginOptions, AgentReporterOptions, ConsoleOutputs };
73
+ export { AgentPluginOptions, AgentReporterOptions, ConsoleOutputs, ReportOption, ReportSettings };
@@ -1,6 +1,7 @@
1
1
  import { ReportError, TestClassification, TestState } from "./Common.js";
2
2
  import { MetricThresholds } from "./Thresholds.js";
3
3
  import { CoverageTotals, FileCoverageReport } from "./Coverage.js";
4
+ import { TestArtifactLocation, TestAttachment } from "./TestArtifacts.js";
4
5
  import { Schema } from "effect";
5
6
 
6
7
  //#region src/schemas/RunEvent.ts
@@ -145,13 +146,18 @@ const RunEvent = Schema.Union([
145
146
  modulePath: Schema.String,
146
147
  testName: Schema.String,
147
148
  suitePath: Schema.Array(Schema.String),
148
- annotation: Schema.String
149
+ annotation: Schema.String,
150
+ annotationType: Schema.String,
151
+ location: Schema.optional(TestArtifactLocation),
152
+ attachments: Schema.Array(TestAttachment)
149
153
  }),
150
154
  Schema.TaggedStruct("TestArtifactRecorded", {
151
155
  modulePath: Schema.String,
152
156
  testName: Schema.String,
153
157
  suitePath: Schema.Array(Schema.String),
154
- artifact: Schema.String
158
+ artifact: Schema.String,
159
+ location: Schema.optional(TestArtifactLocation),
160
+ attachments: Schema.Array(TestAttachment)
155
161
  }),
156
162
  Schema.TaggedStruct("WatcherReady", {}),
157
163
  Schema.TaggedStruct("WatcherRerun", {
@@ -0,0 +1,51 @@
1
+ import { AgentReport } from "./AgentReport.js";
2
+ import { Schema } from "effect";
3
+
4
+ //#region src/schemas/RunReportFile.ts
5
+ /**
6
+ * The `.vitest/vitest-agent/run.json` file contract.
7
+ *
8
+ * This is a versioned public contract, not the raw `AgentReport`
9
+ * encoding — a Claude Code hook or a CI step reads this file, so the
10
+ * envelope carries its own `schemaVersion`. `reports` holds one
11
+ * `AgentReport` per Vitest project in the run.
12
+ */
13
+ /**
14
+ * Canonical `$schema` URL for `RunReportFile` version 1.
15
+ *
16
+ * Written as the first key of `run.json` so an editor or a JSON Schema
17
+ * validator can resolve the contract without knowing about vitest-agent.
18
+ *
19
+ * @public
20
+ */
21
+ const RUN_REPORT_FILE_SCHEMA_URL = "https://vitest-agent.dev/schemas/run-report-file-1.0.0.json";
22
+ /**
23
+ * ISO-8601 instant, the shape `Date.prototype.toISOString` produces.
24
+ * `Date.parse` alone is too permissive — it accepts `"Sep 7 2026"` and
25
+ * other locale spellings a JSON Schema validator would reject — so the
26
+ * pattern is checked first and the parse only rules out impossible
27
+ * component values such as month 13.
28
+ */
29
+ const ISO_8601_INSTANT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
30
+ const ISO_8601_MESSAGE = "generatedAt must be an ISO-8601 instant, e.g. 2026-09-07T23:45:57.308Z.";
31
+ const IsoInstant = Schema.String.check(Schema.isPattern(ISO_8601_INSTANT, {
32
+ description: "An ISO-8601 instant, the shape Date.prototype.toISOString produces.",
33
+ message: ISO_8601_MESSAGE
34
+ }), Schema.makeFilter((value) => Number.isNaN(Date.parse(value)) ? ISO_8601_MESSAGE : void 0)).annotate({ identifier: "IsoInstant" });
35
+ /**
36
+ * Envelope for the `run.json` report file.
37
+ *
38
+ * @public
39
+ */
40
+ const RunReportFile = Schema.Struct({
41
+ $schema: Schema.optionalKey(Schema.String),
42
+ schemaVersion: Schema.Literal(1),
43
+ generatedAt: IsoInstant,
44
+ reports: Schema.Array(AgentReport)
45
+ }).annotate({
46
+ identifier: "RunReportFile",
47
+ description: "The vitest-agent run report file (.vitest/vitest-agent/run.json): one AgentReport per Vitest project from the most recent run. Read reports[].summary for pass/fail counts and reports[].failed for the failing modules and their tests. schemaVersion is the envelope contract version; generatedAt is when the run finished."
48
+ });
49
+
50
+ //#endregion
51
+ export { RUN_REPORT_FILE_SCHEMA_URL, RunReportFile };
@@ -0,0 +1,74 @@
1
+ import { Schema } from "effect";
2
+
3
+ //#region src/schemas/TestArtifacts.ts
4
+ /**
5
+ * Schemas for Vitest 5 test annotations and test artifacts.
6
+ *
7
+ * Vocabulary (spec §4.1): a *test annotation* is a `context.annotate`
8
+ * note; a *test artifact* is a `recordArtifact` payload. Neither is a
9
+ * *TDD artifact* (`tdd_artifacts`), which is red/green evidence.
10
+ */
11
+ /**
12
+ * Source position a test annotation or test artifact points at.
13
+ * @public
14
+ */
15
+ const TestArtifactLocation = Schema.Struct({
16
+ file: Schema.String,
17
+ line: Schema.Number,
18
+ column: Schema.Number
19
+ }).annotate({ identifier: "TestArtifactLocation" });
20
+ /**
21
+ * One attachment descriptor.
22
+ *
23
+ * `path` is the location Vitest already rewrote the attachment to
24
+ * (usually under `.vitest/attachments/`) or an external `http(s)` URL —
25
+ * vitest-agent never copies the file itself. `byteSize` is always
26
+ * recorded so a dangling path stays describable. Vitest treats a string
27
+ * `body` as base64 unless `bodyEncoding` is `"utf-8"`.
28
+ *
29
+ * `body` and `bodyEncoding` are populated only on the persistence path,
30
+ * for inline attachments under the 64 KiB cap. Attachments carried by
31
+ * `TestAnnotated` and `TestArtifactRecorded` run events are descriptors
32
+ * only — never assume an event attachment has a body; read the stored
33
+ * row when the bytes are needed.
34
+ * @public
35
+ */
36
+ const TestAttachment = Schema.Struct({
37
+ contentType: Schema.optional(Schema.String),
38
+ path: Schema.optional(Schema.String),
39
+ body: Schema.optional(Schema.String),
40
+ bodyEncoding: Schema.optional(Schema.Literals(["base64", "utf-8"])),
41
+ byteSize: Schema.Number
42
+ }).annotate({ identifier: "TestAttachment" });
43
+ /**
44
+ * A test annotation as recorded by `context.annotate`.
45
+ *
46
+ * `type` is an arbitrary string — Vitest applies no enum
47
+ * (`.repos/vitest/packages/vitest/src/runtime/runner/types.ts`).
48
+ * @public
49
+ */
50
+ const TestAnnotation = Schema.Struct({
51
+ type: Schema.String,
52
+ message: Schema.String,
53
+ location: Schema.optional(TestArtifactLocation),
54
+ attachments: Schema.Array(TestAttachment)
55
+ }).annotate({ identifier: "TestAnnotation" });
56
+ /**
57
+ * A test artifact as recorded by `recordArtifact`.
58
+ *
59
+ * `data` is the JSON encoding of the artifact's custom fields, minus
60
+ * `attachments` and `location` (both modelled separately). Artifacts
61
+ * whose `type` starts with `internal:` are Vitest's own and are not
62
+ * persisted.
63
+ * @public
64
+ */
65
+ const TestArtifact = Schema.Struct({
66
+ type: Schema.String,
67
+ message: Schema.optional(Schema.String),
68
+ data: Schema.optional(Schema.String),
69
+ location: Schema.optional(TestArtifactLocation),
70
+ attachments: Schema.Array(TestAttachment)
71
+ }).annotate({ identifier: "TestArtifact" });
72
+
73
+ //#endregion
74
+ export { TestAnnotation, TestArtifact, TestArtifactLocation, TestAttachment };
@@ -12,19 +12,39 @@ const MetricThresholds = Schema.Struct({
12
12
  statements: Schema.optional(Schema.Number)
13
13
  }).annotate({ identifier: "MetricThresholds" });
14
14
  /**
15
+ * A `perFile` setting. Vitest 5 allows either a plain boolean or a
16
+ * per-metric object that overrides the metric numbers used for the
17
+ * per-file check.
18
+ *
19
+ * An object constrains exactly the metrics it names, so `{}` enforces
20
+ * nothing at all. Write `true` to check every metric.
21
+ * @public
22
+ */
23
+ const PerFileThresholds = Schema.Union([Schema.Boolean, MetricThresholds]).annotate({ identifier: "PerFileThresholds" });
24
+ /**
25
+ * The metric values carried by a glob-pattern entry. Under Vitest 5 a
26
+ * glob entry no longer inherits the top-level `perFile`, so it carries
27
+ * its own optional setting.
28
+ * @public
29
+ */
30
+ const PatternMetricThresholds = Schema.Struct({
31
+ ...MetricThresholds.fields,
32
+ perFile: Schema.optionalKey(PerFileThresholds)
33
+ }).annotate({ identifier: "PatternMetricThresholds" });
34
+ /**
15
35
  * A glob pattern paired with its metric thresholds.
16
36
  * @public
17
37
  */
18
- const PatternThresholds = Schema.Tuple([Schema.String, MetricThresholds]).annotate({ identifier: "PatternThresholds" });
38
+ const PatternThresholds = Schema.Tuple([Schema.String, PatternMetricThresholds]).annotate({ identifier: "PatternThresholds" });
19
39
  /**
20
40
  * Fully resolved thresholds ready for evaluation.
21
41
  * @public
22
42
  */
23
43
  const ResolvedThresholds = Schema.Struct({
24
44
  global: MetricThresholds,
25
- perFile: Schema.Boolean.pipe(Schema.withDecodingDefaultKey(Effect.succeed(false))),
45
+ perFile: PerFileThresholds.pipe(Schema.withDecodingDefaultKey(Effect.succeed(false))),
26
46
  patterns: Schema.Array(PatternThresholds).pipe(Schema.withDecodingDefaultKey(Effect.succeed([])))
27
47
  }).annotate({ identifier: "ResolvedThresholds" });
28
48
 
29
49
  //#endregion
30
- export { MetricThresholds, PatternThresholds, ResolvedThresholds };
50
+ export { MetricThresholds, PatternMetricThresholds, PatternThresholds, PerFileThresholds, ResolvedThresholds };