@vitest-agent/engine 1.0.6 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.d.ts CHANGED
@@ -2648,6 +2648,35 @@ export declare const normalizeAssertionShape: (message: string) => string | null
2648
2648
  /** @public */
2649
2649
  export declare const computeFailureSignature: (input: FailureSignatureInput) => string;
2650
2650
  //#endregion
2651
+ //#region src/utils/persist-transaction.d.ts
2652
+ /**
2653
+ * Run `effect` as one SQLite write transaction on the `data.db` connection:
2654
+ * every `DataStore` write inside it commits together or not at all, and a
2655
+ * failure rolls the whole block back before the error propagates.
2656
+ *
2657
+ * Built for the reporter's run-end persist phase, where per-statement
2658
+ * autocommits (one fsync each) dominated wall time. Inside the block
2659
+ * `DataStoreLive` caches tag and file ids and defers `writeHistory`'s
2660
+ * keep-10 prune to one statement per run, executed before commit; the
2661
+ * persisted rows are the same as without the block.
2662
+ *
2663
+ * The fiber does not yield to the scheduler while the block runs. node:sqlite
2664
+ * is synchronous, so the whole transaction executes in one turn of the event
2665
+ * loop: a second connection in the same process (another project's reporter,
2666
+ * or the MCP server's own connection while it runs Vitest in-process) can
2667
+ * neither interleave a write nor block the thread in SQLite's busy handler
2668
+ * while this connection holds the write lock. Keep `effect` free of
2669
+ * genuinely asynchronous work for that reason; CPU-heavy work that needs no
2670
+ * database belongs before the block.
2671
+ *
2672
+ * Nested `withTransaction` calls inside `effect` become savepoints. A
2673
+ * `DataReader` query inside it runs on the transaction's connection.
2674
+ *
2675
+ * @param effect - the writes (and the reads they depend on) to run atomically
2676
+ * @public
2677
+ */
2678
+ export declare const withPersistTransaction: <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E | DataStoreError, R | SqlClient>;
2679
+ //#endregion
2651
2680
  //#region src/utils/resolve-data-path.d.ts
2652
2681
  /**
2653
2682
  * Filename of the SQLite database that stores all reporter data.
package/index.js CHANGED
@@ -4,6 +4,7 @@ import { DataReader } from "./services/DataReader.js";
4
4
  import { HistoryTracker, historyKey } from "./services/HistoryTracker.js";
5
5
  import { DataReaderLive } from "./layers/DataReaderLive.js";
6
6
  import { DataStore, INLINE_ATTACHMENT_BODY_CAP_BYTES } from "./services/DataStore.js";
7
+ import { withPersistTransaction } from "./utils/persist-transaction.js";
7
8
  import { DataStoreLive } from "./layers/DataStoreLive.js";
8
9
  import { DetailResolver } from "./services/DetailResolver.js";
9
10
  import { DetailResolverLive } from "./layers/DetailResolverLive.js";
@@ -56,4 +57,4 @@ import { DATABASE_FILENAME, resolveDataPath } from "./utils/resolve-data-path.js
56
57
  import { resolveWorkspaceKey } from "./utils/resolve-workspace-key.js";
57
58
  import { CURRENT_ENGINE_VERSION } from "./version.js";
58
59
 
59
- export { APP_NAMESPACE, AgentContext, CURRENT_ENGINE_VERSION, ConfigLive, DATABASE_FILENAME, DATA_DB_FILENAME, DATA_FALLBACK_DIR, DataReader, DataReaderLive, DataStore, DataStoreLive, DetailResolver, DetailResolverLive, DiscoveryRegistry, DiscoveryRegistryLive, EnvironmentDetector, EnvironmentDetectorLive, EnvironmentDetectorTest, ExecutorResolver, ExecutorResolverLive, HistoryTracker, HistoryTrackerLive, HistoryTrackerTest, IDEMPOTENCY_ROOT_SENTINEL, INLINE_ATTACHMENT_BODY_CAP_BYTES, LoggerLive, NodePlatformLayer, OutputPipelineLive, PROJECT_MIGRATIONS, PathResolutionLive, PerClientSessionMapReader, PerClientSessionMapReaderLive, PerClientSessionMapWriter, PerClientSessionMapWriterLive, PlatformLive, ProjectDiscovery, ProjectDiscoveryLive, ProjectDiscoveryTest, ProjectIdentity, ProjectIdentityLive, ProjectIdentityTest, REGISTRY_DB_FILENAME, REGISTRY_STORE_OPTIONS, RunContext, RunContext$, RunContextLive, RunContextService, RunContextTest, SESSIONS_DB_FILENAME, SidecarPlatformLive, VitestAgentConfigFile, _resetMigrationCacheForTesting, assembleHistoryRecord, assembleManifest, classifyEnvironment, computeFailureSignature, deriveIdempotencyKey, dispatchRecordTddArtifactEffect, endAgentEffect, ensureMigrated, formatTriageEffect, formatWrapupEffect, historyKey, makeSqliteStack, migration as migration0001, migration$1 as migration0002, normalizeAssertionShape, parseAndValidateTurnPayload, parseSessionEnvExports, recordRunWorkspaceChangesEffect, recordSessionEnd, recordSessionStart, recordTddArtifactByTaskIdEffect, recordTddArtifactEffect, recordTurnEffect, recoverSessionContextFromSessionEnv, registerAgentEffect, migration$2 as registryMigration0001, resolveDataPath, resolveHookPaths, resolveLogFile, resolveLogLevel, resolveProjectDir, resolveProjectIdentityFromCandidates, resolveProjectKeyFromCwd, resolveProjectKeyFromCwdEffect, resolveSessionForRecording, resolveSessionMapPath, resolveWorkspaceKey, migration$3 as sessionMapMigration0001 };
60
+ export { APP_NAMESPACE, AgentContext, CURRENT_ENGINE_VERSION, ConfigLive, DATABASE_FILENAME, DATA_DB_FILENAME, DATA_FALLBACK_DIR, DataReader, DataReaderLive, DataStore, DataStoreLive, DetailResolver, DetailResolverLive, DiscoveryRegistry, DiscoveryRegistryLive, EnvironmentDetector, EnvironmentDetectorLive, EnvironmentDetectorTest, ExecutorResolver, ExecutorResolverLive, HistoryTracker, HistoryTrackerLive, HistoryTrackerTest, IDEMPOTENCY_ROOT_SENTINEL, INLINE_ATTACHMENT_BODY_CAP_BYTES, LoggerLive, NodePlatformLayer, OutputPipelineLive, PROJECT_MIGRATIONS, PathResolutionLive, PerClientSessionMapReader, PerClientSessionMapReaderLive, PerClientSessionMapWriter, PerClientSessionMapWriterLive, PlatformLive, ProjectDiscovery, ProjectDiscoveryLive, ProjectDiscoveryTest, ProjectIdentity, ProjectIdentityLive, ProjectIdentityTest, REGISTRY_DB_FILENAME, REGISTRY_STORE_OPTIONS, RunContext, RunContext$, RunContextLive, RunContextService, RunContextTest, SESSIONS_DB_FILENAME, SidecarPlatformLive, VitestAgentConfigFile, _resetMigrationCacheForTesting, assembleHistoryRecord, assembleManifest, classifyEnvironment, computeFailureSignature, deriveIdempotencyKey, dispatchRecordTddArtifactEffect, endAgentEffect, ensureMigrated, formatTriageEffect, formatWrapupEffect, historyKey, makeSqliteStack, migration as migration0001, migration$1 as migration0002, normalizeAssertionShape, parseAndValidateTurnPayload, parseSessionEnvExports, recordRunWorkspaceChangesEffect, recordSessionEnd, recordSessionStart, recordTddArtifactByTaskIdEffect, recordTddArtifactEffect, recordTurnEffect, recoverSessionContextFromSessionEnv, registerAgentEffect, migration$2 as registryMigration0001, resolveDataPath, resolveHookPaths, resolveLogFile, resolveLogLevel, resolveProjectDir, resolveProjectIdentityFromCandidates, resolveProjectKeyFromCwd, resolveProjectKeyFromCwdEffect, resolveSessionForRecording, resolveSessionMapPath, resolveWorkspaceKey, migration$3 as sessionMapMigration0001, withPersistTransaction };
@@ -1,4 +1,5 @@
1
1
  import { DataStore, INLINE_ATTACHMENT_BODY_CAP_BYTES } from "../services/DataStore.js";
2
+ import { PersistBatch } from "../utils/persist-transaction.js";
2
3
  import { Agent, AgentNotFoundError, BehaviorNotFoundError, DataStoreError, GoalNotFoundError, IdempotencyHit, IllegalStatusTransitionError, RegistrationConflictError, TddTaskAlreadyEndedError, TddTaskNotFoundError, coerceErrorText, extractSqlReason } from "@vitest-agent/sdk";
3
4
  import { Effect, Layer, Option } from "effect";
4
5
  import { SqlClient } from "effect/sql/SqlClient";
@@ -37,8 +38,13 @@ const DataStoreLive = Layer.effect(DataStore, Effect.gen(function* () {
37
38
  yield* sql`PRAGMA foreign_keys=ON`.pipe(Effect.catch(() => Effect.void));
38
39
  const ensureFile = (filePath) => Effect.gen(function* () {
39
40
  yield* Effect.logDebug("ensureFile").pipe(Effect.annotateLogs({ filePath }));
41
+ const batch = yield* PersistBatch;
42
+ const cached = batch?.fileIds.get(filePath);
43
+ if (cached !== void 0) return cached;
40
44
  yield* sql`INSERT OR IGNORE INTO files (path) VALUES (${filePath})`;
41
- return (yield* sql`SELECT id FROM files WHERE path = ${filePath}`)[0].id;
45
+ const rows = yield* sql`SELECT id FROM files WHERE path = ${filePath}`;
46
+ batch?.fileIds.set(filePath, rows[0].id);
47
+ return rows[0].id;
42
48
  }).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => new DataStoreError({
43
49
  operation: "write",
44
50
  table: "files",
@@ -110,14 +116,19 @@ const DataStoreLive = Layer.effect(DataStore, Effect.gen(function* () {
110
116
  count: tests.length
111
117
  }));
112
118
  const ids = [];
119
+ const tagIds = (yield* PersistBatch)?.tagIds ?? /* @__PURE__ */ new Map();
113
120
  for (const tc of tests) {
114
121
  yield* sql`INSERT INTO test_cases (module_id, suite_id, vitest_id, name, full_name, state, classification, duration, start_time, flaky, slow, retry_count, repeat_count, heap, mode, each, fails, concurrent, shuffle, timeout, skip_note, location_line, location_column, created_turn_id) VALUES (${moduleId}, ${tc.suiteId ?? null}, ${tc.vitestId ?? null}, ${tc.name}, ${tc.fullName}, ${tc.state}, ${tc.classification ?? null}, ${tc.duration ?? null}, ${tc.startTime ?? null}, ${boolToInt(tc.flaky)}, ${boolToInt(tc.slow)}, ${tc.retryCount ?? 0}, ${tc.repeatCount ?? 0}, ${tc.heap ?? null}, ${tc.mode ?? null}, ${boolToInt(tc.each)}, ${boolToInt(tc.fails)}, ${boolToInt(tc.concurrent)}, ${boolToInt(tc.shuffle)}, ${tc.timeout ?? null}, ${tc.skipNote ?? null}, ${tc.locationLine ?? null}, ${tc.locationColumn ?? null}, ${tc.createdTurnId ?? null})`;
115
122
  const testCaseId = (yield* sql`SELECT last_insert_rowid() as id`)[0].id;
116
123
  ids.push(testCaseId);
117
124
  if (tc.tags && tc.tags.length > 0) for (const tag of tc.tags) {
118
- yield* sql`INSERT OR IGNORE INTO tags (name) VALUES (${tag})`;
119
- const tagRows = yield* sql`SELECT id FROM tags WHERE name = ${tag}`;
120
- if (tagRows.length > 0) yield* sql`INSERT OR IGNORE INTO test_case_tags (test_case_id, tag_id) VALUES (${testCaseId}, ${tagRows[0].id})`;
125
+ let tagId = tagIds.get(tag);
126
+ if (tagId === void 0) {
127
+ yield* sql`INSERT OR IGNORE INTO tags (name) VALUES (${tag})`;
128
+ tagId = (yield* sql`SELECT id FROM tags WHERE name = ${tag}`)[0]?.id;
129
+ if (tagId !== void 0) tagIds.set(tag, tagId);
130
+ }
131
+ if (tagId !== void 0) yield* sql`INSERT OR IGNORE INTO test_case_tags (test_case_id, tag_id) VALUES (${testCaseId}, ${tagId})`;
121
132
  }
122
133
  }
123
134
  return ids;
@@ -224,6 +235,11 @@ const DataStoreLive = Layer.effect(DataStore, Effect.gen(function* () {
224
235
  runId
225
236
  }));
226
237
  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`;
238
+ const batch = yield* PersistBatch;
239
+ if (batch !== void 0) {
240
+ batch.historyRunIds.add(runId);
241
+ return;
242
+ }
227
243
  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}`;
228
244
  }).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => new DataStoreError({
229
245
  operation: "write",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitest-agent/engine",
3
- "version": "1.0.6",
3
+ "version": "1.1.0",
4
4
  "private": false,
5
5
  "description": "Shared services, layers, SQLite data layer, migrations and platform resolution for the vitest-agent CLI and MCP server.",
6
6
  "keywords": [
package/platform.js CHANGED
@@ -6,7 +6,8 @@ import { OutputPipelineLive } from "./layers/OutputPipelineLive.js";
6
6
  import { ProjectDiscoveryLive } from "./layers/ProjectDiscoveryLive.js";
7
7
  import { PROJECT_MIGRATIONS } from "./migrations/index.js";
8
8
  import { LEDGER_OPTIONS, toStoreMigrations } from "./stores.js";
9
- import { Effect, Layer } from "effect";
9
+ import { Context, Effect, Layer } from "effect";
10
+ import { SqlClient } from "effect/sql/SqlClient";
10
11
  import * as NodeServices from "@effect/platform-node/NodeServices";
11
12
  import { layer } from "@effect/sql-sqlite-node/SqliteClient";
12
13
  import { Store } from "@effected/store";
@@ -31,7 +32,7 @@ const NodePlatformLayer = NodeServices.layer;
31
32
  * @public
32
33
  */
33
34
  const makeSqliteStack = (filename, migrations = PROJECT_MIGRATIONS) => {
34
- const SqliteLayer = layer({ filename });
35
+ const SqliteLayer = layer({ filename }).pipe(Layer.tap((context) => Context.get(context, SqlClient)`PRAGMA synchronous = NORMAL`));
35
36
  return {
36
37
  SqliteLayer,
37
38
  MigratorLayer: Layer.effectDiscard(Effect.void).pipe(Layer.provide(Store.layer({
@@ -0,0 +1,85 @@
1
+ import { DataStoreError, extractSqlReason } from "@vitest-agent/sdk";
2
+ import { Context, Effect, References } from "effect";
3
+ import { SqlClient } from "effect/sql/SqlClient";
4
+ import { isSqlError } from "effect/sql/SqlError";
5
+
6
+ //#region src/utils/persist-transaction.ts
7
+ /**
8
+ * The open persist batch, or `undefined` outside {@link withPersistTransaction}.
9
+ * Read by `DataStoreLive`; not part of the public surface.
10
+ *
11
+ * @internal
12
+ */
13
+ const PersistBatch = Context.Reference("@vitest-agent/engine/PersistBatch", { defaultValue: () => void 0 });
14
+ /** Matches the per-write window `DataStoreLive.writeHistory` keeps outside a batch. */
15
+ const HISTORY_WINDOW = 10;
16
+ /**
17
+ * Keep the newest {@link HISTORY_WINDOW} rows of every
18
+ * `(project, module_path, full_name)` series that has a row from `runId`,
19
+ * in one statement. The same rows the per-write prune deletes: a series is
20
+ * unique on `(project, module_path, full_name, timestamp)`, so there are no
21
+ * timestamp ties for `ROW_NUMBER` to break arbitrarily, and every series
22
+ * this run wrote carries `run_id = runId` (the upsert overwrites it).
23
+ */
24
+ const pruneHistoryForRun = (sql, runId) => sql`DELETE FROM test_history WHERE id IN (
25
+ SELECT id FROM (
26
+ SELECT h.id, ROW_NUMBER() OVER (
27
+ PARTITION BY h.project, h.module_path, h.full_name
28
+ ORDER BY h.timestamp DESC
29
+ ) AS rn
30
+ FROM test_history h
31
+ JOIN (
32
+ SELECT DISTINCT project, module_path, full_name FROM test_history WHERE run_id = ${runId}
33
+ ) a ON a.project = h.project AND a.module_path = h.module_path AND a.full_name = h.full_name
34
+ ) WHERE rn > ${HISTORY_WINDOW}
35
+ )`;
36
+ /**
37
+ * Run `effect` as one SQLite write transaction on the `data.db` connection:
38
+ * every `DataStore` write inside it commits together or not at all, and a
39
+ * failure rolls the whole block back before the error propagates.
40
+ *
41
+ * Built for the reporter's run-end persist phase, where per-statement
42
+ * autocommits (one fsync each) dominated wall time. Inside the block
43
+ * `DataStoreLive` caches tag and file ids and defers `writeHistory`'s
44
+ * keep-10 prune to one statement per run, executed before commit; the
45
+ * persisted rows are the same as without the block.
46
+ *
47
+ * The fiber does not yield to the scheduler while the block runs. node:sqlite
48
+ * is synchronous, so the whole transaction executes in one turn of the event
49
+ * loop: a second connection in the same process (another project's reporter,
50
+ * or the MCP server's own connection while it runs Vitest in-process) can
51
+ * neither interleave a write nor block the thread in SQLite's busy handler
52
+ * while this connection holds the write lock. Keep `effect` free of
53
+ * genuinely asynchronous work for that reason; CPU-heavy work that needs no
54
+ * database belongs before the block.
55
+ *
56
+ * Nested `withTransaction` calls inside `effect` become savepoints. A
57
+ * `DataReader` query inside it runs on the transaction's connection.
58
+ *
59
+ * @param effect - the writes (and the reads they depend on) to run atomically
60
+ * @public
61
+ */
62
+ const withPersistTransaction = (effect) => Effect.gen(function* () {
63
+ const sql = yield* SqlClient;
64
+ const batch = {
65
+ historyRunIds: /* @__PURE__ */ new Set(),
66
+ tagIds: /* @__PURE__ */ new Map(),
67
+ fileIds: /* @__PURE__ */ new Map()
68
+ };
69
+ return yield* sql.withTransaction(Effect.gen(function* () {
70
+ const result = yield* effect.pipe(Effect.provideService(PersistBatch, batch));
71
+ for (const runId of batch.historyRunIds) yield* pruneHistoryForRun(sql, runId).pipe(Effect.mapError((e) => new DataStoreError({
72
+ operation: "write",
73
+ table: "test_history",
74
+ reason: extractSqlReason(e)
75
+ })));
76
+ return result;
77
+ }));
78
+ }).pipe(Effect.provideService(References.PreventSchedulerYield, true), Effect.catchIf(isSqlError, (e) => Effect.fail(new DataStoreError({
79
+ operation: "write",
80
+ table: "transaction",
81
+ reason: extractSqlReason(e)
82
+ }))));
83
+
84
+ //#endregion
85
+ export { PersistBatch, withPersistTransaction };
package/version.js CHANGED
@@ -1,6 +1,6 @@
1
1
  //#region src/version.ts
2
2
  /** Build-time injected package version (see architecture.md D36). @public */
3
- const CURRENT_ENGINE_VERSION = "1.0.6";
3
+ const CURRENT_ENGINE_VERSION = "1.1.0";
4
4
 
5
5
  //#endregion
6
6
  export { CURRENT_ENGINE_VERSION };