@vitest-agent/engine 1.2.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.d.ts +86 -8
- package/index.js +2 -2
- package/layers/DataReaderLive.js +59 -6
- package/layers/DataStoreLive.js +20 -1
- package/layers/EnvironmentDetectorLive.js +2 -1
- package/layers/EnvironmentDetectorTest.js +22 -4
- package/layers/ProjectIdentityLive.js +61 -15
- package/lib/format-triage.js +24 -1
- package/migrations/0004_stray_output.js +15 -0
- package/migrations/index.js +3 -1
- package/package.json +2 -2
- package/services/EnvironmentDetector.js +13 -1
- package/services/ProjectIdentity.js +6 -0
- package/testing.d.ts +39 -2
- package/version.js +1 -1
package/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
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, RunRetentionPolicy, 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, ProjectIdentityError, ProjectIdentityNotResolvableError, RegistrationConflictError, ResolvedThresholds, RunRetentionPolicy, StrayOutput, TddTaskAlreadyEndedError, TddTaskNotFoundError, TestClassification, TestRun, TrendEntry, TrendRecord, TurnPayload, VitestAgentConfig } from "@vitest-agent/sdk";
|
|
4
4
|
import { SqlClient } from "effect/sql/SqlClient";
|
|
5
|
-
import { RuntimeEnv } from "@effected/env";
|
|
5
|
+
import { CiName, RuntimeEnv } from "@effected/env";
|
|
6
6
|
import { WorkspaceDiscovery, WorkspaceDiscoveryFailure, WorkspaceRootNotFoundError } from "@effected/workspaces";
|
|
7
7
|
import { AppDirs, AppDirsError, XdgEnvError } from "@effected/xdg";
|
|
8
8
|
import { ChildProcessSpawner } from "effect/process";
|
|
@@ -141,6 +141,13 @@ export interface TestRunInput {
|
|
|
141
141
|
readonly hostSource?: string | null;
|
|
142
142
|
readonly hostValue?: string | null;
|
|
143
143
|
readonly hostMetadata?: Record<string, unknown> | null;
|
|
144
|
+
/**
|
|
145
|
+
* Output the run wrote straight to the terminal past Vitest's console
|
|
146
|
+
* capture (issue 578). A run-level signal: every project row of one
|
|
147
|
+
* invocation carries the same value. Omitted or `null` when the run
|
|
148
|
+
* recorded nothing, which the row stores as NULL in every `stray_*` column.
|
|
149
|
+
*/
|
|
150
|
+
readonly strayOutput?: StrayOutput | null;
|
|
144
151
|
}
|
|
145
152
|
/** @public */
|
|
146
153
|
export interface ModuleInput {
|
|
@@ -615,6 +622,32 @@ export interface ProjectRunSummary {
|
|
|
615
622
|
readonly passed: number;
|
|
616
623
|
readonly failed: number;
|
|
617
624
|
readonly skipped: number;
|
|
625
|
+
/** The latest run's stray output (issue 578); `null` when it recorded none. */
|
|
626
|
+
readonly strayOutput: StrayOutput | null;
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Options for `DataReader.getStrayOutputHistory`.
|
|
630
|
+
* @public
|
|
631
|
+
*/
|
|
632
|
+
export interface StrayOutputHistoryOptions {
|
|
633
|
+
/** Only runs of this project; every project otherwise. */
|
|
634
|
+
readonly project?: string;
|
|
635
|
+
/** How many of the most recent runs to consider. Default 10. */
|
|
636
|
+
readonly limit?: number;
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* How often recent runs wrote stray output (issue 578). A "run" is one
|
|
640
|
+
* reporter invocation: stray output is a run-level signal that every project
|
|
641
|
+
* row of an invocation repeats, so rows are counted once per `invocation_id`.
|
|
642
|
+
* @public
|
|
643
|
+
*/
|
|
644
|
+
export interface StrayOutputHistory {
|
|
645
|
+
/** Runs considered: the most recent ones, up to the requested limit. */
|
|
646
|
+
readonly runs: number;
|
|
647
|
+
/** How many of them recorded any stray output. */
|
|
648
|
+
readonly runsWithStrayOutput: number;
|
|
649
|
+
/** The most recent run's stray output, when that run recorded any. */
|
|
650
|
+
readonly latest: StrayOutput | null;
|
|
618
651
|
}
|
|
619
652
|
/** @public */
|
|
620
653
|
export interface FlakyTest {
|
|
@@ -973,6 +1006,8 @@ export interface TestArtifactRow {
|
|
|
973
1006
|
declare const DataReader_base: Context.ServiceClass<DataReader, "vitest-agent/DataReader", {
|
|
974
1007
|
readonly getLatestRun: (project: string) => Effect.Effect<Option.Option<AgentReport>, DataStoreError>;
|
|
975
1008
|
readonly getRunsByProject: () => Effect.Effect<ReadonlyArray<ProjectRunSummary>, DataStoreError>;
|
|
1009
|
+
/** How often the most recent runs wrote stray output; see {@link StrayOutputHistory}. */
|
|
1010
|
+
readonly getStrayOutputHistory: (options?: StrayOutputHistoryOptions) => Effect.Effect<StrayOutputHistory, DataStoreError>;
|
|
976
1011
|
readonly getHistory: (project: string, options?: HistoryQueryOptions) => Effect.Effect<HistoryRecord, DataStoreError>;
|
|
977
1012
|
readonly getBaselines: (project: string) => Effect.Effect<Option.Option<CoverageBaselines>, DataStoreError>;
|
|
978
1013
|
readonly getTrends: (project: string, limit?: number) => Effect.Effect<Option.Option<TrendRecord>, DataStoreError>;
|
|
@@ -1205,8 +1240,21 @@ declare const EnvironmentDetector_base: Context.ServiceClass<EnvironmentDetector
|
|
|
1205
1240
|
readonly detect: () => Effect.Effect<Environment>;
|
|
1206
1241
|
readonly isAgent: Effect.Effect<boolean>;
|
|
1207
1242
|
readonly agentName: Effect.Effect<string | undefined>;
|
|
1243
|
+
readonly ci: Effect.Effect<Option.Option<CiName>>;
|
|
1208
1244
|
}>;
|
|
1209
|
-
/**
|
|
1245
|
+
/**
|
|
1246
|
+
* Who is running the program and where.
|
|
1247
|
+
*
|
|
1248
|
+
* @remarks
|
|
1249
|
+
* `detect` is the console audience: an agent marker wins over CI, so an
|
|
1250
|
+
* agent inside GitHub Actions reads `agent-shell`. `ci` is the CI vendor
|
|
1251
|
+
* independently of that precedence, so a caller can keep GitHub-only
|
|
1252
|
+
* extras (the step summary, annotations, the `github-actions` reporter)
|
|
1253
|
+
* on for the humans reviewing the job even when the console audience is
|
|
1254
|
+
* the agent (issue #624).
|
|
1255
|
+
*
|
|
1256
|
+
* @public
|
|
1257
|
+
*/
|
|
1210
1258
|
export declare class EnvironmentDetector extends EnvironmentDetector_base {}
|
|
1211
1259
|
/**
|
|
1212
1260
|
* Pure classification behind {@link EnvironmentDetectorLive}: an agent shell
|
|
@@ -1235,9 +1283,21 @@ export declare const classifyEnvironment: (runtime: RuntimeEnv) => Environment;
|
|
|
1235
1283
|
* @public
|
|
1236
1284
|
*/
|
|
1237
1285
|
export declare const EnvironmentDetectorLive: (env: Record<string, string | undefined>) => Layer.Layer<EnvironmentDetector>;
|
|
1238
|
-
/**
|
|
1286
|
+
/**
|
|
1287
|
+
* A fixed `EnvironmentDetector` for tests.
|
|
1288
|
+
*
|
|
1289
|
+
* @remarks
|
|
1290
|
+
* `ci` defaults to the vendor the environment implies (`ci-github` is
|
|
1291
|
+
* GitHub Actions, `ci-generic` another CI, the rest none). Pass
|
|
1292
|
+
* `options.ci` to decouple the two, as the live detector does for an agent
|
|
1293
|
+
* running inside GitHub Actions: `layer("agent-shell", { ci: "github-actions" })`.
|
|
1294
|
+
*
|
|
1295
|
+
* @public
|
|
1296
|
+
*/
|
|
1239
1297
|
export declare const EnvironmentDetectorTest: {
|
|
1240
|
-
layer: (env?: Environment
|
|
1298
|
+
layer: (env?: Environment, options?: {
|
|
1299
|
+
readonly ci?: CiName;
|
|
1300
|
+
}) => Layer.Layer<EnvironmentDetector, never, never>;
|
|
1241
1301
|
};
|
|
1242
1302
|
declare const ExecutorResolver_base: Context.ServiceClass<ExecutorResolver, "vitest-agent/ExecutorResolver", {
|
|
1243
1303
|
readonly resolve: (env: Environment) => Effect.Effect<Executor>;
|
|
@@ -1556,12 +1616,18 @@ export interface ResolveProjectIdentityOptions {
|
|
|
1556
1616
|
*/
|
|
1557
1617
|
export declare const resolveProjectIdentityFromCandidates: (candidates: ProjectIdentityCandidates) => ResolvedIdentity | null;
|
|
1558
1618
|
declare const ProjectIdentity_base: Context.ServiceClass<ProjectIdentity, "vitest-agent/ProjectIdentity", {
|
|
1559
|
-
readonly resolve: (workspaceRoot: string, options?: ResolveProjectIdentityOptions) => Effect.Effect<ResolvedIdentity,
|
|
1619
|
+
readonly resolve: (workspaceRoot: string, options?: ResolveProjectIdentityOptions) => Effect.Effect<ResolvedIdentity, ProjectIdentityError>;
|
|
1560
1620
|
}>;
|
|
1561
1621
|
/**
|
|
1562
1622
|
* Service tag. The Live layer (in `layers/ProjectIdentityLive.ts`)
|
|
1563
1623
|
* wires up the I/O sources; tests provide a stub that returns a
|
|
1564
1624
|
* pre-built `ResolvedIdentity`.
|
|
1625
|
+
*
|
|
1626
|
+
* `resolve` fails with `ProjectIdentityNotResolvableError` when every
|
|
1627
|
+
* source is absent or empty, and with `ProjectIdentitySourceError` when a
|
|
1628
|
+
* source is present but unusable (a malformed `vitest-agent.config.toml`,
|
|
1629
|
+
* an unreadable root `package.json`): a broken config is reported, never
|
|
1630
|
+
* silently replaced by the default identity (issue 321).
|
|
1565
1631
|
* @public
|
|
1566
1632
|
*/
|
|
1567
1633
|
export declare class ProjectIdentity extends ProjectIdentity_base {}
|
|
@@ -1581,17 +1647,21 @@ export type CandidateContext = FileSystem.FileSystem | WorkspaceDiscovery | Vite
|
|
|
1581
1647
|
* `FileSystem`, `CommandExecutor`, `WorkspaceDiscovery`, and
|
|
1582
1648
|
* `VitestAgentConfigFile`. The platform context comes from
|
|
1583
1649
|
* `NodeServices.layer` at the entry point.
|
|
1650
|
+
*
|
|
1651
|
+
* Absent sources fall through (no config file, no workspace root, no
|
|
1652
|
+
* `repository` field, no git remote); a present source that cannot be read
|
|
1653
|
+
* or decoded fails with `ProjectIdentitySourceError`.
|
|
1584
1654
|
* @public
|
|
1585
1655
|
*/
|
|
1586
1656
|
export declare const ProjectIdentityLive: Layer.Layer<ProjectIdentity, never, CandidateContext>;
|
|
1587
1657
|
/**
|
|
1588
1658
|
* Build a Test layer that returns a fixed `ResolvedIdentity` (or
|
|
1589
|
-
* fails with `
|
|
1659
|
+
* fails with the given `ProjectIdentityError`). Use in unit tests
|
|
1590
1660
|
* that depend on `ProjectIdentity` but don't want to wire upstream
|
|
1591
1661
|
* services.
|
|
1592
1662
|
* @public
|
|
1593
1663
|
*/
|
|
1594
|
-
export declare const ProjectIdentityTest: (result: ResolvedIdentity |
|
|
1664
|
+
export declare const ProjectIdentityTest: (result: ResolvedIdentity | ProjectIdentityError) => Layer.Layer<ProjectIdentity, never, never>;
|
|
1595
1665
|
declare const RunContext_base: Schema.Class<RunContext, Schema.Struct<{
|
|
1596
1666
|
readonly gitBranch: Schema.NullOr<Schema.String>;
|
|
1597
1667
|
readonly gitCommitSha: Schema.NullOr<Schema.String>;
|
|
@@ -1671,6 +1741,13 @@ interface FormatTriageOptions {
|
|
|
1671
1741
|
readonly project?: string;
|
|
1672
1742
|
readonly maxLines?: number;
|
|
1673
1743
|
}
|
|
1744
|
+
/**
|
|
1745
|
+
* The "stray output" line of the Recent Test Runs section, or `undefined`
|
|
1746
|
+
* when none of the recent runs wrote past Vitest's console capture (issue
|
|
1747
|
+
* 578). Pure, so the wording is testable without a database.
|
|
1748
|
+
* @public
|
|
1749
|
+
*/
|
|
1750
|
+
export declare const formatStrayOutputLine: (history: StrayOutputHistory) => string | undefined;
|
|
1674
1751
|
/**
|
|
1675
1752
|
* Generates an orientation triage markdown string for LLM agents.
|
|
1676
1753
|
* Summarises recent test runs, active sessions, acceptance metrics,
|
|
@@ -1717,6 +1794,7 @@ export declare const PROJECT_MIGRATIONS: {
|
|
|
1717
1794
|
readonly "0001_initial": import("effect/Effect").Effect<void, import("effect/sql/SqlError").SqlError, import("effect/sql/SqlClient").SqlClient>;
|
|
1718
1795
|
readonly "0002_test_artifacts": import("effect/Effect").Effect<void, import("effect/sql/SqlError").SqlError, import("effect/sql/SqlClient").SqlClient>;
|
|
1719
1796
|
readonly "0003_run_retention": import("effect/Effect").Effect<void, import("effect/sql/SqlError").SqlError, import("effect/sql/SqlClient").SqlClient>;
|
|
1797
|
+
readonly "0004_stray_output": import("effect/Effect").Effect<void, import("effect/sql/SqlError").SqlError, import("effect/sql/SqlClient").SqlClient>;
|
|
1720
1798
|
};
|
|
1721
1799
|
/**
|
|
1722
1800
|
* Migration for the global discovery registry SQLite at
|
package/index.js
CHANGED
|
@@ -29,7 +29,7 @@ import { ProjectIdentity, resolveProjectIdentityFromCandidates } from "./service
|
|
|
29
29
|
import { ProjectIdentityLive, ProjectIdentityTest } from "./layers/ProjectIdentityLive.js";
|
|
30
30
|
import { AgentContext, RunContext, RunContext$, RunContextService } from "./services/RunContext.js";
|
|
31
31
|
import { RunContextLive, RunContextTest } from "./layers/RunContextLive.js";
|
|
32
|
-
import { formatTriageEffect } from "./lib/format-triage.js";
|
|
32
|
+
import { formatStrayOutputLine, formatTriageEffect } from "./lib/format-triage.js";
|
|
33
33
|
import { formatWrapupEffect } from "./lib/format-wrapup.js";
|
|
34
34
|
import migration from "./migrations/0001_initial.js";
|
|
35
35
|
import migration$1 from "./migrations/0002_test_artifacts.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, withPersistSavepoint, 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, formatStrayOutputLine, 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
|
@@ -1,10 +1,25 @@
|
|
|
1
1
|
import { DataReader } from "../services/DataReader.js";
|
|
2
2
|
import { historyKey } from "../services/HistoryTracker.js";
|
|
3
|
-
import { DataStoreError, extractSqlReason } from "@vitest-agent/sdk";
|
|
4
|
-
import { Effect, Layer, Option } from "effect";
|
|
3
|
+
import { DataStoreError, StrayOutputSample, extractSqlReason } from "@vitest-agent/sdk";
|
|
4
|
+
import { Effect, Layer, Option, Schema } from "effect";
|
|
5
5
|
import { SqlClient } from "effect/sql/SqlClient";
|
|
6
6
|
|
|
7
7
|
// src/layers/DataReaderLive.ts
|
|
8
|
+
const decodeStraySamples = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Array(StrayOutputSample)));
|
|
9
|
+
/**
|
|
10
|
+
* The run's stray output from its `test_runs` row, or `null` when the row
|
|
11
|
+
* recorded none (every `stray_*` column NULL: a run that wrote nothing, a
|
|
12
|
+
* passthrough-mode run, or a row older than migration 0004). Pure; the
|
|
13
|
+
* inverse is `DataStoreLive`'s `strayOutputColumns`. A samples column that
|
|
14
|
+
* does not decode reads as no samples rather than failing the row.
|
|
15
|
+
*/
|
|
16
|
+
const strayOutputFromRow = (row) => row.stray_total === null ? null : {
|
|
17
|
+
total: row.stray_total,
|
|
18
|
+
stdout: row.stray_stdout ?? 0,
|
|
19
|
+
stderr: row.stray_stderr ?? 0,
|
|
20
|
+
bytes: row.stray_bytes ?? 0,
|
|
21
|
+
samples: Option.getOrElse(decodeStraySamples(row.stray_samples ?? "[]"), () => [])
|
|
22
|
+
};
|
|
8
23
|
/** @public */
|
|
9
24
|
const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
|
|
10
25
|
const sql = yield* SqlClient;
|
|
@@ -32,7 +47,7 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
|
|
|
32
47
|
});
|
|
33
48
|
const getLatestRun = (project) => Effect.gen(function* () {
|
|
34
49
|
yield* Effect.logDebug("getLatestRun").pipe(Effect.annotateLogs({ project }));
|
|
35
|
-
const runs = yield* sql`SELECT id, timestamp, reason, duration, total, passed, failed, skipped, project
|
|
50
|
+
const runs = yield* sql`SELECT id, timestamp, reason, duration, total, passed, failed, skipped, project, stray_total, stray_stdout, stray_stderr, stray_bytes, stray_samples
|
|
36
51
|
FROM test_runs
|
|
37
52
|
WHERE project = ${project}
|
|
38
53
|
ORDER BY timestamp DESC LIMIT 1`;
|
|
@@ -102,6 +117,7 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
|
|
|
102
117
|
WHERE run_id = ${run.id} AND scope = 'unhandled'`;
|
|
103
118
|
const failedFiles = failedModules.map((m) => m.file_path);
|
|
104
119
|
const projectName = run.project;
|
|
120
|
+
const strayOutput = strayOutputFromRow(run);
|
|
105
121
|
const coverageRunId = yield* resolveCoverageRunId(run.id);
|
|
106
122
|
const fileCovRows = yield* sql`SELECT f.path AS file_path, fc.statements, fc.branches, fc.functions, fc.lines,
|
|
107
123
|
fc.uncovered_lines, fc.tier
|
|
@@ -179,7 +195,8 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
|
|
|
179
195
|
...e.diff != null ? { diff: e.diff } : {}
|
|
180
196
|
})),
|
|
181
197
|
failedFiles,
|
|
182
|
-
...coverage ? { coverage } : {}
|
|
198
|
+
...coverage ? { coverage } : {},
|
|
199
|
+
...strayOutput !== null ? { strayOutput } : {}
|
|
183
200
|
};
|
|
184
201
|
return Option.some(report);
|
|
185
202
|
}).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
|
|
@@ -196,7 +213,12 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
|
|
|
196
213
|
t1.total,
|
|
197
214
|
t1.passed,
|
|
198
215
|
t1.failed,
|
|
199
|
-
t1.skipped
|
|
216
|
+
t1.skipped,
|
|
217
|
+
t1.stray_total,
|
|
218
|
+
t1.stray_stdout,
|
|
219
|
+
t1.stray_stderr,
|
|
220
|
+
t1.stray_bytes,
|
|
221
|
+
t1.stray_samples
|
|
200
222
|
FROM test_runs t1
|
|
201
223
|
INNER JOIN (
|
|
202
224
|
SELECT project, MAX(timestamp) as max_ts
|
|
@@ -210,13 +232,43 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
|
|
|
210
232
|
total: r.total,
|
|
211
233
|
passed: r.passed,
|
|
212
234
|
failed: r.failed,
|
|
213
|
-
skipped: r.skipped
|
|
235
|
+
skipped: r.skipped,
|
|
236
|
+
strayOutput: strayOutputFromRow(r)
|
|
214
237
|
}));
|
|
215
238
|
}).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
|
|
216
239
|
operation: "read",
|
|
217
240
|
table: "test_runs",
|
|
218
241
|
reason: extractSqlReason(e)
|
|
219
242
|
})));
|
|
243
|
+
const getStrayOutputHistory = (options = {}) => Effect.gen(function* () {
|
|
244
|
+
const limit = options.limit ?? 10;
|
|
245
|
+
yield* Effect.logDebug("getStrayOutputHistory").pipe(Effect.annotateLogs({
|
|
246
|
+
project: options.project ?? "*",
|
|
247
|
+
limit
|
|
248
|
+
}));
|
|
249
|
+
const rows = yield* sql`SELECT
|
|
250
|
+
MAX(timestamp) AS timestamp,
|
|
251
|
+
MAX(stray_total) AS stray_total,
|
|
252
|
+
MAX(stray_stdout) AS stray_stdout,
|
|
253
|
+
MAX(stray_stderr) AS stray_stderr,
|
|
254
|
+
MAX(stray_bytes) AS stray_bytes,
|
|
255
|
+
MAX(stray_samples) AS stray_samples
|
|
256
|
+
FROM test_runs
|
|
257
|
+
${options.project !== void 0 ? sql`WHERE project = ${options.project}` : sql``}
|
|
258
|
+
GROUP BY invocation_id
|
|
259
|
+
ORDER BY timestamp DESC
|
|
260
|
+
LIMIT ${limit}`;
|
|
261
|
+
const recorded = rows.map(strayOutputFromRow);
|
|
262
|
+
return {
|
|
263
|
+
runs: rows.length,
|
|
264
|
+
runsWithStrayOutput: recorded.filter((s) => s !== null).length,
|
|
265
|
+
latest: recorded[0] ?? null
|
|
266
|
+
};
|
|
267
|
+
}).pipe(Effect.annotateLogs("service", "DataReader"), Effect.mapError((e) => new DataStoreError({
|
|
268
|
+
operation: "read",
|
|
269
|
+
table: "test_runs",
|
|
270
|
+
reason: extractSqlReason(e)
|
|
271
|
+
})));
|
|
220
272
|
const getHistory = (project, options) => Effect.gen(function* () {
|
|
221
273
|
yield* Effect.logDebug("getHistory").pipe(Effect.annotateLogs({
|
|
222
274
|
project,
|
|
@@ -1878,6 +1930,7 @@ const DataReaderLive = Layer.effect(DataReader, Effect.gen(function* () {
|
|
|
1878
1930
|
return {
|
|
1879
1931
|
getLatestRun,
|
|
1880
1932
|
getRunsByProject,
|
|
1933
|
+
getStrayOutputHistory,
|
|
1881
1934
|
getHistory,
|
|
1882
1935
|
getBaselines,
|
|
1883
1936
|
getTrends,
|
package/layers/DataStoreLive.js
CHANGED
|
@@ -7,6 +7,24 @@ import { Buffer } from "node:buffer";
|
|
|
7
7
|
import { randomUUID } from "node:crypto";
|
|
8
8
|
|
|
9
9
|
// src/layers/DataStoreLive.ts
|
|
10
|
+
/**
|
|
11
|
+
* The five `test_runs.stray_*` column values for a run's stray output: all
|
|
12
|
+
* NULL when the run recorded nothing, so a reader cannot mistake "nothing
|
|
13
|
+
* recorded" for "zero lines" (pure; the inverse lives in `DataReaderLive`).
|
|
14
|
+
*/
|
|
15
|
+
const strayOutputColumns = (stray) => stray == null ? {
|
|
16
|
+
total: null,
|
|
17
|
+
stdout: null,
|
|
18
|
+
stderr: null,
|
|
19
|
+
bytes: null,
|
|
20
|
+
samples: null
|
|
21
|
+
} : {
|
|
22
|
+
total: stray.total,
|
|
23
|
+
stdout: stray.stdout,
|
|
24
|
+
stderr: stray.stderr,
|
|
25
|
+
bytes: stray.bytes,
|
|
26
|
+
samples: JSON.stringify(stray.samples)
|
|
27
|
+
};
|
|
10
28
|
const isLegalLifecycleTransition = (from, to) => {
|
|
11
29
|
if (from === to) return true;
|
|
12
30
|
if (from === "done" || from === "abandoned") return false;
|
|
@@ -70,7 +88,8 @@ const DataStoreLive = Layer.effect(DataStore, Effect.gen(function* () {
|
|
|
70
88
|
const conversationId = input.conversationId ?? null;
|
|
71
89
|
const gitDirtyInt = input.gitDirty === void 0 || input.gitDirty === null ? null : input.gitDirty ? 1 : 0;
|
|
72
90
|
const hostMetadataJson = input.hostMetadata == null ? null : JSON.stringify(input.hostMetadata);
|
|
73
|
-
|
|
91
|
+
const stray = strayOutputColumns(input.strayOutput);
|
|
92
|
+
yield* sql`INSERT INTO test_runs (invocation_id, project, settings_hash, timestamp, commit_sha, branch, reason, duration, total, passed, failed, skipped, scoped, snapshot_added, snapshot_matched, snapshot_unmatched, snapshot_updated, snapshot_unchecked, snapshot_total, snapshot_failure, snapshot_did_update, snapshot_files_added, snapshot_files_removed, snapshot_files_unmatched, snapshot_files_updated, actor_type, agent_id, conversation_id, git_branch, git_commit_sha, git_dirty, git_upstream, git_worktree_dir, host_source, host_value, host_metadata, stray_total, stray_stdout, stray_stderr, stray_bytes, stray_samples) VALUES (${input.invocationId}, ${input.project}, ${input.settingsHash}, ${input.timestamp}, ${input.commitSha ?? null}, ${input.branch ?? null}, ${input.reason}, ${input.duration}, ${input.total}, ${input.passed}, ${input.failed}, ${input.skipped}, ${input.scoped ? 1 : 0}, ${input.snapshotAdded ?? 0}, ${input.snapshotMatched ?? 0}, ${input.snapshotUnmatched ?? 0}, ${input.snapshotUpdated ?? 0}, ${input.snapshotUnchecked ?? 0}, ${input.snapshotTotal ?? 0}, ${boolToInt(input.snapshotFailure) ?? 0}, ${boolToInt(input.snapshotDidUpdate) ?? 0}, ${input.snapshotFilesAdded ?? 0}, ${input.snapshotFilesRemoved ?? 0}, ${input.snapshotFilesUnmatched ?? 0}, ${input.snapshotFilesUpdated ?? 0}, ${actorType}, ${agentId}, ${conversationId}, ${input.gitBranch ?? null}, ${input.gitCommitSha ?? null}, ${gitDirtyInt}, ${input.gitUpstream ?? null}, ${input.gitWorktreeDir ?? null}, ${input.hostSource ?? null}, ${input.hostValue ?? null}, ${hostMetadataJson}, ${stray.total}, ${stray.stdout}, ${stray.stderr}, ${stray.bytes}, ${stray.samples})`;
|
|
74
93
|
return (yield* sql`SELECT last_insert_rowid() as id`)[0].id;
|
|
75
94
|
}).pipe(Effect.annotateLogs("service", "DataStore"), Effect.mapError((e) => new DataStoreError({
|
|
76
95
|
operation: "write",
|
|
@@ -48,7 +48,8 @@ const EnvironmentDetectorLive = (env) => Layer.effect(EnvironmentDetector, Effec
|
|
|
48
48
|
return {
|
|
49
49
|
detect: () => Effect.succeed(environment),
|
|
50
50
|
isAgent: Effect.succeed(Option.isSome(runtime.agent)),
|
|
51
|
-
agentName: Effect.succeed(Option.getOrUndefined(runtime.agent))
|
|
51
|
+
agentName: Effect.succeed(Option.getOrUndefined(runtime.agent)),
|
|
52
|
+
ci: Effect.succeed(runtime.ci)
|
|
52
53
|
};
|
|
53
54
|
})).pipe(Layer.provide(CurrentRuntimeEnv.layerFrom(env)));
|
|
54
55
|
|
|
@@ -1,12 +1,30 @@
|
|
|
1
1
|
import { EnvironmentDetector } from "../services/EnvironmentDetector.js";
|
|
2
|
-
import { Effect, Layer } from "effect";
|
|
2
|
+
import { Effect, Layer, Option } from "effect";
|
|
3
3
|
|
|
4
4
|
// src/layers/EnvironmentDetectorTest.ts
|
|
5
|
-
/**
|
|
6
|
-
const
|
|
5
|
+
/** The CI vendor each environment implies when a test does not say otherwise. */
|
|
6
|
+
const CI_FOR_ENVIRONMENT = {
|
|
7
|
+
"ci-github": Option.some("github-actions"),
|
|
8
|
+
"ci-generic": Option.some("generic"),
|
|
9
|
+
"agent-shell": Option.none(),
|
|
10
|
+
terminal: Option.none()
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* A fixed `EnvironmentDetector` for tests.
|
|
14
|
+
*
|
|
15
|
+
* @remarks
|
|
16
|
+
* `ci` defaults to the vendor the environment implies (`ci-github` is
|
|
17
|
+
* GitHub Actions, `ci-generic` another CI, the rest none). Pass
|
|
18
|
+
* `options.ci` to decouple the two, as the live detector does for an agent
|
|
19
|
+
* running inside GitHub Actions: `layer("agent-shell", { ci: "github-actions" })`.
|
|
20
|
+
*
|
|
21
|
+
* @public
|
|
22
|
+
*/
|
|
23
|
+
const EnvironmentDetectorTest = { layer: (env = "terminal", options = {}) => Layer.succeed(EnvironmentDetector, {
|
|
7
24
|
detect: () => Effect.succeed(env),
|
|
8
25
|
isAgent: Effect.succeed(env === "agent-shell"),
|
|
9
|
-
agentName: Effect.succeed(env === "agent-shell" ? "test-agent" : void 0)
|
|
26
|
+
agentName: Effect.succeed(env === "agent-shell" ? "test-agent" : void 0),
|
|
27
|
+
ci: Effect.succeed(options.ci !== void 0 ? Option.some(options.ci) : CI_FOR_ENVIRONMENT[env])
|
|
10
28
|
}) };
|
|
11
29
|
|
|
12
30
|
export { EnvironmentDetectorTest };
|
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
import { VitestAgentConfigFile } from "../services/Config.js";
|
|
2
2
|
import { ProjectIdentity, resolveProjectIdentityFromCandidates } from "../services/ProjectIdentity.js";
|
|
3
|
-
import { ProjectIdentityNotResolvableError, VitestAgentConfig } from "@vitest-agent/sdk";
|
|
3
|
+
import { ProjectIdentityNotResolvableError, ProjectIdentitySourceError, VitestAgentConfig } from "@vitest-agent/sdk";
|
|
4
4
|
import { Effect, FileSystem, Layer, Option } from "effect";
|
|
5
5
|
import { WorkspaceDiscovery } from "@effected/workspaces";
|
|
6
6
|
import { ChildProcess, ChildProcessSpawner } from "effect/process";
|
|
7
7
|
|
|
8
8
|
// src/layers/ProjectIdentityLive.ts
|
|
9
|
+
/** The one-line text of a kit failure, for `ProjectIdentitySourceError.detail`. */
|
|
10
|
+
const detailOf = (cause) => cause instanceof Error && cause.message.length > 0 ? cause.message : String(cause);
|
|
11
|
+
/**
|
|
12
|
+
* The git remote is an OPTIONAL source: a repository with no
|
|
13
|
+
* `remote.origin.url`, a directory that is not a git checkout, or a machine
|
|
14
|
+
* with no `git` on `PATH` are all "no remote" rather than failures. `git
|
|
15
|
+
* config --get` exits non-zero for an unset key, so every spawner failure
|
|
16
|
+
* folds to `Option.none()` on purpose — this is the one catch in this module
|
|
17
|
+
* that stays broad, and it is a policy, not an accident.
|
|
18
|
+
*/
|
|
9
19
|
const readGitRemote = (cwd) => Effect.gen(function* () {
|
|
10
20
|
const trimmed = (yield* (yield* ChildProcessSpawner.ChildProcessSpawner).string(ChildProcess.make("git", [
|
|
11
21
|
"config",
|
|
@@ -14,23 +24,55 @@ const readGitRemote = (cwd) => Effect.gen(function* () {
|
|
|
14
24
|
]).pipe(ChildProcess.setCwd(cwd)))).trim();
|
|
15
25
|
return trimmed.length > 0 ? Option.some(trimmed) : Option.none();
|
|
16
26
|
}).pipe(Effect.catch(() => Effect.succeed(Option.none())));
|
|
27
|
+
/**
|
|
28
|
+
* The `repository` URL of the root `package.json` discovery named.
|
|
29
|
+
*
|
|
30
|
+
* A file that is not there (`NotFound`) is an absent source. Any other
|
|
31
|
+
* filesystem failure (permission denied, an I/O error) and a body that is not
|
|
32
|
+
* JSON are `ProjectIdentitySourceError`s: the file exists and says
|
|
33
|
+
* something, and silently reading it as "no repository" would hand the
|
|
34
|
+
* operator the wrong identity.
|
|
35
|
+
*/
|
|
17
36
|
const readPackageRepoUrl = (packageJsonPath) => Effect.gen(function* () {
|
|
18
|
-
const raw = yield* (yield* FileSystem.FileSystem).readFileString(packageJsonPath).pipe(Effect.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
37
|
+
const raw = yield* (yield* FileSystem.FileSystem).readFileString(packageJsonPath).pipe(Effect.map(Option.some), Effect.catchTag("PlatformError", (error) => error.reason._tag === "NotFound" ? Effect.succeed(Option.none()) : Effect.fail(new ProjectIdentitySourceError({
|
|
38
|
+
source: "package-json",
|
|
39
|
+
path: packageJsonPath,
|
|
40
|
+
detail: detailOf(error),
|
|
41
|
+
cause: error
|
|
42
|
+
}))));
|
|
43
|
+
if (Option.isNone(raw)) return Option.none();
|
|
44
|
+
const parsed = yield* Effect.try({
|
|
45
|
+
try: () => JSON.parse(raw.value),
|
|
46
|
+
catch: (cause) => new ProjectIdentitySourceError({
|
|
47
|
+
source: "package-json",
|
|
48
|
+
path: packageJsonPath,
|
|
49
|
+
detail: detailOf(cause),
|
|
50
|
+
cause
|
|
51
|
+
})
|
|
52
|
+
});
|
|
53
|
+
if (parsed === null || typeof parsed !== "object") return Option.none();
|
|
54
|
+
const repo = parsed.repository;
|
|
55
|
+
if (typeof repo === "string" && repo.trim().length > 0) return Option.some(repo.trim());
|
|
56
|
+
if (repo !== null && typeof repo === "object" && "url" in repo && typeof repo.url === "string") {
|
|
57
|
+
const url = repo.url.trim();
|
|
58
|
+
return url.length > 0 ? Option.some(url) : Option.none();
|
|
59
|
+
}
|
|
28
60
|
return Option.none();
|
|
29
61
|
});
|
|
30
62
|
const collectCandidates = (workspaceRoot, options) => Effect.gen(function* () {
|
|
31
|
-
const tomlConfig = yield* (yield* VitestAgentConfigFile).loadOrDefault(new VitestAgentConfig({})).pipe(Effect.
|
|
63
|
+
const tomlConfig = yield* (yield* VitestAgentConfigFile).loadOrDefault(new VitestAgentConfig({})).pipe(Effect.mapError((cause) => new ProjectIdentitySourceError({
|
|
64
|
+
source: "toml",
|
|
65
|
+
path: cause._tag === "ConfigValidationError" ? Option.getOrElse(cause.path, () => workspaceRoot) : cause.path ?? workspaceRoot,
|
|
66
|
+
detail: detailOf(cause),
|
|
67
|
+
cause
|
|
68
|
+
})));
|
|
32
69
|
const gitRemote = yield* readGitRemote(workspaceRoot);
|
|
33
|
-
const root = (yield* (yield* WorkspaceDiscovery).listPackagesIn(workspaceRoot).pipe(Effect.
|
|
70
|
+
const root = (yield* (yield* WorkspaceDiscovery).listPackagesIn(workspaceRoot).pipe(Effect.catchTag("WorkspaceRootNotFoundError", () => Effect.succeed([])), Effect.mapError((cause) => new ProjectIdentitySourceError({
|
|
71
|
+
source: "workspace",
|
|
72
|
+
path: cause._tag === "WorkspacePatternError" ? cause.root : cause.path,
|
|
73
|
+
detail: detailOf(cause),
|
|
74
|
+
cause
|
|
75
|
+
})))).find((pkg) => pkg.isRootWorkspace);
|
|
34
76
|
const packageJsonRepoUrl = root ? yield* readPackageRepoUrl(root.packageJsonPath) : Option.none();
|
|
35
77
|
const candidates = {};
|
|
36
78
|
if (options.projectId !== void 0) candidates.explicit = options.projectId;
|
|
@@ -69,6 +111,10 @@ const candidatesToTriedList = (candidates) => [
|
|
|
69
111
|
* `FileSystem`, `CommandExecutor`, `WorkspaceDiscovery`, and
|
|
70
112
|
* `VitestAgentConfigFile`. The platform context comes from
|
|
71
113
|
* `NodeServices.layer` at the entry point.
|
|
114
|
+
*
|
|
115
|
+
* Absent sources fall through (no config file, no workspace root, no
|
|
116
|
+
* `repository` field, no git remote); a present source that cannot be read
|
|
117
|
+
* or decoded fails with `ProjectIdentitySourceError`.
|
|
72
118
|
* @public
|
|
73
119
|
*/
|
|
74
120
|
const ProjectIdentityLive = Layer.effect(ProjectIdentity, Effect.gen(function* () {
|
|
@@ -84,11 +130,11 @@ const ProjectIdentityLive = Layer.effect(ProjectIdentity, Effect.gen(function* (
|
|
|
84
130
|
}));
|
|
85
131
|
/**
|
|
86
132
|
* Build a Test layer that returns a fixed `ResolvedIdentity` (or
|
|
87
|
-
* fails with `
|
|
133
|
+
* fails with the given `ProjectIdentityError`). Use in unit tests
|
|
88
134
|
* that depend on `ProjectIdentity` but don't want to wire upstream
|
|
89
135
|
* services.
|
|
90
136
|
* @public
|
|
91
137
|
*/
|
|
92
|
-
const ProjectIdentityTest = (result) => Layer.succeed(ProjectIdentity, { resolve: () => result instanceof ProjectIdentityNotResolvableError ? Effect.fail(result) : Effect.succeed(result) });
|
|
138
|
+
const ProjectIdentityTest = (result) => Layer.succeed(ProjectIdentity, { resolve: () => result instanceof ProjectIdentityNotResolvableError || result instanceof ProjectIdentitySourceError ? Effect.fail(result) : Effect.succeed(result) });
|
|
93
139
|
|
|
94
140
|
export { ProjectIdentityLive, ProjectIdentityTest };
|
package/lib/format-triage.js
CHANGED
|
@@ -2,6 +2,19 @@ import { DataReader } from "../services/DataReader.js";
|
|
|
2
2
|
import { Effect, Option } from "effect";
|
|
3
3
|
|
|
4
4
|
// src/lib/format-triage.ts
|
|
5
|
+
const STRAY_OUTPUT_WINDOW = 10;
|
|
6
|
+
const describeStrayOutput = (stray) => {
|
|
7
|
+
const lines = `${stray.total} line${stray.total === 1 ? "" : "s"}`;
|
|
8
|
+
const sample = stray.samples[0];
|
|
9
|
+
return sample === void 0 ? `${lines}, ${stray.bytes} bytes` : `${lines}, ${stray.bytes} bytes; first: \`${sample.text}\``;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* The "stray output" line of the Recent Test Runs section, or `undefined`
|
|
13
|
+
* when none of the recent runs wrote past Vitest's console capture (issue
|
|
14
|
+
* 578). Pure, so the wording is testable without a database.
|
|
15
|
+
* @public
|
|
16
|
+
*/
|
|
17
|
+
const formatStrayOutputLine = (history) => history.runsWithStrayOutput === 0 ? void 0 : `- Stray output in ${history.runsWithStrayOutput} of the last ${history.runs} run${history.runs === 1 ? "" : "s"}${history.latest === null ? "" : ` (latest: ${describeStrayOutput(history.latest)})`} — tests or child processes writing straight to the terminal`;
|
|
5
18
|
/**
|
|
6
19
|
* Generates an orientation triage markdown string for LLM agents.
|
|
7
20
|
* Summarises recent test runs, active sessions, acceptance metrics,
|
|
@@ -16,6 +29,14 @@ const formatTriageEffect = (options = {}) => Effect.gen(function* () {
|
|
|
16
29
|
const allProjects = yield* reader.getRunsByProject().pipe(Effect.orElseSucceed(() => []));
|
|
17
30
|
const projects = options.project ? allProjects.filter((p) => p.project === options.project) : allProjects;
|
|
18
31
|
const sessions = yield* reader.listSessions({}).pipe(Effect.orElseSucceed(() => []));
|
|
32
|
+
const strayHistory = yield* reader.getStrayOutputHistory({
|
|
33
|
+
...options.project !== void 0 && { project: options.project },
|
|
34
|
+
limit: STRAY_OUTPUT_WINDOW
|
|
35
|
+
}).pipe(Effect.orElseSucceed(() => ({
|
|
36
|
+
runs: 0,
|
|
37
|
+
runsWithStrayOutput: 0,
|
|
38
|
+
latest: null
|
|
39
|
+
})));
|
|
19
40
|
const fallbackMetrics = {
|
|
20
41
|
phaseEvidenceIntegrity: {
|
|
21
42
|
total: 0,
|
|
@@ -59,6 +80,8 @@ const formatTriageEffect = (options = {}) => Effect.gen(function* () {
|
|
|
59
80
|
const counts = `${p.passed} passed, ${p.failed} failed`;
|
|
60
81
|
lines.push(`- **${p.project}** — ${status} (${counts})`);
|
|
61
82
|
}
|
|
83
|
+
const strayLine = formatStrayOutputLine(strayHistory);
|
|
84
|
+
if (strayLine !== void 0) lines.push(strayLine);
|
|
62
85
|
lines.push("");
|
|
63
86
|
} else {
|
|
64
87
|
lines.push("### Recent Test Runs");
|
|
@@ -99,4 +122,4 @@ const formatTriageEffect = (options = {}) => Effect.gen(function* () {
|
|
|
99
122
|
return out;
|
|
100
123
|
});
|
|
101
124
|
|
|
102
|
-
export { formatTriageEffect };
|
|
125
|
+
export { formatStrayOutputLine, formatTriageEffect };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { SqlClient } from "effect/sql/SqlClient";
|
|
3
|
+
|
|
4
|
+
// src/migrations/0004_stray_output.ts
|
|
5
|
+
/** @internal */
|
|
6
|
+
const migration = Effect.gen(function* () {
|
|
7
|
+
const sql = yield* SqlClient;
|
|
8
|
+
yield* sql`ALTER TABLE test_runs ADD COLUMN stray_total INTEGER`;
|
|
9
|
+
yield* sql`ALTER TABLE test_runs ADD COLUMN stray_stdout INTEGER`;
|
|
10
|
+
yield* sql`ALTER TABLE test_runs ADD COLUMN stray_stderr INTEGER`;
|
|
11
|
+
yield* sql`ALTER TABLE test_runs ADD COLUMN stray_bytes INTEGER`;
|
|
12
|
+
yield* sql`ALTER TABLE test_runs ADD COLUMN stray_samples TEXT`;
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export { migration as default };
|
package/migrations/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import migration from "./0001_initial.js";
|
|
2
2
|
import migration$1 from "./0002_test_artifacts.js";
|
|
3
3
|
import migration$2 from "./0003_run_retention.js";
|
|
4
|
+
import migration$3 from "./0004_stray_output.js";
|
|
4
5
|
|
|
5
6
|
// src/migrations/index.ts
|
|
6
7
|
/**
|
|
@@ -23,7 +24,8 @@ import migration$2 from "./0003_run_retention.js";
|
|
|
23
24
|
const PROJECT_MIGRATIONS = {
|
|
24
25
|
"0001_initial": migration,
|
|
25
26
|
"0002_test_artifacts": migration$1,
|
|
26
|
-
"0003_run_retention": migration$2
|
|
27
|
+
"0003_run_retention": migration$2,
|
|
28
|
+
"0004_stray_output": migration$3
|
|
27
29
|
};
|
|
28
30
|
|
|
29
31
|
export { PROJECT_MIGRATIONS };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vitest-agent/engine",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.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": "
|
|
58
|
+
"@vitest-agent/sdk": "8.0.0",
|
|
59
59
|
"effect": "^4.0.6"
|
|
60
60
|
},
|
|
61
61
|
"engines": {
|
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
import { Context } from "effect";
|
|
2
2
|
|
|
3
3
|
// src/services/EnvironmentDetector.ts
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* Who is running the program and where.
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* `detect` is the console audience: an agent marker wins over CI, so an
|
|
9
|
+
* agent inside GitHub Actions reads `agent-shell`. `ci` is the CI vendor
|
|
10
|
+
* independently of that precedence, so a caller can keep GitHub-only
|
|
11
|
+
* extras (the step summary, annotations, the `github-actions` reporter)
|
|
12
|
+
* on for the humans reviewing the job even when the console audience is
|
|
13
|
+
* the agent (issue #624).
|
|
14
|
+
*
|
|
15
|
+
* @public
|
|
16
|
+
*/
|
|
5
17
|
var EnvironmentDetector = class extends Context.Service()("vitest-agent/EnvironmentDetector") {};
|
|
6
18
|
|
|
7
19
|
export { EnvironmentDetector };
|
|
@@ -54,6 +54,12 @@ const resolveProjectIdentityFromCandidates = (candidates) => {
|
|
|
54
54
|
* Service tag. The Live layer (in `layers/ProjectIdentityLive.ts`)
|
|
55
55
|
* wires up the I/O sources; tests provide a stub that returns a
|
|
56
56
|
* pre-built `ResolvedIdentity`.
|
|
57
|
+
*
|
|
58
|
+
* `resolve` fails with `ProjectIdentityNotResolvableError` when every
|
|
59
|
+
* source is absent or empty, and with `ProjectIdentitySourceError` when a
|
|
60
|
+
* source is present but unusable (a malformed `vitest-agent.config.toml`,
|
|
61
|
+
* an unreadable root `package.json`): a broken config is reported, never
|
|
62
|
+
* silently replaced by the default identity (issue 321).
|
|
57
63
|
* @public
|
|
58
64
|
*/
|
|
59
65
|
var ProjectIdentity = class extends Context.Service()("vitest-agent/ProjectIdentity") {};
|
package/testing.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
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";
|
|
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, StrayOutput, 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";
|
|
5
5
|
import { StoreError, StoreMigrationError } from "@effected/store";
|
|
6
6
|
import "effect/sql/SqlClient";
|
|
7
7
|
import { SqlError } from "effect/sql/SqlError";
|
|
8
|
+
import "@effected/env";
|
|
8
9
|
/** @public */
|
|
9
10
|
interface CreateGoalInput {
|
|
10
11
|
readonly tddTaskId: number;
|
|
@@ -99,6 +100,13 @@ interface TestRunInput {
|
|
|
99
100
|
readonly hostSource?: string | null;
|
|
100
101
|
readonly hostValue?: string | null;
|
|
101
102
|
readonly hostMetadata?: Record<string, unknown> | null;
|
|
103
|
+
/**
|
|
104
|
+
* Output the run wrote straight to the terminal past Vitest's console
|
|
105
|
+
* capture (issue 578). A run-level signal: every project row of one
|
|
106
|
+
* invocation carries the same value. Omitted or `null` when the run
|
|
107
|
+
* recorded nothing, which the row stores as NULL in every `stray_*` column.
|
|
108
|
+
*/
|
|
109
|
+
readonly strayOutput?: StrayOutput | null;
|
|
102
110
|
}
|
|
103
111
|
/** @public */
|
|
104
112
|
interface ModuleInput {
|
|
@@ -566,6 +574,32 @@ interface ProjectRunSummary {
|
|
|
566
574
|
readonly passed: number;
|
|
567
575
|
readonly failed: number;
|
|
568
576
|
readonly skipped: number;
|
|
577
|
+
/** The latest run's stray output (issue 578); `null` when it recorded none. */
|
|
578
|
+
readonly strayOutput: StrayOutput | null;
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Options for `DataReader.getStrayOutputHistory`.
|
|
582
|
+
* @public
|
|
583
|
+
*/
|
|
584
|
+
interface StrayOutputHistoryOptions {
|
|
585
|
+
/** Only runs of this project; every project otherwise. */
|
|
586
|
+
readonly project?: string;
|
|
587
|
+
/** How many of the most recent runs to consider. Default 10. */
|
|
588
|
+
readonly limit?: number;
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* How often recent runs wrote stray output (issue 578). A "run" is one
|
|
592
|
+
* reporter invocation: stray output is a run-level signal that every project
|
|
593
|
+
* row of an invocation repeats, so rows are counted once per `invocation_id`.
|
|
594
|
+
* @public
|
|
595
|
+
*/
|
|
596
|
+
interface StrayOutputHistory {
|
|
597
|
+
/** Runs considered: the most recent ones, up to the requested limit. */
|
|
598
|
+
readonly runs: number;
|
|
599
|
+
/** How many of them recorded any stray output. */
|
|
600
|
+
readonly runsWithStrayOutput: number;
|
|
601
|
+
/** The most recent run's stray output, when that run recorded any. */
|
|
602
|
+
readonly latest: StrayOutput | null;
|
|
569
603
|
}
|
|
570
604
|
/** @public */
|
|
571
605
|
interface FlakyTest {
|
|
@@ -924,6 +958,8 @@ interface TestArtifactRow {
|
|
|
924
958
|
declare const DataReader_base: Context.ServiceClass<DataReader, "vitest-agent/DataReader", {
|
|
925
959
|
readonly getLatestRun: (project: string) => Effect.Effect<Option.Option<AgentReport$1>, DataStoreError$1>;
|
|
926
960
|
readonly getRunsByProject: () => Effect.Effect<ReadonlyArray<ProjectRunSummary>, DataStoreError$1>;
|
|
961
|
+
/** How often the most recent runs wrote stray output; see {@link StrayOutputHistory}. */
|
|
962
|
+
readonly getStrayOutputHistory: (options?: StrayOutputHistoryOptions) => Effect.Effect<StrayOutputHistory, DataStoreError$1>;
|
|
927
963
|
readonly getHistory: (project: string, options?: HistoryQueryOptions) => Effect.Effect<HistoryRecord$1, DataStoreError$1>;
|
|
928
964
|
readonly getBaselines: (project: string) => Effect.Effect<Option.Option<CoverageBaselines$1>, DataStoreError$1>;
|
|
929
965
|
readonly getTrends: (project: string, limit?: number) => Effect.Effect<Option.Option<TrendRecord$1>, DataStoreError$1>;
|
|
@@ -1114,6 +1150,7 @@ export declare const PROJECT_MIGRATIONS: {
|
|
|
1114
1150
|
readonly "0001_initial": import("effect/Effect").Effect<void, import("effect/sql/SqlError").SqlError, import("effect/sql/SqlClient").SqlClient>;
|
|
1115
1151
|
readonly "0002_test_artifacts": import("effect/Effect").Effect<void, import("effect/sql/SqlError").SqlError, import("effect/sql/SqlClient").SqlClient>;
|
|
1116
1152
|
readonly "0003_run_retention": import("effect/Effect").Effect<void, import("effect/sql/SqlError").SqlError, import("effect/sql/SqlClient").SqlClient>;
|
|
1153
|
+
readonly "0004_stray_output": import("effect/Effect").Effect<void, import("effect/sql/SqlError").SqlError, import("effect/sql/SqlClient").SqlClient>;
|
|
1117
1154
|
};
|
|
1118
1155
|
/** @public */
|
|
1119
1156
|
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>;
|
|
@@ -1129,5 +1166,5 @@ export declare function withFailures(filename: string): Layer.Layer<DataReader |
|
|
|
1129
1166
|
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>;
|
|
1130
1167
|
/** @public */
|
|
1131
1168
|
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>;
|
|
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 };
|
|
1169
|
+
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 StrayOutputHistory, type StrayOutputHistoryOptions, 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 };
|
|
1133
1170
|
//# sourceMappingURL=testing.d.ts.map
|
package/version.js
CHANGED