@vitest-agent/sdk 1.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.
Files changed (121) hide show
  1. package/DataReader-2kJ9I9Hh.d.ts +1809 -0
  2. package/LICENSE +21 -0
  3. package/README.md +52 -0
  4. package/dispatch.d.ts +48 -0
  5. package/dispatch.js +5 -0
  6. package/errors/AgentErrors.js +65 -0
  7. package/errors/DataStoreError.js +44 -0
  8. package/errors/DiscoveryError.js +21 -0
  9. package/errors/PathResolutionError.js +27 -0
  10. package/errors/ProjectIdentityError.js +29 -0
  11. package/errors/RunContextError.js +35 -0
  12. package/errors/TddErrors.js +66 -0
  13. package/exit-code-for-tag.js +18 -0
  14. package/formatters/ci-annotations.js +61 -0
  15. package/formatters/gfm.js +20 -0
  16. package/formatters/json.js +18 -0
  17. package/formatters/markdown.js +39 -0
  18. package/formatters/silent.js +9 -0
  19. package/formatters/terminal.js +56 -0
  20. package/index.d.ts +3334 -0
  21. package/index.js +120 -0
  22. package/internal-inject-env.js +42 -0
  23. package/layers/ConfigLive.js +47 -0
  24. package/layers/DataReaderLive.js +1666 -0
  25. package/layers/DataStoreLive.js +1185 -0
  26. package/layers/DetailResolverLive.js +9 -0
  27. package/layers/DiscoveryRegistryLive.js +70 -0
  28. package/layers/EnvironmentDetectorLive.js +21 -0
  29. package/layers/EnvironmentDetectorTest.js +13 -0
  30. package/layers/ExecutorResolverLive.js +9 -0
  31. package/layers/FormatSelectorLive.js +20 -0
  32. package/layers/HistoryTrackerLive.js +47 -0
  33. package/layers/HistoryTrackerTest.js +25 -0
  34. package/layers/LoggerLive.js +64 -0
  35. package/layers/OutputPipelineLive.js +13 -0
  36. package/layers/OutputRendererLive.js +28 -0
  37. package/layers/PathResolutionLive.js +36 -0
  38. package/layers/PerClientSessionMapLive.js +145 -0
  39. package/layers/ProjectDiscoveryLive.js +70 -0
  40. package/layers/ProjectDiscoveryTest.js +12 -0
  41. package/layers/ProjectIdentityLive.js +92 -0
  42. package/layers/RunContextLive.js +81 -0
  43. package/lib/format-triage.js +103 -0
  44. package/lib/format-wrapup.js +67 -0
  45. package/migrations/0001_initial.js +849 -0
  46. package/migrations/registry_0001_initial.js +39 -0
  47. package/migrations/session_map_0001_initial.js +61 -0
  48. package/package.json +64 -0
  49. package/schemas/Agent.js +42 -0
  50. package/schemas/AgentReport.js +71 -0
  51. package/schemas/Baselines.js +16 -0
  52. package/schemas/CacheManifest.js +27 -0
  53. package/schemas/ChannelEvent.js +138 -0
  54. package/schemas/Common.js +105 -0
  55. package/schemas/Config.js +28 -0
  56. package/schemas/Coverage.js +51 -0
  57. package/schemas/CoverageLevel.js +117 -0
  58. package/schemas/CoverageTargets.js +39 -0
  59. package/schemas/History.js +31 -0
  60. package/schemas/Identity.js +64 -0
  61. package/schemas/Options.js +57 -0
  62. package/schemas/RenderState.js +161 -0
  63. package/schemas/RunEvent.js +163 -0
  64. package/schemas/Tdd.js +44 -0
  65. package/schemas/Thresholds.js +30 -0
  66. package/schemas/Transport.js +16 -0
  67. package/schemas/Trends.js +23 -0
  68. package/schemas/turns/FileEditPayload.js +15 -0
  69. package/schemas/turns/HookFirePayload.js +13 -0
  70. package/schemas/turns/HypothesisPayload.js +13 -0
  71. package/schemas/turns/NotePayload.js +13 -0
  72. package/schemas/turns/ToolCallPayload.js +13 -0
  73. package/schemas/turns/ToolResultPayload.js +15 -0
  74. package/schemas/turns/UserPromptPayload.js +12 -0
  75. package/schemas/turns/index.js +15 -0
  76. package/services/Config.js +15 -0
  77. package/services/DataReader.js +8 -0
  78. package/services/DataStore.js +8 -0
  79. package/services/DetailResolver.js +8 -0
  80. package/services/DiscoveryRegistry.js +8 -0
  81. package/services/EnvironmentDetector.js +8 -0
  82. package/services/ExecutorResolver.js +8 -0
  83. package/services/FormatSelector.js +8 -0
  84. package/services/HistoryTracker.js +8 -0
  85. package/services/OutputRenderer.js +8 -0
  86. package/services/PerClientSessionMap.js +22 -0
  87. package/services/ProjectDiscovery.js +8 -0
  88. package/services/ProjectIdentity.js +63 -0
  89. package/services/RunContext.js +53 -0
  90. package/services/idempotency.js +57 -0
  91. package/sidecar-dispatch.js +84 -0
  92. package/sql/assemblers.js +59 -0
  93. package/testing/layers.js +21 -0
  94. package/testing.d.ts +25 -0
  95. package/testing.js +243 -0
  96. package/tsdoc-metadata.json +11 -0
  97. package/utils/ansi.js +49 -0
  98. package/utils/build-report.js +130 -0
  99. package/utils/canonicalize-git-url.js +66 -0
  100. package/utils/classify-test.js +22 -0
  101. package/utils/compress-lines.js +45 -0
  102. package/utils/compute-trend.js +88 -0
  103. package/utils/detect-pm.js +58 -0
  104. package/utils/detect-timeout.js +17 -0
  105. package/utils/ensure-migrated.js +52 -0
  106. package/utils/failure-signature.js +33 -0
  107. package/utils/format-console.js +219 -0
  108. package/utils/format-fatal-error.js +35 -0
  109. package/utils/format-gfm.js +161 -0
  110. package/utils/format-terminal.js +480 -0
  111. package/utils/function-boundary.js +75 -0
  112. package/utils/hyperlink.js +11 -0
  113. package/utils/match-vitest-command.js +88 -0
  114. package/utils/normalize-workspace-key.js +34 -0
  115. package/utils/probe-host-metadata.js +99 -0
  116. package/utils/resolve-data-path.js +66 -0
  117. package/utils/resolve-project-key-from-cwd.js +52 -0
  118. package/utils/resolve-workspace-key.js +33 -0
  119. package/utils/safe-filename.js +34 -0
  120. package/utils/validate-coverage-targets-shape.js +84 -0
  121. package/utils/validate-phase-transition.js +113 -0
@@ -0,0 +1,21 @@
1
+ import { DataReaderLive } from "../layers/DataReaderLive.js";
2
+ import { DataStoreLive } from "../layers/DataStoreLive.js";
3
+ import migration from "../migrations/0001_initial.js";
4
+ import { Layer } from "effect";
5
+ import * as NodeContext from "@effect/platform-node/NodeContext";
6
+ import { layer } from "@effect/sql-sqlite-node/SqliteClient";
7
+ import * as SqliteMigrator from "@effect/sql-sqlite-node/SqliteMigrator";
8
+
9
+ //#region src/testing/layers.ts
10
+ /** @public */
11
+ function makeTestLayer(filename) {
12
+ const SqliteLayer = layer({ filename });
13
+ const PlatformLayer = NodeContext.layer;
14
+ const MigratorLayer = SqliteMigrator.layer({ loader: SqliteMigrator.fromRecord({ "0001_initial": migration }) }).pipe(Layer.provide(Layer.merge(SqliteLayer, PlatformLayer)));
15
+ return Layer.mergeAll(DataStoreLive.pipe(Layer.provide(SqliteLayer)), DataReaderLive.pipe(Layer.provide(SqliteLayer)), MigratorLayer, SqliteLayer, PlatformLayer);
16
+ }
17
+ /** @public */
18
+ const DataStoreTestLayer = makeTestLayer(":memory:");
19
+
20
+ //#endregion
21
+ export { DataStoreTestLayer, makeTestLayer };
package/testing.d.ts ADDED
@@ -0,0 +1,25 @@
1
+ import { Ft as IllegalStatusTransitionError, It as TddTaskAlreadyEndedError, Lt as TddTaskEndOutcome, Mt as BehaviorNotFoundError, Nt as GoalNotFoundError, Pt as IllegalStatusTransitionEntity, Rt as TddTaskNotFoundError, a as DataReader, j as DataStore, zt as DataStoreError } from "./DataReader-2kJ9I9Hh.js";
2
+ import { Layer } from "effect";
3
+ import * as NodeContext from "@effect/platform-node/NodeContext";
4
+ import * as SqliteMigrator from "@effect/sql-sqlite-node/SqliteMigrator";
5
+
6
+ //#region src/testing/layers.d.ts
7
+ /** @public */
8
+ declare function makeTestLayer(filename: string): Layer.Layer<DataStore | import("@effect/sql/SqlClient").SqlClient | import("@effect/sql-sqlite-node/SqliteClient").SqliteClient | DataReader | NodeContext.NodeContext, import("effect/ConfigError").ConfigError | import("@effect/sql/SqlError").SqlError | SqliteMigrator.MigrationError, never>;
9
+ /** @public */
10
+ declare const DataStoreTestLayer: Layer.Layer<DataStore | import("@effect/sql/SqlClient").SqlClient | import("@effect/sql-sqlite-node/SqliteClient").SqliteClient | DataReader | NodeContext.NodeContext, import("effect/ConfigError").ConfigError | import("@effect/sql/SqlError").SqlError | SqliteMigrator.MigrationError, never>;
11
+ //#endregion
12
+ //#region src/testing/index.d.ts
13
+ /** @public */
14
+ declare function empty(filename: string): Layer.Layer<DataStore | import("@effect/sql/SqlClient").SqlClient | import("@effect/sql-sqlite-node/SqliteClient").SqliteClient | DataReader | import("@effect/platform-node/NodeContext").NodeContext, import("effect/ConfigError").ConfigError | import("@effect/sql/SqlError").SqlError | import("@effect/sql/Migrator").MigrationError, never>;
15
+ /** @public */
16
+ declare function singlePassingRun(filename: string): Layer.Layer<DataStore | import("@effect/sql/SqlClient").SqlClient | import("@effect/sql-sqlite-node/SqliteClient").SqliteClient | DataReader | import("@effect/platform-node/NodeContext").NodeContext, import("effect/ConfigError").ConfigError | import("@effect/sql/SqlError").SqlError | import("@effect/sql/Migrator").MigrationError | DataStoreError, never>;
17
+ /** @public */
18
+ declare function withFailures(filename: string): Layer.Layer<DataStore | import("@effect/sql/SqlClient").SqlClient | import("@effect/sql-sqlite-node/SqliteClient").SqliteClient | DataReader | import("@effect/platform-node/NodeContext").NodeContext, import("effect/ConfigError").ConfigError | import("@effect/sql/SqlError").SqlError | import("@effect/sql/Migrator").MigrationError | DataStoreError, never>;
19
+ /** @public */
20
+ declare function flaky(filename: string): Layer.Layer<DataStore | import("@effect/sql/SqlClient").SqlClient | import("@effect/sql-sqlite-node/SqliteClient").SqliteClient | DataReader | import("@effect/platform-node/NodeContext").NodeContext, import("effect/ConfigError").ConfigError | import("@effect/sql/SqlError").SqlError | import("@effect/sql/Migrator").MigrationError | DataStoreError, never>;
21
+ /** @public */
22
+ declare function withTddTask(filename: string): Layer.Layer<DataStore | import("@effect/sql/SqlClient").SqlClient | import("@effect/sql-sqlite-node/SqliteClient").SqliteClient | DataReader | import("@effect/platform-node/NodeContext").NodeContext, import("effect/ConfigError").ConfigError | import("@effect/sql/SqlError").SqlError | import("@effect/sql/Migrator").MigrationError | DataStoreError | TddTaskNotFoundError | TddTaskAlreadyEndedError | GoalNotFoundError | IllegalStatusTransitionError | BehaviorNotFoundError, never>;
23
+ //#endregion
24
+ export { BehaviorNotFoundError, DataReader, DataStore, DataStoreError, DataStoreTestLayer, GoalNotFoundError, type IllegalStatusTransitionEntity, IllegalStatusTransitionError, TddTaskAlreadyEndedError, type TddTaskEndOutcome, TddTaskNotFoundError, empty, flaky, makeTestLayer, singlePassingRun, withFailures, withTddTask };
25
+ //# sourceMappingURL=testing.d.ts.map
package/testing.js ADDED
@@ -0,0 +1,243 @@
1
+ import { DataStoreError } from "./errors/DataStoreError.js";
2
+ import { BehaviorNotFoundError, GoalNotFoundError, IllegalStatusTransitionError, TddTaskAlreadyEndedError, TddTaskNotFoundError } from "./errors/TddErrors.js";
3
+ import { DataReader } from "./services/DataReader.js";
4
+ import { DataStore } from "./services/DataStore.js";
5
+ import { DataStoreTestLayer, makeTestLayer } from "./testing/layers.js";
6
+ import { Effect, Layer } from "effect";
7
+
8
+ //#region src/testing/index.ts
9
+ /** @public */
10
+ function empty(filename) {
11
+ return makeTestLayer(filename);
12
+ }
13
+ /** @public */
14
+ function singlePassingRun(filename) {
15
+ const base = makeTestLayer(filename);
16
+ const seed = Effect.gen(function* () {
17
+ const store = yield* DataStore;
18
+ yield* store.writeSettings("hash-preset-spr", {
19
+ vitestVersion: "4.1.5",
20
+ pool: "forks"
21
+ }, {});
22
+ const runId = yield* store.writeRun({
23
+ invocationId: "inv-preset-spr-1",
24
+ project: "default",
25
+ settingsHash: "hash-preset-spr",
26
+ timestamp: "2026-01-01T00:00:00.000Z",
27
+ commitSha: null,
28
+ branch: null,
29
+ reason: "passed",
30
+ duration: 1200,
31
+ total: 3,
32
+ passed: 3,
33
+ failed: 0,
34
+ skipped: 0,
35
+ scoped: false
36
+ });
37
+ const fileId = yield* store.ensureFile("src/math.test.ts");
38
+ const [moduleId] = yield* store.writeModules(runId, [{
39
+ fileId,
40
+ relativeModuleId: "src/math.test.ts",
41
+ state: "passed",
42
+ duration: 1200
43
+ }]);
44
+ yield* store.writeTestCases(moduleId, [
45
+ {
46
+ name: "adds two numbers",
47
+ fullName: "math > adds two numbers",
48
+ state: "passed",
49
+ duration: 400
50
+ },
51
+ {
52
+ name: "subtracts two numbers",
53
+ fullName: "math > subtracts two numbers",
54
+ state: "passed",
55
+ duration: 400
56
+ },
57
+ {
58
+ name: "multiplies two numbers",
59
+ fullName: "math > multiplies two numbers",
60
+ state: "passed",
61
+ duration: 400
62
+ }
63
+ ]);
64
+ });
65
+ return Layer.effectDiscard(seed).pipe(Layer.provideMerge(base));
66
+ }
67
+ /** @public */
68
+ function withFailures(filename) {
69
+ const base = makeTestLayer(filename);
70
+ const seed = Effect.gen(function* () {
71
+ const store = yield* DataStore;
72
+ yield* store.writeSettings("hash-preset-wf", {
73
+ vitestVersion: "4.1.5",
74
+ pool: "forks"
75
+ }, {});
76
+ const runId = yield* store.writeRun({
77
+ invocationId: "inv-preset-wf-1",
78
+ project: "default",
79
+ settingsHash: "hash-preset-wf",
80
+ timestamp: "2026-01-01T00:00:00.000Z",
81
+ commitSha: null,
82
+ branch: null,
83
+ reason: "failed",
84
+ duration: 1500,
85
+ total: 4,
86
+ passed: 2,
87
+ failed: 2,
88
+ skipped: 0,
89
+ scoped: false
90
+ });
91
+ const fileId = yield* store.ensureFile("src/parser.test.ts");
92
+ const [moduleId] = yield* store.writeModules(runId, [{
93
+ fileId,
94
+ relativeModuleId: "src/parser.test.ts",
95
+ state: "failed",
96
+ duration: 1500
97
+ }]);
98
+ yield* store.writeTestCases(moduleId, [
99
+ {
100
+ name: "parses valid input",
101
+ fullName: "parser > parses valid input",
102
+ state: "passed",
103
+ duration: 300
104
+ },
105
+ {
106
+ name: "parses nested structure",
107
+ fullName: "parser > parses nested structure",
108
+ state: "passed",
109
+ duration: 300
110
+ },
111
+ {
112
+ name: "rejects empty input",
113
+ fullName: "parser > rejects empty input",
114
+ state: "failed",
115
+ duration: 450
116
+ },
117
+ {
118
+ name: "rejects malformed input",
119
+ fullName: "parser > rejects malformed input",
120
+ state: "failed",
121
+ duration: 450
122
+ }
123
+ ]);
124
+ });
125
+ return Layer.effectDiscard(seed).pipe(Layer.provideMerge(base));
126
+ }
127
+ /** @public */
128
+ function flaky(filename) {
129
+ const base = makeTestLayer(filename);
130
+ const seed = Effect.gen(function* () {
131
+ const store = yield* DataStore;
132
+ yield* store.writeSettings("hash-preset-flaky", {
133
+ vitestVersion: "4.1.5",
134
+ pool: "forks"
135
+ }, {});
136
+ const runId1 = yield* store.writeRun({
137
+ invocationId: "inv-preset-flaky-1",
138
+ project: "default",
139
+ settingsHash: "hash-preset-flaky",
140
+ timestamp: "2026-01-01T00:00:00.000Z",
141
+ commitSha: null,
142
+ branch: null,
143
+ reason: "failed",
144
+ duration: 800,
145
+ total: 2,
146
+ passed: 1,
147
+ failed: 1,
148
+ skipped: 0,
149
+ scoped: false
150
+ });
151
+ const fileId = yield* store.ensureFile("src/async.test.ts");
152
+ const [moduleId1] = yield* store.writeModules(runId1, [{
153
+ fileId,
154
+ relativeModuleId: "src/async.test.ts",
155
+ state: "failed",
156
+ duration: 800
157
+ }]);
158
+ yield* store.writeTestCases(moduleId1, [{
159
+ name: "resolves within timeout",
160
+ fullName: "async > resolves within timeout",
161
+ state: "failed",
162
+ duration: 500
163
+ }, {
164
+ name: "rejects on error",
165
+ fullName: "async > rejects on error",
166
+ state: "passed",
167
+ duration: 300
168
+ }]);
169
+ yield* store.writeHistory("default", "async > resolves within timeout", runId1, "2025-12-31T23:00:00.000Z", "passed", 500, false, 0, null);
170
+ yield* store.writeHistory("default", "async > resolves within timeout", runId1, "2026-01-01T00:00:00.000Z", "failed", 500, false, 0, null);
171
+ yield* store.writeHistory("default", "async > rejects on error", runId1, "2026-01-01T00:00:00.000Z", "passed", 300, false, 0, null);
172
+ const runId2 = yield* store.writeRun({
173
+ invocationId: "inv-preset-flaky-2",
174
+ project: "default",
175
+ settingsHash: "hash-preset-flaky",
176
+ timestamp: "2026-01-01T01:00:00.000Z",
177
+ commitSha: null,
178
+ branch: null,
179
+ reason: "passed",
180
+ duration: 700,
181
+ total: 2,
182
+ passed: 2,
183
+ failed: 0,
184
+ skipped: 0,
185
+ scoped: false
186
+ });
187
+ const [moduleId2] = yield* store.writeModules(runId2, [{
188
+ fileId,
189
+ relativeModuleId: "src/async.test.ts",
190
+ state: "passed",
191
+ duration: 700
192
+ }]);
193
+ yield* store.writeTestCases(moduleId2, [{
194
+ name: "resolves within timeout",
195
+ fullName: "async > resolves within timeout",
196
+ state: "passed",
197
+ duration: 400
198
+ }, {
199
+ name: "rejects on error",
200
+ fullName: "async > rejects on error",
201
+ state: "passed",
202
+ duration: 300
203
+ }]);
204
+ yield* store.writeHistory("default", "async > resolves within timeout", runId2, "2026-01-01T01:00:00.000Z", "passed", 400, false, 0, null);
205
+ yield* store.writeHistory("default", "async > rejects on error", runId2, "2026-01-01T01:00:00.000Z", "passed", 300, false, 0, null);
206
+ });
207
+ return Layer.effectDiscard(seed).pipe(Layer.provideMerge(base));
208
+ }
209
+ /** @public */
210
+ function withTddTask(filename) {
211
+ const base = makeTestLayer(filename);
212
+ const seed = Effect.gen(function* () {
213
+ const store = yield* DataStore;
214
+ const sessionId = yield* store.writeSession({
215
+ chatId: "chat-preset-tdd",
216
+ project: "default",
217
+ cwd: "/workspace",
218
+ agentKind: "main",
219
+ startedAt: "2026-01-01T00:00:00.000Z"
220
+ });
221
+ const tddTaskId = yield* store.writeTddTask({
222
+ sessionId,
223
+ goal: "Implement the parser feature",
224
+ startedAt: "2026-01-01T00:00:00.000Z"
225
+ });
226
+ const goal = yield* store.createGoal({
227
+ tddTaskId,
228
+ goal: "Handle edge cases"
229
+ });
230
+ yield* store.createBehavior({
231
+ goalId: goal.id,
232
+ behavior: "rejects empty input"
233
+ });
234
+ yield* store.createBehavior({
235
+ goalId: goal.id,
236
+ behavior: "rejects null input"
237
+ });
238
+ });
239
+ return Layer.effectDiscard(seed).pipe(Layer.provideMerge(base));
240
+ }
241
+
242
+ //#endregion
243
+ export { BehaviorNotFoundError, DataReader, DataStore, DataStoreError, DataStoreTestLayer, GoalNotFoundError, IllegalStatusTransitionError, TddTaskAlreadyEndedError, TddTaskNotFoundError, empty, flaky, makeTestLayer, singlePassingRun, withFailures, withTddTask };
@@ -0,0 +1,11 @@
1
+ // This file is read by tools that parse documentation comments conforming to the TSDoc standard.
2
+ // It should be published with your NPM package. It should not be tracked by Git.
3
+ {
4
+ "tsdocVersion": "0.12",
5
+ "toolPackages": [
6
+ {
7
+ "packageName": "@microsoft/api-extractor",
8
+ "packageVersion": "7.58.9"
9
+ }
10
+ ]
11
+ }
package/utils/ansi.js ADDED
@@ -0,0 +1,49 @@
1
+ //#region src/utils/ansi.ts
2
+ const ANSI_CODES = {
3
+ reset: [0, 0],
4
+ bold: [1, 22],
5
+ dim: [2, 22],
6
+ red: [31, 39],
7
+ green: [32, 39],
8
+ yellow: [33, 39],
9
+ cyan: [36, 39]
10
+ };
11
+ /**
12
+ * Wrap text with an ANSI color/style escape code.
13
+ *
14
+ * No-ops when `options.noColor` is true, keeping output as valid plain
15
+ * markdown when captured by agents or piped to files.
16
+ *
17
+ * @privateRemarks
18
+ * Uses raw ANSI escape sequences (`\x1b[...m`) rather than a dependency
19
+ * like `chalk` or `picocolors` to keep the package zero-dependency. The
20
+ * `noColor` flag respects the `NO_COLOR` environment variable convention.
21
+ *
22
+ * @param text - Text to wrap
23
+ * @param color - ANSI code name: `reset`, `bold`, `dim`, `red`, `green`, `yellow`, `cyan`
24
+ * @param options - Control color output
25
+ * @returns Text with ANSI escape codes, or plain text if noColor
26
+ *
27
+ * @see {@link https://no-color.org/ | NO_COLOR convention}
28
+ * @internal
29
+ */
30
+ function ansi(text, color, options) {
31
+ if (options?.noColor) return text;
32
+ const codes = ANSI_CODES[color];
33
+ if (!codes) return text;
34
+ return `\x1b[${codes[0]}m${text}\x1b[${codes[1]}m`;
35
+ }
36
+ /**
37
+ * Strip all ANSI escape codes from a string.
38
+ *
39
+ * @param text - String potentially containing ANSI escape sequences
40
+ * @returns Plain text with all escape sequences removed
41
+ *
42
+ * @internal
43
+ */
44
+ function stripAnsi(text) {
45
+ return text.replace(/\x1b\[[0-9;]*m/g, "");
46
+ }
47
+
48
+ //#endregion
49
+ export { ansi, stripAnsi };
@@ -0,0 +1,130 @@
1
+ //#region src/utils/build-report.ts
2
+ /**
3
+ * Format a single Vitest stack frame: strings pass through; ParsedStack
4
+ * objects render as `at <method> (<file>:<line>:<column>)`. An empty
5
+ * `method` falls back to `<anonymous>`.
6
+ *
7
+ * @internal
8
+ */
9
+ function formatStackFrame(frame) {
10
+ if (typeof frame === "string") return frame;
11
+ return `at ${frame.method.length > 0 ? frame.method : "<anonymous>"} (${frame.file}:${frame.line}:${frame.column})`;
12
+ }
13
+ /**
14
+ * Map Vitest error objects to the {@link ReportError} shape.
15
+ *
16
+ * @internal
17
+ */
18
+ function mapErrors(errors) {
19
+ if (!errors || errors.length === 0) return void 0;
20
+ return errors.map((e) => {
21
+ return {
22
+ message: e.message,
23
+ ..."diff" in e && e.diff != null ? { diff: e.diff } : {},
24
+ ...e.stacks && e.stacks.length > 0 ? { stack: e.stacks.map(formatStackFrame).join("\n") } : {}
25
+ };
26
+ });
27
+ }
28
+ /**
29
+ * Normalize a Vitest state string to one of the four canonical states.
30
+ *
31
+ * @internal
32
+ */
33
+ function normalizeState(state) {
34
+ if (state === "passed") return "passed";
35
+ if (state === "failed") return "failed";
36
+ if (state === "skipped") return "skipped";
37
+ return "pending";
38
+ }
39
+ /**
40
+ * Convert Vitest TestModule/TestCase objects into an `AgentReport`.
41
+ *
42
+ * @remarks
43
+ * This is a pure data transformation function with no I/O. It tallies
44
+ * pass/fail/skip counts, extracts error details with diffs, and builds
45
+ * the complete report structure. Only modules with at least one failing
46
+ * test are included in the `failed` array to keep reports compact.
47
+ *
48
+ * @param testModules - All test modules from the Vitest run
49
+ * @param unhandledErrors - Unhandled errors from the run
50
+ * @param reason - Overall run outcome (`"passed"`, `"failed"`, or `"interrupted"`)
51
+ * @param options - Reporter options; only `omitPassingTests` is used
52
+ * @param projectName - Optional project name for monorepo grouping
53
+ * @returns Structured report ready for JSON serialization
54
+ *
55
+ * @public
56
+ */
57
+ function buildAgentReport(testModules, unhandledErrors, reason, options, projectName) {
58
+ const omitPassing = options.omitPassingTests !== false;
59
+ let totalCount = 0;
60
+ let passedCount = 0;
61
+ let failedCount = 0;
62
+ let skippedCount = 0;
63
+ let totalDuration = 0;
64
+ const failedModules = [];
65
+ const failedFiles = [];
66
+ for (const testModule of testModules) {
67
+ const moduleDuration = testModule.diagnostic()?.duration ?? 0;
68
+ totalDuration += moduleDuration;
69
+ const moduleState = normalizeState(testModule.state());
70
+ const moduleErrors = mapErrors(testModule.errors());
71
+ const allTests = [...testModule.children.allTests()];
72
+ const testReports = [];
73
+ let moduleHasFailure = false;
74
+ for (const testCase of allTests) {
75
+ totalCount++;
76
+ const result = testCase.result();
77
+ const diag = testCase.diagnostic();
78
+ const state = normalizeState(result?.state ?? "pending");
79
+ if (state === "passed") passedCount++;
80
+ else if (state === "failed") {
81
+ failedCount++;
82
+ moduleHasFailure = true;
83
+ } else if (state === "skipped") skippedCount++;
84
+ else skippedCount++;
85
+ if (!omitPassing || state !== "passed") {
86
+ const errors = mapErrors(result?.errors);
87
+ const testReport = {
88
+ name: testCase.name,
89
+ fullName: testCase.fullName,
90
+ state,
91
+ ...diag?.duration !== void 0 ? { duration: diag.duration } : {},
92
+ ...diag?.flaky ? { flaky: true } : {},
93
+ ...diag?.slow ? { slow: true } : {},
94
+ ...errors ? { errors } : {}
95
+ };
96
+ testReports.push(testReport);
97
+ }
98
+ }
99
+ if (moduleHasFailure || !moduleHasFailure && moduleState === "failed" && moduleErrors !== void 0) {
100
+ failedFiles.push(testModule.relativeModuleId);
101
+ const moduleReport = {
102
+ file: testModule.relativeModuleId,
103
+ state: moduleState,
104
+ duration: moduleDuration,
105
+ tests: testReports,
106
+ ...moduleErrors ? { errors: moduleErrors } : {}
107
+ };
108
+ failedModules.push(moduleReport);
109
+ }
110
+ }
111
+ const mappedUnhandled = mapErrors(unhandledErrors) ?? [];
112
+ return {
113
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
114
+ ...projectName != null ? { project: projectName } : {},
115
+ reason,
116
+ summary: {
117
+ total: totalCount,
118
+ passed: passedCount,
119
+ failed: failedCount,
120
+ skipped: skippedCount,
121
+ duration: totalDuration
122
+ },
123
+ failed: failedModules,
124
+ unhandledErrors: mappedUnhandled,
125
+ failedFiles
126
+ };
127
+ }
128
+
129
+ //#endregion
130
+ export { buildAgentReport };
@@ -0,0 +1,66 @@
1
+ //#region src/utils/canonicalize-git-url.ts
2
+ const SCHEME_PREFIXES = [
3
+ "git+ssh://",
4
+ "git+https://",
5
+ "ssh://",
6
+ "https://",
7
+ "http://"
8
+ ];
9
+ const SSH_SHORTHAND_REGEX = /^(?:[\w.-]+@)?([\w.-]+):(.+)$/;
10
+ /**
11
+ * Canonicalize a git remote URL.
12
+ *
13
+ * Returns `null` when the input is empty, whitespace-only, or has
14
+ * neither a host nor a path after stripping known prefixes.
15
+ *
16
+ * Steps applied in order:
17
+ *
18
+ * 1. Trim whitespace.
19
+ * 2. If the value starts with `[git+]ssh|https|http://`, strip the
20
+ * scheme. After stripping, drop any embedded `user@` segment.
21
+ * 3. Otherwise, try the SSH shorthand `user@host:path` pattern; on a
22
+ * match, rebuild as `host/path` (drop the user).
23
+ * 4. Strip trailing `.git`.
24
+ * 5. Strip trailing `/`.
25
+ * 6. Lowercase the entire string.
26
+ * 7. Reject if the result is empty or has no `/` (no path) — a bare
27
+ * host like `github.com` is not a project identity.
28
+ * @public
29
+ */
30
+ const canonicalizeGitUrl = (raw) => {
31
+ const trimmed = raw.trim();
32
+ if (trimmed.length === 0) return null;
33
+ let value = trimmed;
34
+ const matchedPrefix = SCHEME_PREFIXES.find((prefix) => value.startsWith(prefix));
35
+ if (matchedPrefix !== void 0) {
36
+ value = value.slice(matchedPrefix.length);
37
+ const atIndex = value.indexOf("@");
38
+ const slashIndex = value.indexOf("/");
39
+ if (atIndex !== -1 && (slashIndex === -1 || atIndex < slashIndex)) value = value.slice(atIndex + 1);
40
+ } else {
41
+ const sshMatch = value.match(SSH_SHORTHAND_REGEX);
42
+ if (sshMatch !== null) value = `${sshMatch[1]}/${sshMatch[2]}`;
43
+ }
44
+ if (value.endsWith(".git")) value = value.slice(0, -4);
45
+ if (value.endsWith("/")) value = value.slice(0, -1);
46
+ value = value.toLowerCase();
47
+ if (value.length === 0 || !value.includes("/")) return null;
48
+ const slashIndex = value.indexOf("/");
49
+ if (slashIndex === 0 || slashIndex === value.length - 1) return null;
50
+ return value;
51
+ };
52
+ /**
53
+ * Convert a (possibly raw) git URL to the filesystem-safe `project_key`
54
+ * form. The canonical `host/path` is mapped 1:1 to `host__path` so it
55
+ * can serve as a single path segment under
56
+ * `$XDG_DATA_HOME/vitest-agent/`.
57
+ * @public
58
+ */
59
+ const gitUrlToProjectKey = (raw) => {
60
+ const canonical = canonicalizeGitUrl(raw);
61
+ if (canonical === null) return null;
62
+ return canonical.replaceAll("/", "__");
63
+ };
64
+
65
+ //#endregion
66
+ export { canonicalizeGitUrl, gitUrlToProjectKey };
@@ -0,0 +1,22 @@
1
+ //#region src/utils/classify-test.ts
2
+ /**
3
+ * Classify a test based on its current state and prior run history.
4
+ *
5
+ * @param current - The test's state in the current run
6
+ * @param priorRuns - Previous runs (most recent first), before the current run was prepended
7
+ * @returns The test's classification
8
+ * @public
9
+ */
10
+ function classifyTest(current, priorRuns) {
11
+ if (current === "failed") {
12
+ if (priorRuns.length === 0) return "new-failure";
13
+ if (priorRuns.every((r) => r.state === "passed")) return "new-failure";
14
+ if (priorRuns[0].state === "failed") return "persistent";
15
+ return "flaky";
16
+ }
17
+ if (priorRuns.length === 0) return "stable";
18
+ return priorRuns.some((r) => r.state === "failed") ? "recovered" : "stable";
19
+ }
20
+
21
+ //#endregion
22
+ export { classifyTest };
@@ -0,0 +1,45 @@
1
+ //#region src/utils/compress-lines.ts
2
+ /**
3
+ * Compress an array of line numbers into a compact range string.
4
+ *
5
+ * Consecutive line numbers are collapsed into `start-end` ranges,
6
+ * separated by commas. Duplicate values are removed and the input
7
+ * is sorted ascending before processing.
8
+ *
9
+ * @param lines - Array of line numbers to compress
10
+ * @returns Compressed range string, or empty string for empty input
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * import { compressLines } from "@vitest-agent/sdk/utils";
15
+ *
16
+ * compressLines([1, 2, 3, 5, 10, 11, 12]);
17
+ * // Returns: "1-3,5,10-12"
18
+ *
19
+ * compressLines([42]);
20
+ * // Returns: "42"
21
+ *
22
+ * compressLines([]);
23
+ * // Returns: ""
24
+ * ```
25
+ *
26
+ * @public
27
+ */
28
+ function compressLines(lines) {
29
+ if (lines.length === 0) return "";
30
+ const sorted = [...new Set(lines)].sort((a, b) => a - b);
31
+ const ranges = [];
32
+ let start = sorted[0];
33
+ let end = sorted[0];
34
+ for (let i = 1; i < sorted.length; i++) if (sorted[i] === end + 1) end = sorted[i];
35
+ else {
36
+ ranges.push(start === end ? `${start}` : `${start}-${end}`);
37
+ start = sorted[i];
38
+ end = sorted[i];
39
+ }
40
+ ranges.push(start === end ? `${start}` : `${start}-${end}`);
41
+ return ranges.join(",");
42
+ }
43
+
44
+ //#endregion
45
+ export { compressLines };