@vitest-agent/engine 1.1.2 → 1.2.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 +61 -3
- package/index.js +2 -2
- package/layers/DataReaderLive.js +27 -3
- package/layers/DataStoreLive.js +85 -7
- package/migrations/0003_run_retention.js +66 -0
- package/migrations/index.js +3 -1
- package/package.json +2 -2
- package/testing.d.ts +31 -2
- package/testing.js +2 -2
- package/utils/persist-transaction.js +58 -4
- package/version.js +1 -1
package/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Context, Effect, FileSystem, Layer, LogLevel, Option, Path, Schema } from "effect";
|
|
2
2
|
import { ConfigFileShape } from "@effected/config-file";
|
|
3
|
-
import { Agent, AgentNotFoundError, AgentReport, ArtifactKind, ArtifactKind as ArtifactKind$1, ArtifactSuite, ArtifactSuite as ArtifactSuite$1, BehaviorDetail, BehaviorNotFoundError, BehaviorRow, BehaviorStatus, CacheManifest, CitedArtifact, CoverageBaselines, CoverageReport, DataStoreError, DetailLevel, DiscoveryError, Environment, Executor, FileCoverageReport, GoalDetail, GoalNotFoundError, GoalRow, GoalStatus, HistoryRecord, IdempotencyHit, IllegalStatusTransitionError, Phase, ProjectIdentityNotResolvableError, RegistrationConflictError, ResolvedThresholds, TddTaskAlreadyEndedError, TddTaskNotFoundError, TestClassification, TestRun, TrendEntry, TrendRecord, TurnPayload, VitestAgentConfig } from "@vitest-agent/sdk";
|
|
3
|
+
import { Agent, AgentNotFoundError, AgentReport, ArtifactKind, ArtifactKind as ArtifactKind$1, ArtifactSuite, ArtifactSuite as ArtifactSuite$1, BehaviorDetail, BehaviorNotFoundError, BehaviorRow, BehaviorStatus, CacheManifest, CitedArtifact, CoverageBaselines, CoverageReport, DataStoreError, DetailLevel, DiscoveryError, Environment, Executor, FileCoverageReport, GoalDetail, GoalNotFoundError, GoalRow, GoalStatus, HistoryRecord, IdempotencyHit, IllegalStatusTransitionError, Phase, ProjectIdentityNotResolvableError, RegistrationConflictError, ResolvedThresholds, RunRetentionPolicy, TddTaskAlreadyEndedError, TddTaskNotFoundError, TestClassification, TestRun, TrendEntry, TrendRecord, TurnPayload, VitestAgentConfig } from "@vitest-agent/sdk";
|
|
4
4
|
import { SqlClient } from "effect/sql/SqlClient";
|
|
5
5
|
import { RuntimeEnv } from "@effected/env";
|
|
6
6
|
import { WorkspaceDiscovery, WorkspaceDiscoveryFailure, WorkspaceRootNotFoundError } from "@effected/workspaces";
|
|
@@ -457,6 +457,15 @@ export interface RegisterAgentInput {
|
|
|
457
457
|
*/
|
|
458
458
|
readonly agentId?: string;
|
|
459
459
|
}
|
|
460
|
+
/**
|
|
461
|
+
* What `DataStore.pruneRuns` removed: `test_runs` rows deleted (their
|
|
462
|
+
* cascaded rows are not counted) and orphaned `files` rows garbage-collected.
|
|
463
|
+
* @public
|
|
464
|
+
*/
|
|
465
|
+
export interface PruneRunsResult {
|
|
466
|
+
readonly prunedRuns: number;
|
|
467
|
+
readonly prunedFiles: number;
|
|
468
|
+
}
|
|
460
469
|
/** @public */
|
|
461
470
|
export interface AssociateRunSessionInput {
|
|
462
471
|
readonly chatId: string;
|
|
@@ -554,6 +563,25 @@ declare const DataStore_base: Context.ServiceClass<DataStore, "vitest-agent/Data
|
|
|
554
563
|
readonly affectedSessions: number;
|
|
555
564
|
readonly prunedTurns: number;
|
|
556
565
|
}, DataStoreError>;
|
|
566
|
+
/**
|
|
567
|
+
* Apply a run-retention policy: delete every `test_runs` row beyond the
|
|
568
|
+
* newest `policy.keepRuns` of its project, and any run older than
|
|
569
|
+
* `policy.maxAgeDays` days when set, letting the schema's cascades drop
|
|
570
|
+
* the dependent rows (modules, suites, cases, tags, errors, frames,
|
|
571
|
+
* coverage, trends, triggers), then garbage-collect `files` rows no
|
|
572
|
+
* table references any more. `test_history` rows survive with a NULL
|
|
573
|
+
* `run_id`; they keep their own newest-10-per-series window. Before
|
|
574
|
+
* the delete, `file_coverage` rows held by a doomed run are moved onto
|
|
575
|
+
* the lowest surviving run of the same invocation (coverage is written
|
|
576
|
+
* once per invocation, issue 622) unless a survivor already carries
|
|
577
|
+
* coverage rows, so every sibling project's coverage read stays whole.
|
|
578
|
+
*
|
|
579
|
+
* A handful of synchronous statements on the open connection, no
|
|
580
|
+
* yielding, so it may run inside `withPersistTransaction` after the
|
|
581
|
+
* run's own rows are written: the run just written is the newest of
|
|
582
|
+
* its project and is never pruned.
|
|
583
|
+
*/
|
|
584
|
+
readonly pruneRuns: (policy: RunRetentionPolicy) => Effect.Effect<PruneRunsResult, DataStoreError>;
|
|
557
585
|
readonly associateLatestRunWithSession: (input: AssociateRunSessionInput) => Effect.Effect<void, DataStoreError>;
|
|
558
586
|
readonly backfillTestCaseTurns: (chatId: string) => Effect.Effect<number, DataStoreError>;
|
|
559
587
|
/**
|
|
@@ -1688,6 +1716,7 @@ declare const migration$1: Effect.Effect<void, import("effect/sql/SqlError").Sql
|
|
|
1688
1716
|
export declare const PROJECT_MIGRATIONS: {
|
|
1689
1717
|
readonly "0001_initial": import("effect/Effect").Effect<void, import("effect/sql/SqlError").SqlError, import("effect/sql/SqlClient").SqlClient>;
|
|
1690
1718
|
readonly "0002_test_artifacts": import("effect/Effect").Effect<void, import("effect/sql/SqlError").SqlError, import("effect/sql/SqlClient").SqlClient>;
|
|
1719
|
+
readonly "0003_run_retention": import("effect/Effect").Effect<void, import("effect/sql/SqlError").SqlError, import("effect/sql/SqlClient").SqlClient>;
|
|
1691
1720
|
};
|
|
1692
1721
|
/**
|
|
1693
1722
|
* Migration for the global discovery registry SQLite at
|
|
@@ -2540,6 +2569,30 @@ export interface FailureSignatureInput {
|
|
|
2540
2569
|
export declare const normalizeAssertionShape: (message: string) => string | null;
|
|
2541
2570
|
/** @public */
|
|
2542
2571
|
export declare const computeFailureSignature: (input: FailureSignatureInput) => string;
|
|
2572
|
+
/**
|
|
2573
|
+
* Run `effect` as a nested transaction that keeps the
|
|
2574
|
+
* {@link withPersistTransaction} caches honest.
|
|
2575
|
+
*
|
|
2576
|
+
* Outside a persist batch this is exactly `sql.withTransaction(effect)`.
|
|
2577
|
+
* Inside one, the nested `withTransaction` becomes a SAVEPOINT, and when it
|
|
2578
|
+
* rolls back (a failure, a defect, an interrupt) every `files` / `tags` row
|
|
2579
|
+
* the savepoint inserted is gone while the ids `DataStoreLive` cached for
|
|
2580
|
+
* them would survive in `PersistBatchState`. This helper snapshots the
|
|
2581
|
+
* cache sizes before the savepoint opens and discards the entries added
|
|
2582
|
+
* since whenever it does not commit, so a later write in the same batch
|
|
2583
|
+
* re-inserts the row instead of referencing one that no longer exists
|
|
2584
|
+
* (issue #628: a FK failure or a dangling id). A savepoint that commits
|
|
2585
|
+
* keeps its entries.
|
|
2586
|
+
*
|
|
2587
|
+
* Use this, never a bare `sql.withTransaction`, for any nested transaction
|
|
2588
|
+
* whose failure a caller may catch and continue from inside the batch; a
|
|
2589
|
+
* bare one that rolls back leaves the caches stale. The bookkeeping is
|
|
2590
|
+
* synchronous, so the batch's no-yield guarantee is unchanged.
|
|
2591
|
+
*
|
|
2592
|
+
* @param effect - the writes to run as one savepoint (or transaction)
|
|
2593
|
+
* @public
|
|
2594
|
+
*/
|
|
2595
|
+
export declare const withPersistSavepoint: <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E | SqlError, R | SqlClient>;
|
|
2543
2596
|
/**
|
|
2544
2597
|
* Run `effect` as one SQLite write transaction on the `data.db` connection:
|
|
2545
2598
|
* every `DataStore` write inside it commits together or not at all, and a
|
|
@@ -2560,8 +2613,13 @@ export declare const computeFailureSignature: (input: FailureSignatureInput) =>
|
|
|
2560
2613
|
* genuinely asynchronous work for that reason; CPU-heavy work that needs no
|
|
2561
2614
|
* database belongs before the block.
|
|
2562
2615
|
*
|
|
2563
|
-
* Nested `withTransaction` calls inside `effect` become savepoints.
|
|
2564
|
-
*
|
|
2616
|
+
* Nested `withTransaction` calls inside `effect` become savepoints. Open
|
|
2617
|
+
* them through {@link withPersistSavepoint}: a savepoint that rolls back
|
|
2618
|
+
* also discards the tag / file ids cached while it was open, so a write the
|
|
2619
|
+
* caller recovers from cannot leave a later write pointing at a row the
|
|
2620
|
+
* rollback removed. A bare nested `sql.withTransaction` whose failure is
|
|
2621
|
+
* caught inside the block leaves those caches stale. A `DataReader` query
|
|
2622
|
+
* inside the block runs on the transaction's connection.
|
|
2565
2623
|
*
|
|
2566
2624
|
* @param effect - the writes (and the reads they depend on) to run atomically
|
|
2567
2625
|
* @public
|
package/index.js
CHANGED
|
@@ -4,7 +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
|
+
import { withPersistSavepoint, withPersistTransaction } from "./utils/persist-transaction.js";
|
|
8
8
|
import { DataStoreLive } from "./layers/DataStoreLive.js";
|
|
9
9
|
import { DetailResolver } from "./services/DetailResolver.js";
|
|
10
10
|
import { DetailResolverLive } from "./layers/DetailResolverLive.js";
|
|
@@ -57,4 +57,4 @@ import { DATABASE_FILENAME, resolveDataPath } from "./utils/resolve-data-path.js
|
|
|
57
57
|
import { resolveWorkspaceKey } from "./utils/resolve-workspace-key.js";
|
|
58
58
|
import { CURRENT_ENGINE_VERSION } from "./version.js";
|
|
59
59
|
|
|
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 };
|
|
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, withPersistSavepoint, withPersistTransaction };
|
package/layers/DataReaderLive.js
CHANGED
|
@@ -8,6 +8,28 @@ import { SqlClient } from "effect/sql/SqlClient";
|
|
|
8
8
|
/** @public */
|
|
9
9
|
const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
|
|
10
10
|
const sql = yield* SqlClient;
|
|
11
|
+
/**
|
|
12
|
+
* The run whose `file_coverage` rows describe `runId`'s coverage.
|
|
13
|
+
*
|
|
14
|
+
* The reporter writes per-file coverage ONCE per invocation, under
|
|
15
|
+
* one run of that invocation (issue #622), while every project gets
|
|
16
|
+
* its own `test_runs` row. A project run's rows therefore live under
|
|
17
|
+
* the sibling run sharing its `invocation_id`. Preference order: the
|
|
18
|
+
* run itself when it carries rows (a database an older reporter wrote
|
|
19
|
+
* holds an identical copy under every run of the invocation — reading
|
|
20
|
+
* exactly one run keeps that layout deduplicated), else the lowest
|
|
21
|
+
* sibling run id that carries rows. A run whose invocation wrote no
|
|
22
|
+
* rows resolves to itself, so the caller's `WHERE run_id = ?` is
|
|
23
|
+
* simply empty. Never crosses an invocation boundary.
|
|
24
|
+
*/
|
|
25
|
+
const resolveCoverageRunId = (runId) => Effect.gen(function* () {
|
|
26
|
+
return (yield* sql`SELECT fc.run_id
|
|
27
|
+
FROM file_coverage fc
|
|
28
|
+
JOIN test_runs tr ON tr.id = fc.run_id
|
|
29
|
+
WHERE tr.invocation_id = (SELECT invocation_id FROM test_runs WHERE id = ${runId})
|
|
30
|
+
ORDER BY CASE WHEN fc.run_id = ${runId} THEN 0 ELSE 1 END, fc.run_id ASC
|
|
31
|
+
LIMIT 1`)[0]?.run_id ?? runId;
|
|
32
|
+
});
|
|
11
33
|
const getLatestRun = (project) => Effect.gen(function* () {
|
|
12
34
|
yield* Effect.logDebug("getLatestRun").pipe(Effect.annotateLogs({ project }));
|
|
13
35
|
const runs = yield* sql`SELECT id, timestamp, reason, duration, total, passed, failed, skipped, project
|
|
@@ -80,11 +102,12 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
|
|
|
80
102
|
WHERE run_id = ${run.id} AND scope = 'unhandled'`;
|
|
81
103
|
const failedFiles = failedModules.map((m) => m.file_path);
|
|
82
104
|
const projectName = run.project;
|
|
105
|
+
const coverageRunId = yield* resolveCoverageRunId(run.id);
|
|
83
106
|
const fileCovRows = yield* sql`SELECT f.path AS file_path, fc.statements, fc.branches, fc.functions, fc.lines,
|
|
84
107
|
fc.uncovered_lines, fc.tier
|
|
85
108
|
FROM file_coverage fc
|
|
86
109
|
JOIN files f ON f.id = fc.file_id
|
|
87
|
-
WHERE fc.run_id = ${
|
|
110
|
+
WHERE fc.run_id = ${coverageRunId}`;
|
|
88
111
|
const lowCoverage = fileCovRows.filter((r) => r.tier === "below_threshold").map((r) => ({
|
|
89
112
|
file: r.file_path,
|
|
90
113
|
summary: {
|
|
@@ -440,10 +463,11 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
|
|
|
440
463
|
})));
|
|
441
464
|
const getFileCoverage = (runId) => Effect.gen(function* () {
|
|
442
465
|
yield* Effect.logDebug("getFileCoverage").pipe(Effect.annotateLogs({ runId }));
|
|
466
|
+
const coverageRunId = yield* resolveCoverageRunId(runId);
|
|
443
467
|
return (yield* sql`SELECT f.path as file_path, fc.statements, fc.branches, fc.functions, fc.lines, fc.uncovered_lines
|
|
444
468
|
FROM file_coverage fc
|
|
445
469
|
JOIN files f ON f.id = fc.file_id
|
|
446
|
-
WHERE fc.run_id = ${
|
|
470
|
+
WHERE fc.run_id = ${coverageRunId}`).map((r) => ({
|
|
447
471
|
file: r.file_path,
|
|
448
472
|
summary: {
|
|
449
473
|
statements: r.statements,
|
|
@@ -464,7 +488,7 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
|
|
|
464
488
|
WHERE project = ${project}
|
|
465
489
|
ORDER BY timestamp DESC LIMIT 1`;
|
|
466
490
|
if (runs.length === 0) return Option.none();
|
|
467
|
-
const runId = runs[0].id;
|
|
491
|
+
const runId = yield* resolveCoverageRunId(runs[0].id);
|
|
468
492
|
const fileCoverageRows = yield* sql`SELECT f.path as file_path, fc.statements, fc.branches, fc.functions, fc.lines, fc.uncovered_lines, fc.tier
|
|
469
493
|
FROM file_coverage fc JOIN files f ON f.id = fc.file_id
|
|
470
494
|
WHERE fc.run_id = ${runId}`;
|
package/layers/DataStoreLive.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { DataStore, INLINE_ATTACHMENT_BODY_CAP_BYTES } from "../services/DataStore.js";
|
|
2
|
-
import { PersistBatch } from "../utils/persist-transaction.js";
|
|
2
|
+
import { PersistBatch, withPersistSavepoint } from "../utils/persist-transaction.js";
|
|
3
3
|
import { Agent, AgentNotFoundError, BehaviorNotFoundError, DataStoreError, GoalNotFoundError, IdempotencyHit, IllegalStatusTransitionError, RegistrationConflictError, TddTaskAlreadyEndedError, TddTaskNotFoundError, coerceErrorText, extractSqlReason } from "@vitest-agent/sdk";
|
|
4
|
-
import { Effect, Layer, Option } from "effect";
|
|
4
|
+
import { Clock, Effect, Layer, Option } from "effect";
|
|
5
5
|
import { SqlClient } from "effect/sql/SqlClient";
|
|
6
6
|
import { Buffer } from "node:buffer";
|
|
7
7
|
import { randomUUID } from "node:crypto";
|
|
@@ -36,6 +36,7 @@ const boolToInt = (v) => v === void 0 ? null : v ? 1 : 0;
|
|
|
36
36
|
const DataStoreLive = Layer.effect(DataStore, Effect.gen(function* () {
|
|
37
37
|
const sql = yield* SqlClient;
|
|
38
38
|
yield* sql`PRAGMA foreign_keys=ON`.pipe(Effect.catch(() => Effect.void));
|
|
39
|
+
const savepoint = (effect) => withPersistSavepoint(effect).pipe(Effect.provideService(SqlClient, sql));
|
|
39
40
|
const ensureFile = (filePath) => Effect.gen(function* () {
|
|
40
41
|
yield* Effect.logDebug("ensureFile").pipe(Effect.annotateLogs({ filePath }));
|
|
41
42
|
const batch = yield* PersistBatch;
|
|
@@ -275,7 +276,7 @@ const DataStoreLive = Layer.effect(DataStore, Effect.gen(function* () {
|
|
|
275
276
|
* enforced Vitest thresholds or the aspirational coverageTargets
|
|
276
277
|
* (issue #237).
|
|
277
278
|
*/
|
|
278
|
-
const writeCoveragePolicy = (kind, policy) =>
|
|
279
|
+
const writeCoveragePolicy = (kind, policy) => savepoint(Effect.gen(function* () {
|
|
279
280
|
yield* Effect.logDebug("writeCoveragePolicy").pipe(Effect.annotateLogs({ kind }));
|
|
280
281
|
const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
281
282
|
const { global: g, patterns = [] } = policy;
|
|
@@ -427,7 +428,7 @@ const DataStoreLive = Layer.effect(DataStore, Effect.gen(function* () {
|
|
|
427
428
|
table: "sessions",
|
|
428
429
|
reason: extractSqlReason(e)
|
|
429
430
|
})));
|
|
430
|
-
const writeTurn = (input) =>
|
|
431
|
+
const writeTurn = (input) => savepoint(Effect.gen(function* () {
|
|
431
432
|
yield* Effect.logDebug("writeTurn").pipe(Effect.annotateLogs({
|
|
432
433
|
sessionId: input.sessionId,
|
|
433
434
|
type: input.type
|
|
@@ -533,7 +534,7 @@ const DataStoreLive = Layer.effect(DataStore, Effect.gen(function* () {
|
|
|
533
534
|
table: "hypotheses",
|
|
534
535
|
reason: extractSqlReason(e)
|
|
535
536
|
})));
|
|
536
|
-
const writeTddTask = (input) =>
|
|
537
|
+
const writeTddTask = (input) => savepoint(Effect.gen(function* () {
|
|
537
538
|
yield* Effect.logDebug("writeTddTask").pipe(Effect.annotateLogs({
|
|
538
539
|
sessionId: input.sessionId,
|
|
539
540
|
goal: input.goal
|
|
@@ -797,7 +798,7 @@ const DataStoreLive = Layer.effect(DataStore, Effect.gen(function* () {
|
|
|
797
798
|
reason: extractSqlReason(e)
|
|
798
799
|
})));
|
|
799
800
|
});
|
|
800
|
-
const createBehavior = (input) =>
|
|
801
|
+
const createBehavior = (input) => savepoint(Effect.gen(function* () {
|
|
801
802
|
yield* Effect.logDebug("createBehavior").pipe(Effect.annotateLogs({
|
|
802
803
|
goalId: input.goalId,
|
|
803
804
|
behavior: input.behavior
|
|
@@ -836,7 +837,7 @@ const DataStoreLive = Layer.effect(DataStore, Effect.gen(function* () {
|
|
|
836
837
|
table: "tdd_session_behaviors",
|
|
837
838
|
reason: extractSqlReason(e)
|
|
838
839
|
})));
|
|
839
|
-
const updateBehavior = (input) =>
|
|
840
|
+
const updateBehavior = (input) => savepoint(Effect.gen(function* () {
|
|
840
841
|
yield* Effect.logDebug("updateBehavior").pipe(Effect.annotateLogs({ id: input.id }));
|
|
841
842
|
const existing = yield* sql`
|
|
842
843
|
SELECT id, goal_id, ordinal, behavior, suggested_test_name, status, created_at
|
|
@@ -1113,6 +1114,82 @@ const DataStoreLive = Layer.effect(DataStore, Effect.gen(function* () {
|
|
|
1113
1114
|
table: "turns",
|
|
1114
1115
|
reason: extractSqlReason(e)
|
|
1115
1116
|
})));
|
|
1117
|
+
const pruneRuns = (policy) => Effect.gen(function* () {
|
|
1118
|
+
yield* Effect.logDebug("pruneRuns").pipe(Effect.annotateLogs({
|
|
1119
|
+
keepRuns: policy.keepRuns,
|
|
1120
|
+
maxAgeDays: policy.maxAgeDays ?? null
|
|
1121
|
+
}));
|
|
1122
|
+
const now = yield* Clock.currentTimeMillis;
|
|
1123
|
+
const cutoff = policy.maxAgeDays === void 0 ? null : (/* @__PURE__ */ new Date(now - policy.maxAgeDays * 24 * 60 * 60 * 1e3)).toISOString();
|
|
1124
|
+
yield* sql`CREATE TEMP TABLE IF NOT EXISTS prune_doomed_runs (id INTEGER PRIMARY KEY)`;
|
|
1125
|
+
yield* sql`DELETE FROM prune_doomed_runs`;
|
|
1126
|
+
yield* sql`
|
|
1127
|
+
INSERT INTO prune_doomed_runs (id)
|
|
1128
|
+
SELECT id FROM (
|
|
1129
|
+
SELECT id, timestamp,
|
|
1130
|
+
ROW_NUMBER() OVER (PARTITION BY project ORDER BY timestamp DESC, id DESC) AS rn
|
|
1131
|
+
FROM test_runs
|
|
1132
|
+
)
|
|
1133
|
+
WHERE rn > ${policy.keepRuns} OR (${cutoff} IS NOT NULL AND timestamp < ${cutoff})`;
|
|
1134
|
+
yield* sql`
|
|
1135
|
+
WITH sources AS (
|
|
1136
|
+
SELECT tr.invocation_id, MIN(tr.id) AS from_run_id
|
|
1137
|
+
FROM test_runs tr
|
|
1138
|
+
JOIN prune_doomed_runs d ON d.id = tr.id
|
|
1139
|
+
WHERE EXISTS (SELECT 1 FROM file_coverage fc WHERE fc.run_id = tr.id)
|
|
1140
|
+
GROUP BY tr.invocation_id
|
|
1141
|
+
),
|
|
1142
|
+
moves AS (
|
|
1143
|
+
SELECT s.from_run_id,
|
|
1144
|
+
(SELECT MIN(tr2.id) FROM test_runs tr2
|
|
1145
|
+
WHERE tr2.invocation_id = s.invocation_id
|
|
1146
|
+
AND tr2.id NOT IN (SELECT id FROM prune_doomed_runs)) AS to_run_id
|
|
1147
|
+
FROM sources s
|
|
1148
|
+
WHERE NOT EXISTS (
|
|
1149
|
+
SELECT 1 FROM file_coverage fc2
|
|
1150
|
+
JOIN test_runs tr3 ON tr3.id = fc2.run_id
|
|
1151
|
+
WHERE tr3.invocation_id = s.invocation_id
|
|
1152
|
+
AND tr3.id NOT IN (SELECT id FROM prune_doomed_runs)
|
|
1153
|
+
)
|
|
1154
|
+
)
|
|
1155
|
+
UPDATE file_coverage
|
|
1156
|
+
SET run_id = (SELECT to_run_id FROM moves WHERE moves.from_run_id = file_coverage.run_id)
|
|
1157
|
+
WHERE run_id IN (SELECT from_run_id FROM moves WHERE to_run_id IS NOT NULL)`;
|
|
1158
|
+
const deletedRuns = yield* sql`
|
|
1159
|
+
DELETE FROM test_runs WHERE id IN (SELECT id FROM prune_doomed_runs) RETURNING id`;
|
|
1160
|
+
yield* sql`DELETE FROM prune_doomed_runs`;
|
|
1161
|
+
if (deletedRuns.length === 0) return {
|
|
1162
|
+
prunedRuns: 0,
|
|
1163
|
+
prunedFiles: 0
|
|
1164
|
+
};
|
|
1165
|
+
const deletedFiles = yield* sql`
|
|
1166
|
+
DELETE FROM files WHERE
|
|
1167
|
+
NOT EXISTS (SELECT 1 FROM test_modules WHERE file_id = files.id)
|
|
1168
|
+
AND NOT EXISTS (SELECT 1 FROM scoped_files WHERE file_id = files.id)
|
|
1169
|
+
AND NOT EXISTS (SELECT 1 FROM file_coverage WHERE file_id = files.id)
|
|
1170
|
+
AND NOT EXISTS (SELECT 1 FROM stack_frames WHERE file_id = files.id)
|
|
1171
|
+
AND NOT EXISTS (SELECT 1 FROM test_annotations WHERE location_file_id = files.id)
|
|
1172
|
+
AND NOT EXISTS (SELECT 1 FROM test_artifacts WHERE location_file_id = files.id)
|
|
1173
|
+
AND NOT EXISTS (SELECT 1 FROM import_durations WHERE file_id = files.id OR importer_file_id = files.id)
|
|
1174
|
+
AND NOT EXISTS (SELECT 1 FROM source_test_map WHERE source_file_id = files.id)
|
|
1175
|
+
AND NOT EXISTS (SELECT 1 FROM file_edits WHERE file_id = files.id)
|
|
1176
|
+
AND NOT EXISTS (SELECT 1 FROM run_changed_files WHERE file_id = files.id)
|
|
1177
|
+
AND NOT EXISTS (SELECT 1 FROM tdd_artifacts WHERE file_id = files.id)
|
|
1178
|
+
RETURNING id`;
|
|
1179
|
+
const batch = yield* PersistBatch;
|
|
1180
|
+
if (batch !== void 0 && deletedFiles.length > 0) {
|
|
1181
|
+
const gone = new Set(deletedFiles.map((row) => row.id));
|
|
1182
|
+
for (const [path, id] of batch.fileIds) if (gone.has(id)) batch.fileIds.delete(path);
|
|
1183
|
+
}
|
|
1184
|
+
return {
|
|
1185
|
+
prunedRuns: deletedRuns.length,
|
|
1186
|
+
prunedFiles: deletedFiles.length
|
|
1187
|
+
};
|
|
1188
|
+
}).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => new DataStoreError({
|
|
1189
|
+
operation: "write",
|
|
1190
|
+
table: "test_runs",
|
|
1191
|
+
reason: extractSqlReason(e)
|
|
1192
|
+
})));
|
|
1116
1193
|
const associateLatestRunWithSession = (input) => Effect.gen(function* () {
|
|
1117
1194
|
yield* Effect.logDebug("associateLatestRunWithSession").pipe(Effect.annotateLogs({
|
|
1118
1195
|
chatId: input.chatId,
|
|
@@ -1288,6 +1365,7 @@ const DataStoreLive = Layer.effect(DataStore, Effect.gen(function* () {
|
|
|
1288
1365
|
writeRunChangedFiles,
|
|
1289
1366
|
recordIdempotentResponse,
|
|
1290
1367
|
pruneSessions,
|
|
1368
|
+
pruneRuns,
|
|
1291
1369
|
associateLatestRunWithSession,
|
|
1292
1370
|
backfillTestCaseTurns,
|
|
1293
1371
|
registerAgent,
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { SqlClient } from "effect/sql/SqlClient";
|
|
3
|
+
|
|
4
|
+
// src/migrations/0003_run_retention.ts
|
|
5
|
+
/** @internal */
|
|
6
|
+
const migration = Effect.gen(function* () {
|
|
7
|
+
const sql = yield* SqlClient;
|
|
8
|
+
yield* sql`
|
|
9
|
+
CREATE TABLE test_history_new (
|
|
10
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
11
|
+
run_id INTEGER REFERENCES test_runs(id) ON DELETE SET NULL,
|
|
12
|
+
project TEXT NOT NULL,
|
|
13
|
+
module_path TEXT NOT NULL,
|
|
14
|
+
full_name TEXT NOT NULL,
|
|
15
|
+
timestamp TEXT NOT NULL,
|
|
16
|
+
state TEXT NOT NULL CHECK (state IN ('passed', 'failed', 'skipped', 'pending')),
|
|
17
|
+
duration INTEGER,
|
|
18
|
+
flaky INTEGER CHECK (flaky IN (0, 1)),
|
|
19
|
+
retry_count INTEGER DEFAULT 0,
|
|
20
|
+
error_message TEXT,
|
|
21
|
+
UNIQUE(project, module_path, full_name, timestamp)
|
|
22
|
+
)
|
|
23
|
+
`;
|
|
24
|
+
yield* sql`
|
|
25
|
+
INSERT INTO test_history_new (id, run_id, project, module_path, full_name, timestamp, state, duration, flaky, retry_count, error_message)
|
|
26
|
+
SELECT id, run_id, project, module_path, full_name, timestamp, state, duration, flaky, retry_count, error_message
|
|
27
|
+
FROM test_history
|
|
28
|
+
`;
|
|
29
|
+
yield* sql`DROP INDEX IF EXISTS idx_test_history_lookup`;
|
|
30
|
+
yield* sql`DROP INDEX IF EXISTS idx_test_history_full_name`;
|
|
31
|
+
yield* sql`DROP INDEX IF EXISTS idx_test_history_run`;
|
|
32
|
+
yield* sql`DROP TABLE test_history`;
|
|
33
|
+
yield* sql`ALTER TABLE test_history_new RENAME TO test_history`;
|
|
34
|
+
yield* sql`CREATE INDEX idx_test_history_lookup ON test_history(project, module_path, full_name)`;
|
|
35
|
+
yield* sql`CREATE INDEX idx_test_history_full_name ON test_history(full_name, timestamp)`;
|
|
36
|
+
yield* sql`CREATE INDEX idx_test_history_run ON test_history(run_id)`;
|
|
37
|
+
yield* sql`CREATE INDEX IF NOT EXISTS idx_test_runs_project_timestamp ON test_runs(project, timestamp)`;
|
|
38
|
+
yield* sql`CREATE INDEX IF NOT EXISTS idx_test_modules_file ON test_modules(file_id)`;
|
|
39
|
+
yield* sql`CREATE INDEX IF NOT EXISTS idx_scoped_files_file ON scoped_files(file_id)`;
|
|
40
|
+
yield* sql`CREATE INDEX IF NOT EXISTS idx_stack_frames_file ON stack_frames(file_id)`;
|
|
41
|
+
yield* sql`CREATE INDEX IF NOT EXISTS idx_test_annotations_file ON test_annotations(location_file_id)`;
|
|
42
|
+
yield* sql`CREATE INDEX IF NOT EXISTS idx_test_artifacts_file ON test_artifacts(location_file_id)`;
|
|
43
|
+
yield* sql`CREATE INDEX IF NOT EXISTS idx_import_durations_file ON import_durations(file_id)`;
|
|
44
|
+
yield* sql`CREATE INDEX IF NOT EXISTS idx_tdd_artifacts_file ON tdd_artifacts(file_id)`;
|
|
45
|
+
yield* sql`CREATE INDEX IF NOT EXISTS idx_tdd_artifacts_test_run ON tdd_artifacts(test_run_id)`;
|
|
46
|
+
yield* sql`CREATE INDEX IF NOT EXISTS idx_tdd_artifacts_first_failure_run ON tdd_artifacts(test_first_failure_run_id)`;
|
|
47
|
+
yield* sql`CREATE INDEX IF NOT EXISTS idx_failure_signatures_first_seen_run ON failure_signatures(first_seen_run_id)`;
|
|
48
|
+
yield* sql`CREATE INDEX IF NOT EXISTS idx_test_suites_parent ON test_suites(parent_suite_id)`;
|
|
49
|
+
yield* sql`CREATE INDEX IF NOT EXISTS idx_test_errors_suite ON test_errors(test_suite_id)`;
|
|
50
|
+
yield* sql`CREATE INDEX IF NOT EXISTS idx_test_errors_module ON test_errors(module_id)`;
|
|
51
|
+
yield* sql`CREATE INDEX IF NOT EXISTS idx_test_errors_cause ON test_errors(cause_error_id)`;
|
|
52
|
+
yield* sql`CREATE INDEX IF NOT EXISTS idx_tdd_artifacts_test_case ON tdd_artifacts(test_case_id)`;
|
|
53
|
+
yield* sql`CREATE INDEX IF NOT EXISTS idx_task_metadata_case ON task_metadata(test_case_id)`;
|
|
54
|
+
yield* sql`CREATE INDEX IF NOT EXISTS idx_task_metadata_suite ON task_metadata(test_suite_id)`;
|
|
55
|
+
yield* sql`CREATE INDEX IF NOT EXISTS idx_task_metadata_module ON task_metadata(module_id)`;
|
|
56
|
+
yield* sql`CREATE INDEX IF NOT EXISTS idx_hook_executions_module ON hook_executions(test_module_id)`;
|
|
57
|
+
yield* sql`CREATE INDEX IF NOT EXISTS idx_hook_executions_suite ON hook_executions(test_suite_id)`;
|
|
58
|
+
yield* sql`CREATE INDEX IF NOT EXISTS idx_hook_executions_case ON hook_executions(test_case_id)`;
|
|
59
|
+
yield* sql`CREATE INDEX IF NOT EXISTS idx_hook_executions_error ON hook_executions(error_id)`;
|
|
60
|
+
yield* sql`CREATE INDEX IF NOT EXISTS idx_attachments_artifact ON attachments(artifact_id)`;
|
|
61
|
+
yield* sql`CREATE INDEX IF NOT EXISTS idx_attachments_annotation ON attachments(annotation_id)`;
|
|
62
|
+
yield* sql`CREATE INDEX IF NOT EXISTS idx_hypotheses_cited_error ON hypotheses(cited_test_error_id)`;
|
|
63
|
+
yield* sql`CREATE INDEX IF NOT EXISTS idx_hypotheses_cited_frame ON hypotheses(cited_stack_frame_id)`;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
export { migration as default };
|
package/migrations/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import migration from "./0001_initial.js";
|
|
2
2
|
import migration$1 from "./0002_test_artifacts.js";
|
|
3
|
+
import migration$2 from "./0003_run_retention.js";
|
|
3
4
|
|
|
4
5
|
// src/migrations/index.ts
|
|
5
6
|
/**
|
|
@@ -21,7 +22,8 @@ import migration$1 from "./0002_test_artifacts.js";
|
|
|
21
22
|
*/
|
|
22
23
|
const PROJECT_MIGRATIONS = {
|
|
23
24
|
"0001_initial": migration,
|
|
24
|
-
"0002_test_artifacts": migration$1
|
|
25
|
+
"0002_test_artifacts": migration$1,
|
|
26
|
+
"0003_run_retention": migration$2
|
|
25
27
|
};
|
|
26
28
|
|
|
27
29
|
export { PROJECT_MIGRATIONS };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vitest-agent/engine",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.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": [
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"@effected/workspaces": "^0.38.0",
|
|
56
56
|
"@effected/xdg": "^0.14.1",
|
|
57
57
|
"@effected/yaml": "^0.25.0",
|
|
58
|
-
"@vitest-agent/sdk": "7.0
|
|
58
|
+
"@vitest-agent/sdk": "7.1.0",
|
|
59
59
|
"effect": "^4.0.6"
|
|
60
60
|
},
|
|
61
61
|
"engines": {
|
package/testing.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Agent, Agent as Agent$1, AgentId, AgentNotFoundError, AgentNotFoundError as AgentNotFoundError$1, AgentReport, AgentReport as AgentReport$1, ArtifactKind, ArtifactKind as ArtifactKind$1, ArtifactKind as ArtifactKind$2, ArtifactSuite, ArtifactSuite as ArtifactSuite$1, ArtifactSuite as ArtifactSuite$2, BehaviorDetail, BehaviorDetail as BehaviorDetail$1, BehaviorNotFoundError, BehaviorNotFoundError as BehaviorNotFoundError$1, BehaviorRow, BehaviorRow as BehaviorRow$1, BehaviorStatus, BehaviorStatus as BehaviorStatus$1, CacheManifest, CacheManifest as CacheManifest$1, ChatId, CitedArtifact, CitedArtifact as CitedArtifact$1, CoverageBaselines, CoverageBaselines as CoverageBaselines$1, CoverageReport, CoverageReport as CoverageReport$1, DataStoreError, DataStoreError as DataStoreError$1, FileCoverageReport, FileCoverageReport as FileCoverageReport$1, GoalDetail, GoalDetail as GoalDetail$1, GoalNotFoundError, GoalNotFoundError as GoalNotFoundError$1, GoalRow, GoalRow as GoalRow$1, GoalStatus, GoalStatus as GoalStatus$1, HistoryRecord, HistoryRecord as HistoryRecord$1, IdempotencyHit, IdempotencyHit as IdempotencyHit$1, IllegalStatusTransitionEntity, IllegalStatusTransitionError, IllegalStatusTransitionError as IllegalStatusTransitionError$1, Phase, Phase as Phase$1, RegistrationConflictError, RegistrationConflictError as RegistrationConflictError$1, ResolvedThresholds, ResolvedThresholds as ResolvedThresholds$1, TddTaskAlreadyEndedError, TddTaskAlreadyEndedError as TddTaskAlreadyEndedError$1, TddTaskEndOutcome, TddTaskNotFoundError, TddTaskNotFoundError as TddTaskNotFoundError$1, TrendEntry, TrendEntry as TrendEntry$1, TrendRecord, TrendRecord as TrendRecord$1 } from "@vitest-agent/sdk";
|
|
1
|
+
import { Agent, Agent as Agent$1, AgentId, AgentNotFoundError, AgentNotFoundError as AgentNotFoundError$1, AgentReport, AgentReport as AgentReport$1, ArtifactKind, ArtifactKind as ArtifactKind$1, ArtifactKind as ArtifactKind$2, ArtifactSuite, ArtifactSuite as ArtifactSuite$1, ArtifactSuite as ArtifactSuite$2, BehaviorDetail, BehaviorDetail as BehaviorDetail$1, BehaviorNotFoundError, BehaviorNotFoundError as BehaviorNotFoundError$1, BehaviorRow, BehaviorRow as BehaviorRow$1, BehaviorStatus, BehaviorStatus as BehaviorStatus$1, CacheManifest, CacheManifest as CacheManifest$1, ChatId, CitedArtifact, CitedArtifact as CitedArtifact$1, CoverageBaselines, CoverageBaselines as CoverageBaselines$1, CoverageReport, CoverageReport as CoverageReport$1, DataStoreError, DataStoreError as DataStoreError$1, FileCoverageReport, FileCoverageReport as FileCoverageReport$1, GoalDetail, GoalDetail as GoalDetail$1, GoalNotFoundError, GoalNotFoundError as GoalNotFoundError$1, GoalRow, GoalRow as GoalRow$1, GoalStatus, GoalStatus as GoalStatus$1, HistoryRecord, HistoryRecord as HistoryRecord$1, IdempotencyHit, IdempotencyHit as IdempotencyHit$1, IllegalStatusTransitionEntity, IllegalStatusTransitionError, IllegalStatusTransitionError as IllegalStatusTransitionError$1, Phase, Phase as Phase$1, RegistrationConflictError, RegistrationConflictError as RegistrationConflictError$1, ResolvedThresholds, ResolvedThresholds as ResolvedThresholds$1, RunRetentionPolicy, RunRetentionPolicy as RunRetentionPolicy$1, TddTaskAlreadyEndedError, TddTaskAlreadyEndedError as TddTaskAlreadyEndedError$1, TddTaskEndOutcome, TddTaskNotFoundError, TddTaskNotFoundError as TddTaskNotFoundError$1, TrendEntry, TrendEntry as TrendEntry$1, TrendRecord, TrendRecord as TrendRecord$1 } from "@vitest-agent/sdk";
|
|
2
2
|
import { Context, Effect, Layer, Option } from "effect";
|
|
3
3
|
import "@effect/platform-node/NodeServices";
|
|
4
4
|
import "@effect/sql-sqlite-node/SqliteClient";
|
|
@@ -408,6 +408,15 @@ interface RegisterAgentInput {
|
|
|
408
408
|
*/
|
|
409
409
|
readonly agentId?: string;
|
|
410
410
|
}
|
|
411
|
+
/**
|
|
412
|
+
* What `DataStore.pruneRuns` removed: `test_runs` rows deleted (their
|
|
413
|
+
* cascaded rows are not counted) and orphaned `files` rows garbage-collected.
|
|
414
|
+
* @public
|
|
415
|
+
*/
|
|
416
|
+
interface PruneRunsResult {
|
|
417
|
+
readonly prunedRuns: number;
|
|
418
|
+
readonly prunedFiles: number;
|
|
419
|
+
}
|
|
411
420
|
/** @public */
|
|
412
421
|
interface AssociateRunSessionInput {
|
|
413
422
|
readonly chatId: string;
|
|
@@ -505,6 +514,25 @@ declare const DataStore_base: Context.ServiceClass<DataStore, "vitest-agent/Data
|
|
|
505
514
|
readonly affectedSessions: number;
|
|
506
515
|
readonly prunedTurns: number;
|
|
507
516
|
}, DataStoreError$1>;
|
|
517
|
+
/**
|
|
518
|
+
* Apply a run-retention policy: delete every `test_runs` row beyond the
|
|
519
|
+
* newest `policy.keepRuns` of its project, and any run older than
|
|
520
|
+
* `policy.maxAgeDays` days when set, letting the schema's cascades drop
|
|
521
|
+
* the dependent rows (modules, suites, cases, tags, errors, frames,
|
|
522
|
+
* coverage, trends, triggers), then garbage-collect `files` rows no
|
|
523
|
+
* table references any more. `test_history` rows survive with a NULL
|
|
524
|
+
* `run_id`; they keep their own newest-10-per-series window. Before
|
|
525
|
+
* the delete, `file_coverage` rows held by a doomed run are moved onto
|
|
526
|
+
* the lowest surviving run of the same invocation (coverage is written
|
|
527
|
+
* once per invocation, issue 622) unless a survivor already carries
|
|
528
|
+
* coverage rows, so every sibling project's coverage read stays whole.
|
|
529
|
+
*
|
|
530
|
+
* A handful of synchronous statements on the open connection, no
|
|
531
|
+
* yielding, so it may run inside `withPersistTransaction` after the
|
|
532
|
+
* run's own rows are written: the run just written is the newest of
|
|
533
|
+
* its project and is never pruned.
|
|
534
|
+
*/
|
|
535
|
+
readonly pruneRuns: (policy: RunRetentionPolicy$1) => Effect.Effect<PruneRunsResult, DataStoreError$1>;
|
|
508
536
|
readonly associateLatestRunWithSession: (input: AssociateRunSessionInput) => Effect.Effect<void, DataStoreError$1>;
|
|
509
537
|
readonly backfillTestCaseTurns: (chatId: string) => Effect.Effect<number, DataStoreError$1>;
|
|
510
538
|
/**
|
|
@@ -1085,6 +1113,7 @@ type PlatformLiveError = SqlError | StoreError | StoreMigrationError;
|
|
|
1085
1113
|
export declare const PROJECT_MIGRATIONS: {
|
|
1086
1114
|
readonly "0001_initial": import("effect/Effect").Effect<void, import("effect/sql/SqlError").SqlError, import("effect/sql/SqlClient").SqlClient>;
|
|
1087
1115
|
readonly "0002_test_artifacts": import("effect/Effect").Effect<void, import("effect/sql/SqlError").SqlError, import("effect/sql/SqlClient").SqlClient>;
|
|
1116
|
+
readonly "0003_run_retention": import("effect/Effect").Effect<void, import("effect/sql/SqlError").SqlError, import("effect/sql/SqlClient").SqlClient>;
|
|
1088
1117
|
};
|
|
1089
1118
|
/** @public */
|
|
1090
1119
|
export declare function makeTestLayer(filename: string): Layer.Layer<DataReader | DataStore | import("effect/sql/SqlClient").SqlClient | import("@effect/sql-sqlite-node/SqliteClient").SqliteClient | import("@effect/platform-node/NodeServices").NodeServices, PlatformLiveError, never>;
|
|
@@ -1100,5 +1129,5 @@ export declare function withFailures(filename: string): Layer.Layer<DataReader |
|
|
|
1100
1129
|
export declare function flaky(filename: string): Layer.Layer<DataReader | DataStore | import("effect/sql/SqlClient").SqlClient | import("@effect/sql-sqlite-node/SqliteClient").SqliteClient | import("@effect/platform-node/NodeServices").NodeServices, import("@vitest-agent/sdk").DataStoreError | PlatformLiveError, never>;
|
|
1101
1130
|
/** @public */
|
|
1102
1131
|
export declare function withTddTask(filename: string): Layer.Layer<DataReader | DataStore | import("effect/sql/SqlClient").SqlClient | import("@effect/sql-sqlite-node/SqliteClient").SqliteClient | import("@effect/platform-node/NodeServices").NodeServices, import("@vitest-agent/sdk").BehaviorNotFoundError | import("@vitest-agent/sdk").DataStoreError | import("@vitest-agent/sdk").GoalNotFoundError | import("@vitest-agent/sdk").IllegalStatusTransitionError | import("@vitest-agent/sdk").TddTaskAlreadyEndedError | import("@vitest-agent/sdk").TddTaskNotFoundError | PlatformLiveError, never>;
|
|
1103
|
-
export { type AcceptanceMetrics, Agent, AgentId, AgentNotFoundError, AgentReport, type ArtifactKind, type ArtifactSuite, type AssociateRunSessionInput, BehaviorDetail, BehaviorNotFoundError, BehaviorRow, BehaviorStatus, CacheManifest, type ChangeKind, ChatId, type CitedArtifact, type CitedArtifactRow, type ClassificationQueryOptions, type CommitChangesEntry, CoverageBaselines, CoverageReport, type CreateBehaviorInput, type CreateGoalInput, type CurrentTddPhase, DataStoreError, type EndTddTaskInput, type FailureSignatureDetail, type FailureSignatureWriteInput, type FileCoverageInput, FileCoverageReport, type FlakyTest, GoalDetail, GoalNotFoundError, GoalRow, GoalStatus, type HistoryQueryOptions, HistoryRecord, type HypothesisDetail, type HypothesisInput, IdempotencyHit, type IdempotentResponseInput, type IllegalStatusTransitionEntity, IllegalStatusTransitionError, type ModuleInput, type ModuleListEntry, type NoteInput, type NoteRow, type PersistedAttachment, type PersistentFailure, type Phase, type PlatformLiveError, type ProjectRunSummary, type RegisterAgentInput, RegistrationConflictError, type ResolvedThresholds, type RunChangedFile, type RunInvocationMethod, type SessionDetail, type SessionInput, type SettingsInput, type SettingsListEntry, type SettingsRow, type StackFrameInput, type SuiteInput, type SuiteListEntry, type TagInventoryRow, type TddArtifactDetail, type TddArtifactRow, type TddPhaseDetail, TddTaskAlreadyEndedError, type TddTaskDetail, type TddTaskEndOutcome, type TddTaskInput, TddTaskNotFoundError, type TddTaskSummary, type TestAnnotationInput, type TestAnnotationRow, type TestArtifactInput, type TestArtifactQueryOptions, type TestArtifactRow, type TestAttachmentInput, type TestCaseInput, type TestError, type TestErrorInput, type TestListEntry, type TestLookupOptions, type TestRunInput, TrendEntry, TrendRecord, type TurnInput, type TurnSearchOptions, type TurnSummary, type UpdateBehaviorInput, type UpdateGoalInput, type ValidateHypothesisInput, type WriteCommitInput, type WriteRunChangedFilesInput, type WriteTddArtifactInput, type WriteTddPhaseInput, type WriteTddPhaseOutput };
|
|
1132
|
+
export { type AcceptanceMetrics, Agent, AgentId, AgentNotFoundError, AgentReport, type ArtifactKind, type ArtifactSuite, type AssociateRunSessionInput, BehaviorDetail, BehaviorNotFoundError, BehaviorRow, BehaviorStatus, CacheManifest, type ChangeKind, ChatId, type CitedArtifact, type CitedArtifactRow, type ClassificationQueryOptions, type CommitChangesEntry, CoverageBaselines, CoverageReport, type CreateBehaviorInput, type CreateGoalInput, type CurrentTddPhase, DataStoreError, type EndTddTaskInput, type FailureSignatureDetail, type FailureSignatureWriteInput, type FileCoverageInput, FileCoverageReport, type FlakyTest, GoalDetail, GoalNotFoundError, GoalRow, GoalStatus, type HistoryQueryOptions, HistoryRecord, type HypothesisDetail, type HypothesisInput, IdempotencyHit, type IdempotentResponseInput, type IllegalStatusTransitionEntity, IllegalStatusTransitionError, type ModuleInput, type ModuleListEntry, type NoteInput, type NoteRow, type PersistedAttachment, type PersistentFailure, type Phase, type PlatformLiveError, type ProjectRunSummary, type PruneRunsResult, type RegisterAgentInput, RegistrationConflictError, type ResolvedThresholds, type RunChangedFile, type RunInvocationMethod, RunRetentionPolicy, type SessionDetail, type SessionInput, type SettingsInput, type SettingsListEntry, type SettingsRow, type StackFrameInput, type SuiteInput, type SuiteListEntry, type TagInventoryRow, type TddArtifactDetail, type TddArtifactRow, type TddPhaseDetail, TddTaskAlreadyEndedError, type TddTaskDetail, type TddTaskEndOutcome, type TddTaskInput, TddTaskNotFoundError, type TddTaskSummary, type TestAnnotationInput, type TestAnnotationRow, type TestArtifactInput, type TestArtifactQueryOptions, type TestArtifactRow, type TestAttachmentInput, type TestCaseInput, type TestError, type TestErrorInput, type TestListEntry, type TestLookupOptions, type TestRunInput, TrendEntry, TrendRecord, type TurnInput, type TurnSearchOptions, type TurnSummary, type UpdateBehaviorInput, type UpdateGoalInput, type ValidateHypothesisInput, type WriteCommitInput, type WriteRunChangedFilesInput, type WriteTddArtifactInput, type WriteTddPhaseInput, type WriteTddPhaseOutput };
|
|
1104
1133
|
//# sourceMappingURL=testing.d.ts.map
|
package/testing.js
CHANGED
|
@@ -2,7 +2,7 @@ import { DataReader } from "./services/DataReader.js";
|
|
|
2
2
|
import { DataStore } from "./services/DataStore.js";
|
|
3
3
|
import { PROJECT_MIGRATIONS } from "./migrations/index.js";
|
|
4
4
|
import { DataStoreTestLayer, makeTestLayer } from "./testing/layers.js";
|
|
5
|
-
import { Agent, AgentId, AgentNotFoundError, AgentReport, BehaviorDetail, BehaviorNotFoundError, BehaviorRow, BehaviorStatus, CacheManifest, ChatId, CoverageBaselines, CoverageReport, DataStoreError, FileCoverageReport, GoalDetail, GoalNotFoundError, GoalRow, GoalStatus, HistoryRecord, IdempotencyHit, IllegalStatusTransitionError, RegistrationConflictError, TddTaskAlreadyEndedError, TddTaskNotFoundError, TrendEntry, TrendRecord } from "@vitest-agent/sdk";
|
|
5
|
+
import { Agent, AgentId, AgentNotFoundError, AgentReport, BehaviorDetail, BehaviorNotFoundError, BehaviorRow, BehaviorStatus, CacheManifest, ChatId, CoverageBaselines, CoverageReport, DataStoreError, FileCoverageReport, GoalDetail, GoalNotFoundError, GoalRow, GoalStatus, HistoryRecord, IdempotencyHit, IllegalStatusTransitionError, RegistrationConflictError, RunRetentionPolicy, TddTaskAlreadyEndedError, TddTaskNotFoundError, TrendEntry, TrendRecord } from "@vitest-agent/sdk";
|
|
6
6
|
import { Effect, Layer } from "effect";
|
|
7
7
|
|
|
8
8
|
// src/testing/index.ts
|
|
@@ -239,4 +239,4 @@ function withTddTask(filename) {
|
|
|
239
239
|
return Layer.effectDiscard(seed).pipe(Layer.provideMerge(base));
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
-
export { Agent, AgentId, AgentNotFoundError, AgentReport, BehaviorDetail, BehaviorNotFoundError, BehaviorRow, BehaviorStatus, CacheManifest, ChatId, CoverageBaselines, CoverageReport, DataReader, DataStore, DataStoreError, DataStoreTestLayer, FileCoverageReport, GoalDetail, GoalNotFoundError, GoalRow, GoalStatus, HistoryRecord, IdempotencyHit, IllegalStatusTransitionError, PROJECT_MIGRATIONS, RegistrationConflictError, TddTaskAlreadyEndedError, TddTaskNotFoundError, TrendEntry, TrendRecord, empty, flaky, makeTestLayer, singlePassingRun, withFailures, withTddTask };
|
|
242
|
+
export { Agent, AgentId, AgentNotFoundError, AgentReport, BehaviorDetail, BehaviorNotFoundError, BehaviorRow, BehaviorStatus, CacheManifest, ChatId, CoverageBaselines, CoverageReport, DataReader, DataStore, DataStoreError, DataStoreTestLayer, FileCoverageReport, GoalDetail, GoalNotFoundError, GoalRow, GoalStatus, HistoryRecord, IdempotencyHit, IllegalStatusTransitionError, PROJECT_MIGRATIONS, RegistrationConflictError, RunRetentionPolicy, TddTaskAlreadyEndedError, TddTaskNotFoundError, TrendEntry, TrendRecord, empty, flaky, makeTestLayer, singlePassingRun, withFailures, withTddTask };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DataStoreError, extractSqlReason } from "@vitest-agent/sdk";
|
|
2
|
-
import { Context, Effect, References } from "effect";
|
|
2
|
+
import { Context, Effect, Exit, References } from "effect";
|
|
3
3
|
import { SqlClient } from "effect/sql/SqlClient";
|
|
4
4
|
import { isSqlError } from "effect/sql/SqlError";
|
|
5
5
|
|
|
@@ -11,6 +11,55 @@ import { isSqlError } from "effect/sql/SqlError";
|
|
|
11
11
|
* @internal
|
|
12
12
|
*/
|
|
13
13
|
const PersistBatch = Context.Reference("@vitest-agent/engine/PersistBatch", { defaultValue: () => void 0 });
|
|
14
|
+
/**
|
|
15
|
+
* Drop every entry added after `size`. The caches only ever grow (an id for
|
|
16
|
+
* a name never changes while the write lock is held), and a `Map` / `Set`
|
|
17
|
+
* iterates in insertion order, so the entries past the snapshot size are
|
|
18
|
+
* exactly the ones a rolled-back savepoint added.
|
|
19
|
+
*/
|
|
20
|
+
const truncate = (collection, size) => {
|
|
21
|
+
if (collection.size <= size) return;
|
|
22
|
+
let index = 0;
|
|
23
|
+
for (const key of collection.keys()) if (index++ >= size) collection.delete(key);
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Run `effect` as a nested transaction that keeps the
|
|
27
|
+
* {@link withPersistTransaction} caches honest.
|
|
28
|
+
*
|
|
29
|
+
* Outside a persist batch this is exactly `sql.withTransaction(effect)`.
|
|
30
|
+
* Inside one, the nested `withTransaction` becomes a SAVEPOINT, and when it
|
|
31
|
+
* rolls back (a failure, a defect, an interrupt) every `files` / `tags` row
|
|
32
|
+
* the savepoint inserted is gone while the ids `DataStoreLive` cached for
|
|
33
|
+
* them would survive in `PersistBatchState`. This helper snapshots the
|
|
34
|
+
* cache sizes before the savepoint opens and discards the entries added
|
|
35
|
+
* since whenever it does not commit, so a later write in the same batch
|
|
36
|
+
* re-inserts the row instead of referencing one that no longer exists
|
|
37
|
+
* (issue #628: a FK failure or a dangling id). A savepoint that commits
|
|
38
|
+
* keeps its entries.
|
|
39
|
+
*
|
|
40
|
+
* Use this, never a bare `sql.withTransaction`, for any nested transaction
|
|
41
|
+
* whose failure a caller may catch and continue from inside the batch; a
|
|
42
|
+
* bare one that rolls back leaves the caches stale. The bookkeeping is
|
|
43
|
+
* synchronous, so the batch's no-yield guarantee is unchanged.
|
|
44
|
+
*
|
|
45
|
+
* @param effect - the writes to run as one savepoint (or transaction)
|
|
46
|
+
* @public
|
|
47
|
+
*/
|
|
48
|
+
const withPersistSavepoint = (effect) => Effect.gen(function* () {
|
|
49
|
+
const sql = yield* SqlClient;
|
|
50
|
+
const batch = yield* PersistBatch;
|
|
51
|
+
if (batch === void 0) return yield* sql.withTransaction(effect);
|
|
52
|
+
const snapshot = {
|
|
53
|
+
historyRunIds: batch.historyRunIds.size,
|
|
54
|
+
tagIds: batch.tagIds.size,
|
|
55
|
+
fileIds: batch.fileIds.size
|
|
56
|
+
};
|
|
57
|
+
return yield* sql.withTransaction(effect).pipe(Effect.onExit((exit) => Exit.isSuccess(exit) ? Effect.void : Effect.sync(() => {
|
|
58
|
+
truncate(batch.historyRunIds, snapshot.historyRunIds);
|
|
59
|
+
truncate(batch.tagIds, snapshot.tagIds);
|
|
60
|
+
truncate(batch.fileIds, snapshot.fileIds);
|
|
61
|
+
})));
|
|
62
|
+
});
|
|
14
63
|
/** Matches the per-write window `DataStoreLive.writeHistory` keeps outside a batch. */
|
|
15
64
|
const HISTORY_WINDOW = 10;
|
|
16
65
|
/**
|
|
@@ -53,8 +102,13 @@ const pruneHistoryForRun = (sql, runId) => sql`DELETE FROM test_history WHERE id
|
|
|
53
102
|
* genuinely asynchronous work for that reason; CPU-heavy work that needs no
|
|
54
103
|
* database belongs before the block.
|
|
55
104
|
*
|
|
56
|
-
* Nested `withTransaction` calls inside `effect` become savepoints.
|
|
57
|
-
*
|
|
105
|
+
* Nested `withTransaction` calls inside `effect` become savepoints. Open
|
|
106
|
+
* them through {@link withPersistSavepoint}: a savepoint that rolls back
|
|
107
|
+
* also discards the tag / file ids cached while it was open, so a write the
|
|
108
|
+
* caller recovers from cannot leave a later write pointing at a row the
|
|
109
|
+
* rollback removed. A bare nested `sql.withTransaction` whose failure is
|
|
110
|
+
* caught inside the block leaves those caches stale. A `DataReader` query
|
|
111
|
+
* inside the block runs on the transaction's connection.
|
|
58
112
|
*
|
|
59
113
|
* @param effect - the writes (and the reads they depend on) to run atomically
|
|
60
114
|
* @public
|
|
@@ -81,4 +135,4 @@ const withPersistTransaction = (effect) => Effect.gen(function* () {
|
|
|
81
135
|
reason: extractSqlReason(e)
|
|
82
136
|
}))));
|
|
83
137
|
|
|
84
|
-
export { PersistBatch, withPersistTransaction };
|
|
138
|
+
export { PersistBatch, withPersistSavepoint, withPersistTransaction };
|
package/version.js
CHANGED