@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 +29 -0
- package/index.js +2 -1
- package/layers/DataStoreLive.js +20 -4
- package/package.json +1 -1
- package/platform.js +3 -2
- package/utils/persist-transaction.js +85 -0
- package/version.js +1 -1
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 };
|
package/layers/DataStoreLive.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
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