@vitest-agent/sdk 1.2.0 → 1.3.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.d.ts CHANGED
@@ -1932,6 +1932,7 @@ type TestRun = typeof TestRun.Type;
1932
1932
  * @public
1933
1933
  */
1934
1934
  declare const TestHistory: Schema.Struct<{
1935
+ modulePath: typeof Schema.String;
1935
1936
  fullName: typeof Schema.String;
1936
1937
  runs: Schema.Array$<Schema.Struct<{
1937
1938
  timestamp: typeof Schema.String;
@@ -1948,6 +1949,7 @@ declare const HistoryRecord: Schema.Struct<{
1948
1949
  project: typeof Schema.String;
1949
1950
  updatedAt: typeof Schema.String;
1950
1951
  tests: Schema.Array$<Schema.Struct<{
1952
+ modulePath: typeof Schema.String;
1951
1953
  fullName: typeof Schema.String;
1952
1954
  runs: Schema.Array$<Schema.Struct<{
1953
1955
  timestamp: typeof Schema.String;
@@ -2596,7 +2598,7 @@ declare const DataStore_base: Context.TagClass<DataStore, "vitest-agent/DataStor
2596
2598
  readonly writeTestCases: (moduleId: number, tests: ReadonlyArray<TestCaseInput>) => Effect.Effect<ReadonlyArray<number>, DataStoreError>;
2597
2599
  readonly writeErrors: (runId: number, errors: ReadonlyArray<TestErrorInput>) => Effect.Effect<void, DataStoreError>;
2598
2600
  readonly writeCoverage: (runId: number, coverage: ReadonlyArray<FileCoverageInput>) => Effect.Effect<void, DataStoreError>;
2599
- readonly writeHistory: (project: string, fullName: string, runId: number, timestamp: string, state: string, duration: number | null, flaky: boolean, retryCount: number, errorMessage: string | null) => Effect.Effect<void, DataStoreError>;
2601
+ readonly writeHistory: (project: string, fullName: string, modulePath: string, runId: number, timestamp: string, state: string, duration: number | null, flaky: boolean, retryCount: number, errorMessage: string | null) => Effect.Effect<void, DataStoreError>;
2600
2602
  readonly writeBaselines: (baselines: CoverageBaselines) => Effect.Effect<void, DataStoreError>;
2601
2603
  readonly writeTrends: (project: string, runId: number, entry: TrendEntry) => Effect.Effect<void, DataStoreError>;
2602
2604
  readonly writeSourceMap: (sourceFilePath: string, testModuleId: number, mappingType: string) => Effect.Effect<void, DataStoreError>;
@@ -2682,6 +2684,7 @@ interface ProjectRunSummary {
2682
2684
  /** @public */
2683
2685
  interface FlakyTest {
2684
2686
  readonly fullName: string;
2687
+ readonly modulePath: string;
2685
2688
  readonly project: string;
2686
2689
  readonly passCount: number;
2687
2690
  readonly failCount: number;
@@ -2691,6 +2694,7 @@ interface FlakyTest {
2691
2694
  /** @public */
2692
2695
  interface PersistentFailure {
2693
2696
  readonly fullName: string;
2697
+ readonly modulePath: string;
2694
2698
  readonly project: string;
2695
2699
  readonly consecutiveFailures: number;
2696
2700
  readonly firstFailedAt: string;
@@ -3208,9 +3212,22 @@ declare const FormatSelectorLive: Layer.Layer<FormatSelector, never, never>;
3208
3212
  * @public
3209
3213
  */
3210
3214
  interface TestOutcome {
3215
+ readonly modulePath: string;
3211
3216
  readonly fullName: string;
3212
3217
  readonly state: "passed" | "failed";
3213
3218
  }
3219
+ /**
3220
+ * Builds the composite (modulePath, fullName) key used to key the internal
3221
+ * testMap and the returned classifications Map, so identically-named tests
3222
+ * in different files classify independently instead of colliding.
3223
+ *
3224
+ * Uses `JSON.stringify` for an injective encoding: a plain delimiter such as
3225
+ * a space is not collision-proof (e.g. `("a b", "c")` and `("a", "b c")` would
3226
+ * both yield `"a b c"`), which would reintroduce the very collision class this
3227
+ * key exists to prevent.
3228
+ * @public
3229
+ */
3230
+ declare const historyKey: (modulePath: string, fullName: string) => string;
3214
3231
  declare const HistoryTracker_base: Context.TagClass<HistoryTracker, "vitest-agent/HistoryTracker", {
3215
3232
  readonly classify: (project: string, testOutcomes: ReadonlyArray<TestOutcome>, timestamp: string) => Effect.Effect<{
3216
3233
  history: HistoryRecord;
@@ -4325,12 +4342,15 @@ interface ManifestRow {
4325
4342
  * @public
4326
4343
  */
4327
4344
  interface HistoryRow {
4345
+ module_path: string;
4328
4346
  full_name: string;
4329
4347
  timestamp: string;
4330
4348
  state: string;
4331
4349
  }
4332
4350
  /**
4333
- * A `Record<fullName, { runs: TestRun[] }>` keyed by test full name.
4351
+ * A `Record<compositeKey, { modulePath, fullName, runs: TestRun[] }>` keyed by
4352
+ * the injective `historyKey(modulePath, fullName)` string, so identically-named
4353
+ * tests in different files are tracked as distinct entries.
4334
4354
  *
4335
4355
  * This is the internal representation returned by `assembleHistoryRecord`.
4336
4356
  * Callers that need the Effect Schema `HistoryRecord` type should adapt this
@@ -4338,7 +4358,9 @@ interface HistoryRow {
4338
4358
  * @public
4339
4359
  */
4340
4360
  interface AssembledHistoryRecord {
4341
- [fullName: string]: {
4361
+ [compositeKey: string]: {
4362
+ modulePath: string;
4363
+ fullName: string;
4342
4364
  runs: TestRun[];
4343
4365
  };
4344
4366
  }
@@ -4351,10 +4373,13 @@ interface AssembledHistoryRecord {
4351
4373
  */
4352
4374
  declare function assembleManifest(rows: ManifestRow[], dbPath: string): CacheManifest;
4353
4375
  /**
4354
- * Groups test_history rows by `full_name` into a keyed record.
4376
+ * Groups test_history rows by the composite (module_path, full_name) key
4377
+ * into a keyed record.
4355
4378
  *
4356
- * Returns `Record<fullName, { runs: TestRun[] }>` where `runs` are the
4357
- * accumulated test runs for that test, in the order they appear in `rows`.
4379
+ * Returns `Record<compositeKey, { modulePath, fullName, runs: TestRun[] }>`
4380
+ * where `runs` are the accumulated test runs for that test, in the order
4381
+ * they appear in `rows`. Two rows sharing `full_name` but differing
4382
+ * `module_path` are tracked as distinct entries rather than merged.
4358
4383
  *
4359
4384
  * Only rows with state `"passed"` or `"failed"` are included in runs
4360
4385
  * (matching the `TestRun` schema constraint).
@@ -5240,5 +5265,5 @@ declare function validateCoverageTargetsShape(input: unknown): CoverageTargetsSh
5240
5265
  */
5241
5266
  declare const CURRENT_SDK_VERSION: string;
5242
5267
  //#endregion
5243
- 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, deriveIdempotencyKey, detectPackageManager, detectVitestScripts, determineTier, ensureMigrated, extractSqlReason, findFunctionBoundary, formatConsoleMarkdown, formatFatalError, formatGfm, formatTriageEffect, formatWrapupEffect, getRecentDirection, getRunCommand, getWorstMetric, gitUrlToProjectKey, hashTargets, 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 };
5268
+ 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, 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 };
5244
5269
  //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -20,6 +20,7 @@ import { VitestAgentConfig } from "./schemas/Config.js";
20
20
  import { VitestAgentConfigFile } from "./services/Config.js";
21
21
  import { ConfigLive } from "./layers/ConfigLive.js";
22
22
  import { DataReader } from "./services/DataReader.js";
23
+ import { HistoryTracker, historyKey } from "./services/HistoryTracker.js";
23
24
  import { DataReaderLive } from "./layers/DataReaderLive.js";
24
25
  import { ActorType, AgentId, ChatId, ConversationId, HostKind, ProjectKey, TddTaskId } from "./schemas/Identity.js";
25
26
  import { Agent, IdempotencyHit } from "./schemas/Agent.js";
@@ -36,7 +37,6 @@ import { ExecutorResolver } from "./services/ExecutorResolver.js";
36
37
  import { ExecutorResolverLive } from "./layers/ExecutorResolverLive.js";
37
38
  import { FormatSelector } from "./services/FormatSelector.js";
38
39
  import { FormatSelectorLive } from "./layers/FormatSelectorLive.js";
39
- import { HistoryTracker } from "./services/HistoryTracker.js";
40
40
  import { classifyTest } from "./utils/classify-test.js";
41
41
  import { HistoryTrackerLive } from "./layers/HistoryTrackerLive.js";
42
42
  import { HistoryTrackerTest } from "./layers/HistoryTrackerTest.js";
@@ -115,7 +115,7 @@ import { requiredArtifactForTransition, transitionEnforcesBehaviorMatch, validat
115
115
  * substituted yet. Exported for version introspection by downstream tooling.
116
116
  * @public
117
117
  */
118
- const CURRENT_SDK_VERSION = "1.2.0";
118
+ const CURRENT_SDK_VERSION = "1.3.0";
119
119
 
120
120
  //#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, deriveIdempotencyKey, detectPackageManager, detectVitestScripts, determineTier, ensureMigrated, extractSqlReason, findFunctionBoundary, formatConsoleMarkdown, formatFatalError, formatGfm, formatTriageEffect, formatWrapupEffect, getRecentDirection, getRunCommand, getWorstMetric, gitUrlToProjectKey, hashTargets, 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 };
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, 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 };
@@ -1,5 +1,6 @@
1
1
  import { DataStoreError, extractSqlReason } from "../errors/DataStoreError.js";
2
2
  import { DataReader } from "../services/DataReader.js";
3
+ import { historyKey } from "../services/HistoryTracker.js";
3
4
  import { Effect, Layer, Option } from "effect";
4
5
  import { SqlClient } from "@effect/sql/SqlClient";
5
6
 
@@ -195,24 +196,30 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
195
196
  })));
196
197
  const getHistory = (project) => Effect.gen(function* () {
197
198
  yield* Effect.logDebug("getHistory").pipe(Effect.annotateLogs({ project }));
198
- const rows = yield* sql`SELECT full_name, timestamp, state
199
+ const rows = yield* sql`SELECT module_path, full_name, timestamp, state
199
200
  FROM test_history
200
201
  WHERE project = ${project}
201
- ORDER BY full_name, timestamp DESC`;
202
+ ORDER BY module_path, full_name, timestamp DESC`;
202
203
  const testsMap = /* @__PURE__ */ new Map();
203
204
  for (const row of rows) {
204
205
  if (row.state !== "passed" && row.state !== "failed") continue;
205
- const existing = testsMap.get(row.full_name);
206
- if (existing) existing.push({
206
+ const key = historyKey(row.module_path, row.full_name);
207
+ const existing = testsMap.get(key);
208
+ if (existing) existing.runs.push({
207
209
  timestamp: row.timestamp,
208
210
  state: row.state
209
211
  });
210
- else testsMap.set(row.full_name, [{
211
- timestamp: row.timestamp,
212
- state: row.state
213
- }]);
212
+ else testsMap.set(key, {
213
+ modulePath: row.module_path,
214
+ fullName: row.full_name,
215
+ runs: [{
216
+ timestamp: row.timestamp,
217
+ state: row.state
218
+ }]
219
+ });
214
220
  }
215
- const tests = Array.from(testsMap.entries()).map(([fullName, runs]) => ({
221
+ const tests = Array.from(testsMap.values()).map(({ modulePath, fullName, runs }) => ({
222
+ modulePath,
216
223
  fullName,
217
224
  runs
218
225
  }));
@@ -307,27 +314,31 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
307
314
  yield* Effect.logDebug("getFlaky").pipe(Effect.annotateLogs({ project }));
308
315
  return (yield* sql`SELECT
309
316
  th1.full_name,
317
+ th1.module_path,
310
318
  th1.project,
311
319
  SUM(CASE WHEN th1.state = 'passed' THEN 1 ELSE 0 END) as pass_count,
312
320
  SUM(CASE WHEN th1.state = 'failed' THEN 1 ELSE 0 END) as fail_count,
313
321
  (SELECT state FROM test_history th2
314
322
  WHERE th2.full_name = th1.full_name
323
+ AND th2.module_path = th1.module_path
315
324
  AND th2.project = th1.project
316
325
  ORDER BY timestamp DESC LIMIT 1) as last_state,
317
326
  MAX(th1.timestamp) as last_timestamp
318
327
  FROM test_history th1
319
328
  WHERE th1.project = ${project}
320
- GROUP BY th1.full_name, th1.project
329
+ GROUP BY th1.full_name, th1.module_path, th1.project
321
330
  -- Flaky means oscillation, not a clean recovery. A test with both
322
331
  -- passes and fails is only flaky when at least one failure occurs at
323
332
  -- or after the earliest pass (a fail-after-pass regression). A
324
333
  -- monotonic red->green cycle — every failure precedes every pass — is
325
334
  -- a recovery, not flakiness, so it is excluded here. ISO-8601
326
- -- timestamps compare lexicographically.
335
+ -- timestamps compare lexicographically. Grouping keys on module_path
336
+ -- so same-named tests in different files stay distinct series.
327
337
  HAVING pass_count > 0 AND fail_count > 0
328
338
  AND MAX(CASE WHEN th1.state = 'failed' THEN th1.timestamp END)
329
339
  >= MIN(CASE WHEN th1.state = 'passed' THEN th1.timestamp END)`).map((r) => ({
330
340
  fullName: r.full_name,
341
+ modulePath: r.module_path,
331
342
  project: r.project,
332
343
  passCount: r.pass_count,
333
344
  failCount: r.fail_count,
@@ -342,36 +353,38 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
342
353
  const getPersistentFailures = (project) => Effect.gen(function* () {
343
354
  yield* Effect.logDebug("getPersistentFailures").pipe(Effect.annotateLogs({ project }));
344
355
  return (yield* sql`WITH ranked AS (
345
- SELECT full_name, project, state, timestamp, error_message,
356
+ SELECT full_name, module_path, project, state, timestamp, error_message,
346
357
  ROW_NUMBER() OVER (
347
- PARTITION BY project, full_name
358
+ PARTITION BY project, module_path, full_name
348
359
  ORDER BY timestamp DESC
349
360
  ) as rn
350
361
  FROM test_history
351
362
  WHERE project = ${project}
352
363
  ),
353
364
  streak AS (
354
- SELECT full_name, project, state, timestamp, error_message, rn,
365
+ SELECT full_name, module_path, project, state, timestamp, error_message, rn,
355
366
  MIN(CASE WHEN state != 'failed' THEN rn END) OVER (
356
- PARTITION BY project, full_name
367
+ PARTITION BY project, module_path, full_name
357
368
  ) as first_pass_rn
358
369
  FROM ranked
359
370
  )
360
371
  SELECT
361
- full_name, project,
372
+ full_name, module_path, project,
362
373
  COUNT(*) as consecutive_failures,
363
374
  MIN(timestamp) as first_failed_at,
364
375
  MAX(timestamp) as last_failed_at,
365
376
  (SELECT error_message FROM streak s2
366
377
  WHERE s2.full_name = streak.full_name
378
+ AND s2.module_path = streak.module_path
367
379
  AND s2.project = streak.project
368
380
  AND s2.rn = 1) as last_error_message
369
381
  FROM streak
370
382
  WHERE state = 'failed'
371
383
  AND (first_pass_rn IS NULL OR rn < first_pass_rn)
372
- GROUP BY full_name, project
384
+ GROUP BY full_name, module_path, project
373
385
  HAVING consecutive_failures >= 2`).map((row) => ({
374
386
  fullName: row.full_name,
387
+ modulePath: row.module_path,
375
388
  project: row.project,
376
389
  consecutiveFailures: row.consecutive_failures,
377
390
  firstFailedAt: row.first_failed_at,
@@ -174,13 +174,14 @@ const DataStoreLive = Layer.effect(DataStore, Effect.gen(function* () {
174
174
  table: "file_coverage",
175
175
  reason: extractSqlReason(e)
176
176
  })));
177
- const writeHistory = (project, fullName, runId, timestamp, state, duration, flaky, retryCount, errorMessage) => Effect.gen(function* () {
177
+ const writeHistory = (project, fullName, modulePath, runId, timestamp, state, duration, flaky, retryCount, errorMessage) => Effect.gen(function* () {
178
178
  yield* Effect.logDebug("writeHistory").pipe(Effect.annotateLogs({
179
179
  project,
180
+ modulePath,
180
181
  runId
181
182
  }));
182
- yield* sql`INSERT INTO test_history (run_id, project, full_name, timestamp, state, duration, flaky, retry_count, error_message) VALUES (${runId}, ${project}, ${fullName}, ${timestamp}, ${state}, ${duration}, ${flaky ? 1 : 0}, ${retryCount}, ${errorMessage})`;
183
- yield* sql`DELETE FROM test_history WHERE id NOT IN (SELECT id FROM test_history WHERE project = ${project} AND full_name = ${fullName} ORDER BY timestamp DESC LIMIT 10) AND project = ${project} AND full_name = ${fullName}`;
183
+ 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})`;
184
+ 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}`;
184
185
  }).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => new DataStoreError({
185
186
  operation: "write",
186
187
  table: "test_history",
@@ -1,5 +1,5 @@
1
1
  import { DataReader } from "../services/DataReader.js";
2
- import { HistoryTracker } from "../services/HistoryTracker.js";
2
+ import { HistoryTracker, historyKey } from "../services/HistoryTracker.js";
3
3
  import { classifyTest } from "../utils/classify-test.js";
4
4
  import { Effect, Layer } from "effect";
5
5
 
@@ -11,26 +11,28 @@ const HistoryTrackerLive = Layer.effect(HistoryTracker, Effect.gen(function* ()
11
11
  return { classify: (project, testOutcomes, timestamp) => Effect.gen(function* () {
12
12
  const existing = yield* reader.getHistory(project);
13
13
  const testMap = /* @__PURE__ */ new Map();
14
- for (const entry of existing.tests) testMap.set(entry.fullName, {
14
+ for (const entry of existing.tests) testMap.set(historyKey(entry.modulePath, entry.fullName), {
15
15
  ...entry,
16
16
  runs: [...entry.runs]
17
17
  });
18
18
  const classifications = /* @__PURE__ */ new Map();
19
19
  for (const outcome of testOutcomes) {
20
- let entry = testMap.get(outcome.fullName);
20
+ const key = historyKey(outcome.modulePath, outcome.fullName);
21
+ let entry = testMap.get(key);
21
22
  if (!entry) {
22
23
  entry = {
24
+ modulePath: outcome.modulePath,
23
25
  fullName: outcome.fullName,
24
26
  runs: []
25
27
  };
26
- testMap.set(outcome.fullName, entry);
28
+ testMap.set(key, entry);
27
29
  }
28
30
  const priorRuns = entry.runs;
29
31
  entry.runs = [{
30
32
  timestamp,
31
33
  state: outcome.state
32
34
  }, ...priorRuns].slice(0, WINDOW_SIZE);
33
- classifications.set(outcome.fullName, classifyTest(outcome.state, priorRuns));
35
+ classifications.set(key, classifyTest(outcome.state, priorRuns));
34
36
  }
35
37
  return {
36
38
  history: {
@@ -352,6 +352,7 @@ const migration = Effect.gen(function* () {
352
352
  id INTEGER PRIMARY KEY AUTOINCREMENT,
353
353
  run_id INTEGER NOT NULL REFERENCES test_runs(id) ON DELETE CASCADE,
354
354
  project TEXT NOT NULL,
355
+ module_path TEXT NOT NULL,
355
356
  full_name TEXT NOT NULL,
356
357
  timestamp TEXT NOT NULL,
357
358
  state TEXT NOT NULL CHECK (state IN ('passed', 'failed', 'skipped', 'pending')),
@@ -359,10 +360,10 @@ const migration = Effect.gen(function* () {
359
360
  flaky INTEGER CHECK (flaky IN (0, 1)),
360
361
  retry_count INTEGER DEFAULT 0,
361
362
  error_message TEXT,
362
- UNIQUE(project, full_name, timestamp)
363
+ UNIQUE(project, module_path, full_name, timestamp)
363
364
  )
364
365
  `;
365
- yield* sql`CREATE INDEX idx_test_history_lookup ON test_history(project, full_name)`;
366
+ yield* sql`CREATE INDEX idx_test_history_lookup ON test_history(project, module_path, full_name)`;
366
367
  yield* sql`CREATE INDEX idx_test_history_full_name ON test_history(full_name, timestamp)`;
367
368
  yield* sql`CREATE INDEX idx_test_history_run ON test_history(run_id)`;
368
369
  yield* sql`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitest-agent/sdk",
3
- "version": "1.2.0",
3
+ "version": "1.3.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": [
@@ -52,11 +52,11 @@
52
52
  "acorn": "^8.17.0",
53
53
  "acorn-typescript": "^1.4.13",
54
54
  "ajv": "^8.20.0",
55
- "config-file-effect": "^0.2.3",
55
+ "config-file-effect": "^0.3.0",
56
56
  "effect": "^3.21.4",
57
57
  "std-env": "^4.1.0",
58
- "workspaces-effect": "^1.2.0",
59
- "xdg-effect": "^2.0.1"
58
+ "workspaces-effect": "^1.3.0",
59
+ "xdg-effect": "^2.1.0"
60
60
  },
61
61
  "engines": {
62
62
  "node": ">=24.11.0"
@@ -14,6 +14,7 @@ const TestRun = Schema.Struct({
14
14
  * @public
15
15
  */
16
16
  const TestHistory = Schema.Struct({
17
+ modulePath: Schema.String,
17
18
  fullName: Schema.String,
18
19
  runs: Schema.Array(TestRun)
19
20
  }).annotations({ identifier: "TestHistory" });
@@ -1,8 +1,20 @@
1
1
  import { Context } from "effect";
2
2
 
3
3
  //#region src/services/HistoryTracker.ts
4
+ /**
5
+ * Builds the composite (modulePath, fullName) key used to key the internal
6
+ * testMap and the returned classifications Map, so identically-named tests
7
+ * in different files classify independently instead of colliding.
8
+ *
9
+ * Uses `JSON.stringify` for an injective encoding: a plain delimiter such as
10
+ * a space is not collision-proof (e.g. `("a b", "c")` and `("a", "b c")` would
11
+ * both yield `"a b c"`), which would reintroduce the very collision class this
12
+ * key exists to prevent.
13
+ * @public
14
+ */
15
+ const historyKey = (modulePath, fullName) => JSON.stringify([modulePath, fullName]);
4
16
  /** @public */
5
17
  var HistoryTracker = class extends Context.Tag("vitest-agent/HistoryTracker")() {};
6
18
 
7
19
  //#endregion
8
- export { HistoryTracker };
20
+ export { HistoryTracker, historyKey };
package/sql/assemblers.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { historyKey } from "../services/HistoryTracker.js";
2
+
1
3
  //#region src/sql/assemblers.ts
2
4
  /**
3
5
  * Builds a `CacheManifest` from an array of test_runs aggregation rows.
@@ -32,10 +34,13 @@ function assembleManifest(rows, dbPath) {
32
34
  };
33
35
  }
34
36
  /**
35
- * Groups test_history rows by `full_name` into a keyed record.
37
+ * Groups test_history rows by the composite (module_path, full_name) key
38
+ * into a keyed record.
36
39
  *
37
- * Returns `Record<fullName, { runs: TestRun[] }>` where `runs` are the
38
- * accumulated test runs for that test, in the order they appear in `rows`.
40
+ * Returns `Record<compositeKey, { modulePath, fullName, runs: TestRun[] }>`
41
+ * where `runs` are the accumulated test runs for that test, in the order
42
+ * they appear in `rows`. Two rows sharing `full_name` but differing
43
+ * `module_path` are tracked as distinct entries rather than merged.
39
44
  *
40
45
  * Only rows with state `"passed"` or `"failed"` are included in runs
41
46
  * (matching the `TestRun` schema constraint).
@@ -46,8 +51,13 @@ function assembleHistoryRecord(rows) {
46
51
  for (const row of rows) {
47
52
  const state = row.state === "passed" || row.state === "failed" ? row.state : null;
48
53
  if (state === null) continue;
49
- if (!record[row.full_name]) record[row.full_name] = { runs: [] };
50
- record[row.full_name].runs.push({
54
+ const key = historyKey(row.module_path, row.full_name);
55
+ if (!record[key]) record[key] = {
56
+ modulePath: row.module_path,
57
+ fullName: row.full_name,
58
+ runs: []
59
+ };
60
+ record[key].runs.push({
51
61
  timestamp: row.timestamp,
52
62
  state
53
63
  });
package/testing.d.ts CHANGED
@@ -742,7 +742,7 @@ declare const DataStore_base: Context.TagClass<DataStore, "vitest-agent/DataStor
742
742
  readonly writeTestCases: (moduleId: number, tests: ReadonlyArray<TestCaseInput>) => Effect.Effect<ReadonlyArray<number>, DataStoreError>;
743
743
  readonly writeErrors: (runId: number, errors: ReadonlyArray<TestErrorInput>) => Effect.Effect<void, DataStoreError>;
744
744
  readonly writeCoverage: (runId: number, coverage: ReadonlyArray<FileCoverageInput>) => Effect.Effect<void, DataStoreError>;
745
- readonly writeHistory: (project: string, fullName: string, runId: number, timestamp: string, state: string, duration: number | null, flaky: boolean, retryCount: number, errorMessage: string | null) => Effect.Effect<void, DataStoreError>;
745
+ readonly writeHistory: (project: string, fullName: string, modulePath: string, runId: number, timestamp: string, state: string, duration: number | null, flaky: boolean, retryCount: number, errorMessage: string | null) => Effect.Effect<void, DataStoreError>;
746
746
  readonly writeBaselines: (baselines: CoverageBaselines) => Effect.Effect<void, DataStoreError>;
747
747
  readonly writeTrends: (project: string, runId: number, entry: TrendEntry) => Effect.Effect<void, DataStoreError>;
748
748
  readonly writeSourceMap: (sourceFilePath: string, testModuleId: number, mappingType: string) => Effect.Effect<void, DataStoreError>;
@@ -1102,6 +1102,7 @@ declare const HistoryRecord: Schema.Struct<{
1102
1102
  project: typeof Schema.String;
1103
1103
  updatedAt: typeof Schema.String;
1104
1104
  tests: Schema.Array$<Schema.Struct<{
1105
+ modulePath: typeof Schema.String;
1105
1106
  fullName: typeof Schema.String;
1106
1107
  runs: Schema.Array$<Schema.Struct<{
1107
1108
  timestamp: typeof Schema.String;
@@ -1126,6 +1127,7 @@ interface ProjectRunSummary {
1126
1127
  /** @public */
1127
1128
  interface FlakyTest {
1128
1129
  readonly fullName: string;
1130
+ readonly modulePath: string;
1129
1131
  readonly project: string;
1130
1132
  readonly passCount: number;
1131
1133
  readonly failCount: number;
@@ -1135,6 +1137,7 @@ interface FlakyTest {
1135
1137
  /** @public */
1136
1138
  interface PersistentFailure {
1137
1139
  readonly fullName: string;
1140
+ readonly modulePath: string;
1138
1141
  readonly project: string;
1139
1142
  readonly consecutiveFailures: number;
1140
1143
  readonly firstFailedAt: string;
package/testing.js CHANGED
@@ -176,9 +176,9 @@ function flaky(filename) {
176
176
  state: "passed",
177
177
  duration: 300
178
178
  }]);
179
- yield* store.writeHistory("default", "async > resolves within timeout", runId1, "2025-12-31T23:00:00.000Z", "passed", 500, false, 0, null);
180
- yield* store.writeHistory("default", "async > resolves within timeout", runId1, "2026-01-01T00:00:00.000Z", "failed", 500, false, 0, null);
181
- yield* store.writeHistory("default", "async > rejects on error", runId1, "2026-01-01T00:00:00.000Z", "passed", 300, false, 0, null);
179
+ yield* store.writeHistory("default", "async > resolves within timeout", "src/async.test.ts", runId1, "2025-12-31T23:00:00.000Z", "passed", 500, false, 0, null);
180
+ yield* store.writeHistory("default", "async > resolves within timeout", "src/async.test.ts", runId1, "2026-01-01T00:00:00.000Z", "failed", 500, false, 0, null);
181
+ yield* store.writeHistory("default", "async > rejects on error", "src/async.test.ts", runId1, "2026-01-01T00:00:00.000Z", "passed", 300, false, 0, null);
182
182
  const runId2 = yield* store.writeRun({
183
183
  invocationId: "inv-preset-flaky-2",
184
184
  project: "default",
@@ -211,8 +211,8 @@ function flaky(filename) {
211
211
  state: "passed",
212
212
  duration: 300
213
213
  }]);
214
- yield* store.writeHistory("default", "async > resolves within timeout", runId2, "2026-01-01T01:00:00.000Z", "passed", 400, false, 0, null);
215
- yield* store.writeHistory("default", "async > rejects on error", runId2, "2026-01-01T01:00:00.000Z", "passed", 300, false, 0, null);
214
+ yield* store.writeHistory("default", "async > resolves within timeout", "src/async.test.ts", runId2, "2026-01-01T01:00:00.000Z", "passed", 400, false, 0, null);
215
+ yield* store.writeHistory("default", "async > rejects on error", "src/async.test.ts", runId2, "2026-01-01T01:00:00.000Z", "passed", 300, false, 0, null);
216
216
  });
217
217
  return Layer.effectDiscard(seed).pipe(Layer.provideMerge(base));
218
218
  }