@vitest-agent/mcp 2.4.15 → 3.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 +102 -0
- package/index.js +1 -1
- package/layers/McpLive.js +2 -2
- package/package.json +3 -3
- package/server.js +19 -4
- package/tools/errors.js +46 -5
- package/tools/help.js +6 -3
- package/tools/run-tests.js +53 -11
- package/tools/test.js +161 -14
package/index.d.ts
CHANGED
|
@@ -213,6 +213,12 @@ export declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
213
213
|
readonly functions?: number | undefined;
|
|
214
214
|
readonly branches?: number | undefined;
|
|
215
215
|
readonly statements?: number | undefined;
|
|
216
|
+
readonly perFile?: boolean | {
|
|
217
|
+
readonly lines?: number | undefined;
|
|
218
|
+
readonly functions?: number | undefined;
|
|
219
|
+
readonly branches?: number | undefined;
|
|
220
|
+
readonly statements?: number | undefined;
|
|
221
|
+
};
|
|
216
222
|
}])[];
|
|
217
223
|
};
|
|
218
224
|
readonly targets?: {
|
|
@@ -227,6 +233,12 @@ export declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
227
233
|
readonly functions?: number | undefined;
|
|
228
234
|
readonly branches?: number | undefined;
|
|
229
235
|
readonly statements?: number | undefined;
|
|
236
|
+
readonly perFile?: boolean | {
|
|
237
|
+
readonly lines?: number | undefined;
|
|
238
|
+
readonly functions?: number | undefined;
|
|
239
|
+
readonly branches?: number | undefined;
|
|
240
|
+
readonly statements?: number | undefined;
|
|
241
|
+
};
|
|
230
242
|
}])[];
|
|
231
243
|
} | undefined;
|
|
232
244
|
readonly baselines?: {
|
|
@@ -241,6 +253,12 @@ export declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
241
253
|
readonly functions?: number | undefined;
|
|
242
254
|
readonly branches?: number | undefined;
|
|
243
255
|
readonly statements?: number | undefined;
|
|
256
|
+
readonly perFile?: boolean | {
|
|
257
|
+
readonly lines?: number | undefined;
|
|
258
|
+
readonly functions?: number | undefined;
|
|
259
|
+
readonly branches?: number | undefined;
|
|
260
|
+
readonly statements?: number | undefined;
|
|
261
|
+
};
|
|
244
262
|
}])[];
|
|
245
263
|
} | undefined;
|
|
246
264
|
readonly scoped: boolean;
|
|
@@ -377,6 +395,15 @@ export declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
377
395
|
readonly scope: "module" | "suite" | "test" | "unhandled";
|
|
378
396
|
readonly testFullName: string | null;
|
|
379
397
|
readonly moduleFile: string | null;
|
|
398
|
+
readonly annotations: readonly {
|
|
399
|
+
readonly type: string;
|
|
400
|
+
readonly message: string;
|
|
401
|
+
readonly location?: {
|
|
402
|
+
readonly file: string;
|
|
403
|
+
readonly line: number;
|
|
404
|
+
readonly column: number;
|
|
405
|
+
} | undefined;
|
|
406
|
+
}[];
|
|
380
407
|
}[];
|
|
381
408
|
};
|
|
382
409
|
meta: object;
|
|
@@ -400,6 +427,18 @@ export declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
400
427
|
readonly action: "for_tag";
|
|
401
428
|
readonly tag: string;
|
|
402
429
|
readonly project?: string | undefined;
|
|
430
|
+
} | {
|
|
431
|
+
readonly action: "annotations";
|
|
432
|
+
readonly fullName: string;
|
|
433
|
+
readonly project?: string | undefined;
|
|
434
|
+
readonly modulePath?: string | undefined;
|
|
435
|
+
readonly maxBytes?: number | undefined;
|
|
436
|
+
} | {
|
|
437
|
+
readonly action: "artifacts";
|
|
438
|
+
readonly fullName: string;
|
|
439
|
+
readonly project?: string | undefined;
|
|
440
|
+
readonly modulePath?: string | undefined;
|
|
441
|
+
readonly maxBytes?: number | undefined;
|
|
403
442
|
};
|
|
404
443
|
output: {
|
|
405
444
|
readonly action: "list";
|
|
@@ -464,6 +503,51 @@ export declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
464
503
|
readonly classification: string | null;
|
|
465
504
|
}[];
|
|
466
505
|
}[];
|
|
506
|
+
} | {
|
|
507
|
+
readonly action: "annotations";
|
|
508
|
+
readonly project: string;
|
|
509
|
+
readonly fullName: string;
|
|
510
|
+
readonly count: number;
|
|
511
|
+
readonly annotations: readonly {
|
|
512
|
+
readonly id: number;
|
|
513
|
+
readonly type: string;
|
|
514
|
+
readonly message: string;
|
|
515
|
+
readonly location?: {
|
|
516
|
+
readonly file: string;
|
|
517
|
+
readonly line: number;
|
|
518
|
+
readonly column: number;
|
|
519
|
+
} | undefined;
|
|
520
|
+
readonly attachments: readonly {
|
|
521
|
+
readonly contentType?: string | undefined;
|
|
522
|
+
readonly path?: string | undefined;
|
|
523
|
+
readonly byteSize: number | null;
|
|
524
|
+
readonly body?: string | undefined;
|
|
525
|
+
readonly bodyEncoding?: "base64" | "utf-8" | undefined;
|
|
526
|
+
}[];
|
|
527
|
+
}[];
|
|
528
|
+
} | {
|
|
529
|
+
readonly action: "artifacts";
|
|
530
|
+
readonly project: string;
|
|
531
|
+
readonly fullName: string;
|
|
532
|
+
readonly count: number;
|
|
533
|
+
readonly artifacts: readonly {
|
|
534
|
+
readonly id: number;
|
|
535
|
+
readonly type: string;
|
|
536
|
+
readonly message: string | null;
|
|
537
|
+
readonly data: string | null;
|
|
538
|
+
readonly location?: {
|
|
539
|
+
readonly file: string;
|
|
540
|
+
readonly line: number;
|
|
541
|
+
readonly column: number;
|
|
542
|
+
} | undefined;
|
|
543
|
+
readonly attachments: readonly {
|
|
544
|
+
readonly contentType?: string | undefined;
|
|
545
|
+
readonly path?: string | undefined;
|
|
546
|
+
readonly byteSize: number | null;
|
|
547
|
+
readonly body?: string | undefined;
|
|
548
|
+
readonly bodyEncoding?: "base64" | "utf-8" | undefined;
|
|
549
|
+
}[];
|
|
550
|
+
}[];
|
|
467
551
|
};
|
|
468
552
|
meta: object;
|
|
469
553
|
}>;
|
|
@@ -608,6 +692,12 @@ export declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
608
692
|
readonly functions?: number | undefined;
|
|
609
693
|
readonly branches?: number | undefined;
|
|
610
694
|
readonly statements?: number | undefined;
|
|
695
|
+
readonly perFile?: boolean | {
|
|
696
|
+
readonly lines?: number | undefined;
|
|
697
|
+
readonly functions?: number | undefined;
|
|
698
|
+
readonly branches?: number | undefined;
|
|
699
|
+
readonly statements?: number | undefined;
|
|
700
|
+
};
|
|
611
701
|
}])[];
|
|
612
702
|
};
|
|
613
703
|
readonly targets?: {
|
|
@@ -622,6 +712,12 @@ export declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
622
712
|
readonly functions?: number | undefined;
|
|
623
713
|
readonly branches?: number | undefined;
|
|
624
714
|
readonly statements?: number | undefined;
|
|
715
|
+
readonly perFile?: boolean | {
|
|
716
|
+
readonly lines?: number | undefined;
|
|
717
|
+
readonly functions?: number | undefined;
|
|
718
|
+
readonly branches?: number | undefined;
|
|
719
|
+
readonly statements?: number | undefined;
|
|
720
|
+
};
|
|
625
721
|
}])[];
|
|
626
722
|
} | undefined;
|
|
627
723
|
readonly baselines?: {
|
|
@@ -636,6 +732,12 @@ export declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
636
732
|
readonly functions?: number | undefined;
|
|
637
733
|
readonly branches?: number | undefined;
|
|
638
734
|
readonly statements?: number | undefined;
|
|
735
|
+
readonly perFile?: boolean | {
|
|
736
|
+
readonly lines?: number | undefined;
|
|
737
|
+
readonly functions?: number | undefined;
|
|
738
|
+
readonly branches?: number | undefined;
|
|
739
|
+
readonly statements?: number | undefined;
|
|
740
|
+
};
|
|
639
741
|
}])[];
|
|
640
742
|
} | undefined;
|
|
641
743
|
readonly scoped: boolean;
|
package/index.js
CHANGED
|
@@ -12,7 +12,7 @@ import { parseSessionEnvExports, recoverSessionContextFromSessionEnv } from "./s
|
|
|
12
12
|
*
|
|
13
13
|
* @public
|
|
14
14
|
*/
|
|
15
|
-
const CURRENT_MCP_VERSION = "
|
|
15
|
+
const CURRENT_MCP_VERSION = "3.0.0";
|
|
16
16
|
|
|
17
17
|
//#endregion
|
|
18
18
|
export { CURRENT_MCP_VERSION, McpLive, appRouter, buildMcpServer, createCallerFactory, createCurrentSessionIdRef, createSessionContextRef, parseSessionEnvExports, recoverSessionContextFromSessionEnv, startMcpServer };
|
package/layers/McpLive.js
CHANGED
|
@@ -2,7 +2,7 @@ import { Layer } from "effect";
|
|
|
2
2
|
import * as NodeServices from "@effect/platform-node/NodeServices";
|
|
3
3
|
import { layer } from "@effect/sql-sqlite-node/SqliteClient";
|
|
4
4
|
import * as SqliteMigrator from "@effect/sql-sqlite-node/SqliteMigrator";
|
|
5
|
-
import { DataReaderLive, DataStoreLive, LoggerLive, OutputPipelineLive,
|
|
5
|
+
import { DataReaderLive, DataStoreLive, LoggerLive, OutputPipelineLive, PROJECT_MIGRATIONS, ProjectDiscoveryLive } from "@vitest-agent/sdk";
|
|
6
6
|
|
|
7
7
|
//#region src/layers/McpLive.ts
|
|
8
8
|
/**
|
|
@@ -21,7 +21,7 @@ import { DataReaderLive, DataStoreLive, LoggerLive, OutputPipelineLive, ProjectD
|
|
|
21
21
|
const McpLive = (dbPath, logLevel, logFile) => {
|
|
22
22
|
const SqliteLayer = layer({ filename: dbPath });
|
|
23
23
|
const PlatformLayer = NodeServices.layer;
|
|
24
|
-
const MigratorLayer = SqliteMigrator.layer({ loader: SqliteMigrator.fromRecord(
|
|
24
|
+
const MigratorLayer = SqliteMigrator.layer({ loader: SqliteMigrator.fromRecord(PROJECT_MIGRATIONS) }).pipe(Layer.provide(Layer.merge(SqliteLayer, PlatformLayer)));
|
|
25
25
|
return Layer.mergeAll(DataReaderLive, DataStoreLive, ProjectDiscoveryLive, OutputPipelineLive).pipe(Layer.provideMerge(MigratorLayer), Layer.provideMerge(SqliteLayer), Layer.provideMerge(PlatformLayer), Layer.provideMerge(LoggerLive(logLevel, logFile)));
|
|
26
26
|
};
|
|
27
27
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vitest-agent/mcp",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Model Context Protocol server for vitest-agent. Exposes 53 tools for agent access to test data, TDD lifecycle, and session management.",
|
|
6
6
|
"keywords": [
|
|
@@ -42,12 +42,12 @@
|
|
|
42
42
|
"@effect/sql-sqlite-node": "4.0.0-rc.112",
|
|
43
43
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
44
44
|
"@trpc/server": "^11.18.0",
|
|
45
|
-
"@vitest-agent/sdk": "
|
|
45
|
+
"@vitest-agent/sdk": "3.0.0",
|
|
46
46
|
"effect": "4.0.0-rc.112",
|
|
47
47
|
"zod": "^4.5.4"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
|
-
"vitest": "^
|
|
50
|
+
"vitest": "^5.0.0"
|
|
51
51
|
},
|
|
52
52
|
"engines": {
|
|
53
53
|
"node": ">=24.11.0"
|
package/server.js
CHANGED
|
@@ -259,17 +259,18 @@ function buildMcpServer(ctx) {
|
|
|
259
259
|
return structuredResult(Schema.decodeSync(TestErrorsAsMarkdown)(data), data);
|
|
260
260
|
});
|
|
261
261
|
server.registerTool("test", {
|
|
262
|
-
description: "Use to inspect tests, with an action discriminator: action='list' (project?, state?, module?, limit?) returns matching tests; action='get' (fullName, project?, modulePath?) returns details + errors + run history — a fullName that exists in more than one module returns found=false with ambiguous=true and candidateModules[], so pass modulePath to disambiguate; action='for_file' (filePath) returns test modules covering a source file; action='for_tag' (tag, project?) returns tests carrying a tag, grouped by project. structuredContent carries the typed payload (discriminate on `action`, then on `found` for get).",
|
|
262
|
+
description: "Use to inspect tests, with an action discriminator: action='list' (project?, state?, module?, limit?) returns matching tests; action='get' (fullName, project?, modulePath?) returns details + errors + run history — a fullName that exists in more than one module returns found=false with ambiguous=true and candidateModules[], so pass modulePath to disambiguate; action='for_file' (filePath) returns test modules covering a source file; action='for_tag' (tag, project?) returns tests carrying a tag, grouped by project; action='annotations' (fullName, project?, modulePath?) returns the test annotations the author recorded via context.annotate; action='artifacts' (fullName, project?, modulePath?) returns the test artifacts recorded for the test — both return attachment descriptors (contentType, path, byteSize) and omit inline bodies unless maxBytes (a non-negative integer byte budget for the whole response, default 0) is passed, and neither has anything to do with TDD artifacts (see tdd_artifact_list). structuredContent carries the typed payload (discriminate on `action`, then on `found` for get).",
|
|
263
263
|
inputSchema: strict({
|
|
264
264
|
action: z.enum(TEST_ACTIONS).describe("Inspection discriminator"),
|
|
265
265
|
project: z.optional(z.string()),
|
|
266
266
|
state: z.optional(z.string()).describe("list: filter by state"),
|
|
267
267
|
module: z.optional(z.string()).describe("list: filter by module path"),
|
|
268
268
|
limit: z.optional(z.coerce.number()).describe("list: max rows to return"),
|
|
269
|
-
fullName: z.optional(z.string()).describe("get: full test name"),
|
|
270
|
-
modulePath: z.optional(z.string()).describe("get: exact module path, disambiguating a fullName present in several files"),
|
|
269
|
+
fullName: z.optional(z.string()).describe("get / annotations / artifacts: full test name"),
|
|
270
|
+
modulePath: z.optional(z.string()).describe("get / annotations / artifacts: exact module path, disambiguating a fullName present in several files"),
|
|
271
271
|
filePath: z.optional(z.string()).describe("for_file: source file path"),
|
|
272
|
-
tag: z.optional(z.string()).describe("for_tag: tag name")
|
|
272
|
+
tag: z.optional(z.string()).describe("for_tag: tag name"),
|
|
273
|
+
maxBytes: z.optional(z.coerce.number().int().nonnegative()).describe("annotations / artifacts: total byte budget for inline attachment bodies across the response; default 0 returns descriptors only")
|
|
273
274
|
}),
|
|
274
275
|
outputSchema: effectToZodSchema(TestResult)
|
|
275
276
|
}, async (args) => {
|
|
@@ -291,6 +292,20 @@ function buildMcpServer(ctx) {
|
|
|
291
292
|
action: "for_file",
|
|
292
293
|
filePath: args.filePath
|
|
293
294
|
});
|
|
295
|
+
else if (args.action === "annotations") data = await caller.test({
|
|
296
|
+
action: "annotations",
|
|
297
|
+
fullName: args.fullName,
|
|
298
|
+
...args.project !== void 0 && { project: args.project },
|
|
299
|
+
...args.modulePath !== void 0 && { modulePath: args.modulePath },
|
|
300
|
+
...args.maxBytes !== void 0 && { maxBytes: args.maxBytes }
|
|
301
|
+
});
|
|
302
|
+
else if (args.action === "artifacts") data = await caller.test({
|
|
303
|
+
action: "artifacts",
|
|
304
|
+
fullName: args.fullName,
|
|
305
|
+
...args.project !== void 0 && { project: args.project },
|
|
306
|
+
...args.modulePath !== void 0 && { modulePath: args.modulePath },
|
|
307
|
+
...args.maxBytes !== void 0 && { maxBytes: args.maxBytes }
|
|
308
|
+
});
|
|
294
309
|
else data = await caller.test({
|
|
295
310
|
action: "for_tag",
|
|
296
311
|
tag: args.tag,
|
package/tools/errors.js
CHANGED
|
@@ -20,6 +20,16 @@ import { DataReader } from "@vitest-agent/sdk";
|
|
|
20
20
|
*
|
|
21
21
|
* @packageDocumentation
|
|
22
22
|
*/
|
|
23
|
+
/** One annotation attached to a failing test, surfaced with its error. */
|
|
24
|
+
const TestErrorAnnotation = Schema.Struct({
|
|
25
|
+
type: Schema.String.annotate({ description: "Annotation type as the test author wrote it — an arbitrary string, not an enum." }),
|
|
26
|
+
message: Schema.String,
|
|
27
|
+
location: Schema.optional(Schema.Struct({
|
|
28
|
+
file: Schema.String,
|
|
29
|
+
line: Schema.Number,
|
|
30
|
+
column: Schema.Number
|
|
31
|
+
}))
|
|
32
|
+
}).annotate({ identifier: "TestErrorAnnotation" });
|
|
23
33
|
/** One row in the structured `errors[]` array. */
|
|
24
34
|
const TestErrorRow = Schema.Struct({
|
|
25
35
|
id: Schema.Finite.annotate({
|
|
@@ -43,7 +53,8 @@ const TestErrorRow = Schema.Struct({
|
|
|
43
53
|
"unhandled"
|
|
44
54
|
]).annotate({ description: "Where the error fired: `test` (a single test case), `suite` (a `describe` setup), `module` (collection / import time), or `unhandled` (uncaught from a background context)." }),
|
|
45
55
|
testFullName: Schema.NullOr(Schema.String).annotate({ description: "Full hierarchical test name (`describe > it`); `null` for non-test scopes (`module`, `unhandled`)." }),
|
|
46
|
-
moduleFile: Schema.NullOr(Schema.String).annotate({ description: "Repo-relative path of the test module the error originated in." })
|
|
56
|
+
moduleFile: Schema.NullOr(Schema.String).annotate({ description: "Repo-relative path of the test module the error originated in." }),
|
|
57
|
+
annotations: Schema.Array(TestErrorAnnotation).annotate({ description: "Test annotations the author recorded on this test (`context.annotate`). Empty for non-test scopes and for tests with no annotations." })
|
|
47
58
|
}).annotate({
|
|
48
59
|
identifier: "TestErrorRow",
|
|
49
60
|
title: "Test error row",
|
|
@@ -87,6 +98,10 @@ const formatTestErrorsMarkdown = (data) => {
|
|
|
87
98
|
lines.push(`**Scope:** ${error.scope}`);
|
|
88
99
|
if (error.testFullName !== null) lines.push(`**Test:** ${error.testFullName}`);
|
|
89
100
|
if (error.moduleFile !== null) lines.push(`**File:** \`${error.moduleFile}\``);
|
|
101
|
+
if (error.annotations.length > 0) {
|
|
102
|
+
lines.push("**Annotations:**");
|
|
103
|
+
for (const annotation of error.annotations) lines.push(`- [${annotation.type}] ${annotation.message}`);
|
|
104
|
+
}
|
|
90
105
|
lines.push("");
|
|
91
106
|
lines.push("**Cite-able IDs (for `hypothesis (action: record)`):**");
|
|
92
107
|
lines.push(`- citedTestErrorId: ${error.id}`);
|
|
@@ -142,14 +157,40 @@ const testErrors = publicProcedure.input(Schema.toStandardSchemaV1(Schema.Struct
|
|
|
142
157
|
project: Schema.String,
|
|
143
158
|
errorName: Schema.optional(Schema.String)
|
|
144
159
|
}))).query(async ({ ctx, input }) => ctx.runtime.runPromise(Effect.gen(function* () {
|
|
145
|
-
const
|
|
160
|
+
const reader = yield* DataReader;
|
|
161
|
+
const errors = yield* reader.getErrors(input.project, input.errorName);
|
|
162
|
+
const cache = /* @__PURE__ */ new Map();
|
|
163
|
+
const rows = [];
|
|
164
|
+
for (const error of errors) {
|
|
165
|
+
if (error.testFullName === null) {
|
|
166
|
+
rows.push({
|
|
167
|
+
...error,
|
|
168
|
+
annotations: []
|
|
169
|
+
});
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const key = `${error.testFullName}\u0000${error.moduleFile ?? ""}`;
|
|
173
|
+
let annotations = cache.get(key);
|
|
174
|
+
if (annotations === void 0) {
|
|
175
|
+
annotations = (yield* reader.getAnnotationsForTest(input.project, error.testFullName, { ...error.moduleFile !== null && { modulePath: error.moduleFile } })).map((a) => ({
|
|
176
|
+
type: a.type,
|
|
177
|
+
message: a.message,
|
|
178
|
+
...a.location && { location: a.location }
|
|
179
|
+
}));
|
|
180
|
+
cache.set(key, annotations);
|
|
181
|
+
}
|
|
182
|
+
rows.push({
|
|
183
|
+
...error,
|
|
184
|
+
annotations
|
|
185
|
+
});
|
|
186
|
+
}
|
|
146
187
|
return {
|
|
147
188
|
project: input.project,
|
|
148
189
|
...input.errorName !== void 0 && { errorName: input.errorName },
|
|
149
|
-
count:
|
|
150
|
-
errors
|
|
190
|
+
count: rows.length,
|
|
191
|
+
errors: rows
|
|
151
192
|
};
|
|
152
193
|
})));
|
|
153
194
|
|
|
154
195
|
//#endregion
|
|
155
|
-
export { TestErrorRow, TestErrorsAsMarkdown, TestErrorsResult, formatTestErrorsMarkdown, testErrors };
|
|
196
|
+
export { TestErrorAnnotation, TestErrorRow, TestErrorsAsMarkdown, TestErrorsResult, formatTestErrorsMarkdown, testErrors };
|
package/tools/help.js
CHANGED
|
@@ -31,14 +31,17 @@ const HELP_TEXT = `# vitest-agent MCP Tools
|
|
|
31
31
|
| \`file_coverage\` | \`filePath\`, \`project?\` | Per-file coverage with uncovered lines and related tests |
|
|
32
32
|
| \`test_history\` | \`project\` | Flaky/persistent/recovered tests |
|
|
33
33
|
| \`test_trends\` | \`project\`, \`limit?\` | Coverage trajectory over time |
|
|
34
|
-
| \`test_errors\` | \`project\`, \`errorName?\`, \`format?\` (\`markdown\` \\| \`xml\`) | Errors with diffs, stacks, and the cite-able \`testErrorId\` / \`topStackFrameId\` values needed by \`hypothesis (action: record)\` |
|
|
35
|
-
| \`test\` | \`action\` (\`list\`/\`get\`/\`for_file\`/\`for_tag\`), plus per-action params | Consolidated test inspection: list/get/for_file/for_tag |
|
|
34
|
+
| \`test_errors\` | \`project\`, \`errorName?\`, \`format?\` (\`markdown\` \\| \`xml\`) | Errors with diffs, stacks, each row's \`annotations[]\` (the test annotations the author recorded), and the cite-able \`testErrorId\` / \`topStackFrameId\` values needed by \`hypothesis (action: record)\` |
|
|
35
|
+
| \`test\` | \`action\` (\`list\`/\`get\`/\`for_file\`/\`for_tag\`/\`annotations\`/\`artifacts\`), plus per-action params | Consolidated test inspection: list/get/for_file/for_tag/annotations/artifacts |
|
|
36
36
|
|
|
37
37
|
\`test\` actions:
|
|
38
38
|
- \`{ action: "list", project?, state?, module?, limit? }\`
|
|
39
39
|
- \`{ action: "get", fullName, project?, modulePath? }\` — a \`fullName\` present in more than one module returns \`found: false\` with \`ambiguous: true\` and \`candidateModules[]\`; pass \`modulePath\` to pick one
|
|
40
40
|
- \`{ action: "for_file", filePath }\`
|
|
41
41
|
- \`{ action: "for_tag", tag, project? }\` — list every test carrying a tag, grouped by project (or one group when project is supplied)
|
|
42
|
+
- \`{ action: "annotations", fullName, project?, modulePath?, maxBytes? }\` — the test annotations the author recorded via \`context.annotate\`, with attachment descriptors (\`contentType\`, \`path\`, \`byteSize\`)
|
|
43
|
+
- \`{ action: "artifacts", fullName, project?, modulePath?, maxBytes? }\` — the test artifacts recorded for the test, same descriptor shape. These are Vitest test artifacts, not TDD artifacts
|
|
44
|
+
- \`maxBytes\` (both actions) is the total byte budget for inline attachment bodies across the response; it defaults to 0, so a \`body\` is returned only when you ask for one and the running total stays inside the budget
|
|
42
45
|
|
|
43
46
|
## Discovery
|
|
44
47
|
|
|
@@ -107,7 +110,7 @@ const HELP_TEXT = `# vitest-agent MCP Tools
|
|
|
107
110
|
| \`tdd_task\` | \`action\` (\`start\`/\`end\`/\`get\`/\`resume\`), plus per-action params | TDD task lifecycle |
|
|
108
111
|
| \`tdd_goal\` | \`action\` (\`create\`/\`update\`/\`delete\`/\`get\`/\`list\`), plus per-action params | Goals under a TDD task |
|
|
109
112
|
| \`tdd_behavior\` | \`action\` (\`create\`/\`update\`/\`delete\`/\`get\`/\`list_by_goal\`/\`list_by_tdd_task\`), plus per-action params | Behaviors under a goal |
|
|
110
|
-
| \`tdd_artifact_list\` | \`tddTaskId\`, \`artifactKind?\`, \`phaseId?\`, \`behaviorId?\`, \`limit?\`, \`format?\` | List recorded TDD artifacts (newest first); use to find the artifact id to cite in \`tdd_phase_transition_request\` |
|
|
113
|
+
| \`tdd_artifact_list\` | \`tddTaskId\`, \`artifactKind?\`, \`phaseId?\`, \`behaviorId?\`, \`limit?\`, \`format?\` | List recorded TDD artifacts (newest first); use to find the artifact id to cite in \`tdd_phase_transition_request\`. TDD artifacts are red/green evidence rows, not Vitest test artifacts (see \`test\` action \`artifacts\`) |
|
|
111
114
|
| \`tdd_phase_transition_request\` | \`tddTaskId\`, \`goalId\`, \`requestedPhase\`, \`citedArtifactId?\`, \`citedArtifactKind?\`, \`behaviorId?\`, \`reason?\` | Request a phase transition; validates D2 binding rules. \`citedArtifactId\` is optional — when omitted, the most recent matching artifact (kind from \`citedArtifactKind\` or the transition's required-evidence rule) is auto-resolved |
|
|
112
115
|
| \`tdd_progress_push\` | \`payload\` | Push a TDD progress event to the main agent (best-effort) |
|
|
113
116
|
|
package/tools/run-tests.js
CHANGED
|
@@ -209,9 +209,38 @@ const VITEST_CONFIG_EXTENSIONS = [
|
|
|
209
209
|
"mjs",
|
|
210
210
|
"cjs"
|
|
211
211
|
];
|
|
212
|
+
function findConfigInDir(dir) {
|
|
213
|
+
for (const prefix of ["vitest.config.", "vite.config."]) for (const ext of VITEST_CONFIG_EXTENSIONS) {
|
|
214
|
+
const candidate = join(dir, `${prefix}${ext}`);
|
|
215
|
+
if (existsSync(candidate)) return candidate;
|
|
216
|
+
}
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
212
219
|
function dirHasVitestOrViteConfig(dir) {
|
|
213
|
-
|
|
214
|
-
|
|
220
|
+
return findConfigInDir(dir) !== null;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Walk UP from `startDir` looking for the vitest/vite config file, returning
|
|
224
|
+
* its absolute path. Same walk and same git-root bound as
|
|
225
|
+
* `resolveConfigAnchoredRoot`; returns `null` when no config is found in
|
|
226
|
+
* range or when anything about the walk throws.
|
|
227
|
+
*
|
|
228
|
+
* @internal exported for tests
|
|
229
|
+
*/
|
|
230
|
+
function resolveAnchoredConfigFile(startDir) {
|
|
231
|
+
try {
|
|
232
|
+
let dir = resolve(startDir);
|
|
233
|
+
for (;;) {
|
|
234
|
+
const found = findConfigInDir(dir);
|
|
235
|
+
if (found !== null) return found;
|
|
236
|
+
if (existsSync(join(dir, ".git"))) return null;
|
|
237
|
+
const parent = dirname(dir);
|
|
238
|
+
if (parent === dir) return null;
|
|
239
|
+
dir = parent;
|
|
240
|
+
}
|
|
241
|
+
} catch {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
215
244
|
}
|
|
216
245
|
/**
|
|
217
246
|
* Walk UP from `startDir` looking for the vitest/vite config Vitest would
|
|
@@ -245,11 +274,14 @@ function resolveConfigAnchoredRoot(startDir) {
|
|
|
245
274
|
* defaulted to anything else when no config is found in range.
|
|
246
275
|
*
|
|
247
276
|
* A supplied `projectRoot` is VALIDATED, not trusted, and used VERBATIM
|
|
248
|
-
* once validated — explicit is explicit, no anchoring applied
|
|
249
|
-
* resolve to an existing directory that shares a git common
|
|
250
|
-
* `ctxCwd` (same repository, including across worktrees).
|
|
251
|
-
* returns `{ ok: false, message }` naming both paths — never a
|
|
252
|
-
* fallback to `ctxCwd`, never a raw throw.
|
|
277
|
+
* once validated — explicit is explicit, no anchoring applied to `root`.
|
|
278
|
+
* It must resolve to an existing directory that shares a git common
|
|
279
|
+
* directory with `ctxCwd` (same repository, including across worktrees).
|
|
280
|
+
* Any failure returns `{ ok: false, message }` naming both paths — never a
|
|
281
|
+
* silent fallback to `ctxCwd`, never a raw throw. The CONFIG for such a
|
|
282
|
+
* root is resolved separately by the caller via
|
|
283
|
+
* `resolveAnchoredConfigFile`, because Vitest 5 no longer walks up to find
|
|
284
|
+
* one.
|
|
253
285
|
*
|
|
254
286
|
* @internal exported for tests
|
|
255
287
|
*/
|
|
@@ -288,8 +320,8 @@ async function validateProjectRoot(projectRoot, ctxCwd) {
|
|
|
288
320
|
* `@vitest-agent/mcp`'s OWN install location. `vitest` is a peerDependency
|
|
289
321
|
* of this package, and pnpm routinely materializes MORE THAN ONE physical
|
|
290
322
|
* instance of the same vitest version when peer-resolution hashes differ
|
|
291
|
-
* (e.g. `vitest@
|
|
292
|
-
* `vitest@
|
|
323
|
+
* (e.g. `vitest@<version>_@types+node@26.2.0_...` alongside
|
|
324
|
+
* `vitest@<version>_@types+node@26.3.0_...` under `node_modules/.pnpm`). When
|
|
293
325
|
* the bare specifier resolves to a DIFFERENT physical copy than the one the
|
|
294
326
|
* project's test files import, `SnapshotClient.setup()` runs against one
|
|
295
327
|
* copy's module-level `_client` singleton while `expect(...).toMatchSnapshot()`
|
|
@@ -508,6 +540,15 @@ const runTests = publicProcedure.input(Schema.toStandardSchemaV1(Schema.Struct({
|
|
|
508
540
|
message: projectRootValidation.message
|
|
509
541
|
};
|
|
510
542
|
const resolvedRoot = projectRootValidation.root;
|
|
543
|
+
let anchoredConfig;
|
|
544
|
+
if (input.projectRoot !== void 0) {
|
|
545
|
+
const found = resolveAnchoredConfigFile(resolvedRoot);
|
|
546
|
+
if (found === null) return {
|
|
547
|
+
kind: "error",
|
|
548
|
+
message: `No vitest.config.* or vite.config.* was found at or above projectRoot "${resolvedRoot}" within the repository. Vitest 5 does not search ancestor directories, so this run would collect no tests and load no plugins. Pass a projectRoot that contains a Vitest config, or omit projectRoot to anchor automatically.`
|
|
549
|
+
};
|
|
550
|
+
anchoredConfig = found;
|
|
551
|
+
}
|
|
511
552
|
const timeoutMs = (input.timeout ?? 120) * 1e3;
|
|
512
553
|
const recovered = input._sessionContext ?? null ?? ctx.sessionContext.get();
|
|
513
554
|
if (recovered !== null) {
|
|
@@ -523,8 +564,9 @@ const runTests = publicProcedure.input(Schema.toStandardSchemaV1(Schema.Struct({
|
|
|
523
564
|
let covOverride;
|
|
524
565
|
try {
|
|
525
566
|
covOverride = makeCoverageDirOverride();
|
|
526
|
-
vitest = await createVitest(
|
|
567
|
+
vitest = await createVitest({
|
|
527
568
|
root: resolvedRoot,
|
|
569
|
+
...anchoredConfig !== void 0 ? { config: anchoredConfig } : {},
|
|
528
570
|
run: true,
|
|
529
571
|
coverage: covOverride.coverage,
|
|
530
572
|
...project ? { project } : {},
|
|
@@ -640,4 +682,4 @@ const runTests = publicProcedure.input(Schema.toStandardSchemaV1(Schema.Struct({
|
|
|
640
682
|
}));
|
|
641
683
|
|
|
642
684
|
//#endregion
|
|
643
|
-
export { RunTestsAsMarkdown, RunTestsResult, coerceErrors, composeTagExpression, formatNoMatchMarkdown, formatReportMarkdown, formatRunTestsMarkdown, makeCoverageDirOverride, readDiscoveryLastScannedAt, resolveConfigAnchoredRoot, resolveGitCommonDir, resolveVitestNodeEntry, runTests, sanitizeTestArgs, validateProjectRoot, vitestLoader, withStdioCaptured };
|
|
685
|
+
export { RunTestsAsMarkdown, RunTestsResult, coerceErrors, composeTagExpression, formatNoMatchMarkdown, formatReportMarkdown, formatRunTestsMarkdown, makeCoverageDirOverride, readDiscoveryLastScannedAt, resolveAnchoredConfigFile, resolveConfigAnchoredRoot, resolveGitCommonDir, resolveVitestNodeEntry, runTests, sanitizeTestArgs, validateProjectRoot, vitestLoader, withStdioCaptured };
|
package/tools/test.js
CHANGED
|
@@ -4,16 +4,6 @@ import { Effect, Match, Option, Schema, SchemaGetter } from "effect";
|
|
|
4
4
|
import { DataReader } from "@vitest-agent/sdk";
|
|
5
5
|
|
|
6
6
|
//#region src/tools/test.ts
|
|
7
|
-
/**
|
|
8
|
-
* Consolidated `test` MCP tool — Schema-driven implementation.
|
|
9
|
-
*
|
|
10
|
-
* Replaces `test_list`, `test_get`, and `test_for_file` with one
|
|
11
|
-
* tool keyed on `action`. Result variants discriminate on
|
|
12
|
-
* `inventoryKind` so a single Effect Schema describes every shape
|
|
13
|
-
* the tool can emit.
|
|
14
|
-
*
|
|
15
|
-
* @packageDocumentation
|
|
16
|
-
*/
|
|
17
7
|
const TestRowSchema = Schema.Struct({
|
|
18
8
|
id: Schema.Number,
|
|
19
9
|
fullName: Schema.String,
|
|
@@ -76,16 +66,93 @@ const TestForTagResult = Schema.Struct({
|
|
|
76
66
|
count: Schema.Number,
|
|
77
67
|
groups: Schema.Array(TestListGroup)
|
|
78
68
|
}).annotate({ identifier: "TestForTag" });
|
|
69
|
+
const AttachmentDescriptor = Schema.Struct({
|
|
70
|
+
contentType: Schema.optional(Schema.String),
|
|
71
|
+
path: Schema.optional(Schema.String),
|
|
72
|
+
byteSize: Schema.NullOr(Schema.Number),
|
|
73
|
+
/** Present only for inline attachments under the 64 KiB persistence cap. */
|
|
74
|
+
body: Schema.optional(Schema.String),
|
|
75
|
+
/** How to read `body`. Absent when nothing was stored inline. */
|
|
76
|
+
bodyEncoding: Schema.optional(Schema.Literals(["base64", "utf-8"]))
|
|
77
|
+
}).annotate({ identifier: "TestAttachmentDescriptor" });
|
|
78
|
+
/**
|
|
79
|
+
* Inline bodies are opt-in and budgeted. The 64 KiB persistence cap is
|
|
80
|
+
* per attachment, so a test with many attachments could still flood an
|
|
81
|
+
* agent's context; `maxBytes` (default 0) is the total byte budget for
|
|
82
|
+
* every body in one response. Attachments are walked in order and each
|
|
83
|
+
* body is charged its recorded `byteSize` (falling back to the stored
|
|
84
|
+
* string's length when the row carries none). A body that would take
|
|
85
|
+
* the running total past the budget is dropped along with its
|
|
86
|
+
* `bodyEncoding`; the descriptor half — `contentType`, `path`,
|
|
87
|
+
* `byteSize` — always survives.
|
|
88
|
+
*/
|
|
89
|
+
const applyBodyBudget = (rows, maxBytes) => {
|
|
90
|
+
let spent = 0;
|
|
91
|
+
return rows.map((row) => ({
|
|
92
|
+
...row,
|
|
93
|
+
attachments: row.attachments.map((attachment) => {
|
|
94
|
+
const { body, bodyEncoding, ...descriptor } = attachment;
|
|
95
|
+
if (body === void 0) return descriptor;
|
|
96
|
+
const cost = attachment.byteSize ?? body.length;
|
|
97
|
+
if (spent + cost > maxBytes) return descriptor;
|
|
98
|
+
spent += cost;
|
|
99
|
+
return {
|
|
100
|
+
...descriptor,
|
|
101
|
+
body,
|
|
102
|
+
...bodyEncoding !== void 0 && { bodyEncoding }
|
|
103
|
+
};
|
|
104
|
+
})
|
|
105
|
+
}));
|
|
106
|
+
};
|
|
107
|
+
const AnnotationRowSchema = Schema.Struct({
|
|
108
|
+
id: Schema.Number,
|
|
109
|
+
type: Schema.String,
|
|
110
|
+
message: Schema.String,
|
|
111
|
+
location: Schema.optional(Schema.Struct({
|
|
112
|
+
file: Schema.String,
|
|
113
|
+
line: Schema.Number,
|
|
114
|
+
column: Schema.Number
|
|
115
|
+
})),
|
|
116
|
+
attachments: Schema.Array(AttachmentDescriptor)
|
|
117
|
+
}).annotate({ identifier: "TestAnnotationRow" });
|
|
118
|
+
const ArtifactRowSchema = Schema.Struct({
|
|
119
|
+
id: Schema.Number,
|
|
120
|
+
type: Schema.String,
|
|
121
|
+
message: Schema.NullOr(Schema.String),
|
|
122
|
+
data: Schema.NullOr(Schema.String),
|
|
123
|
+
location: Schema.optional(Schema.Struct({
|
|
124
|
+
file: Schema.String,
|
|
125
|
+
line: Schema.Number,
|
|
126
|
+
column: Schema.Number
|
|
127
|
+
})),
|
|
128
|
+
attachments: Schema.Array(AttachmentDescriptor)
|
|
129
|
+
}).annotate({ identifier: "TestArtifactRow" });
|
|
130
|
+
const TestAnnotationsResult = Schema.Struct({
|
|
131
|
+
action: Schema.Literal("annotations"),
|
|
132
|
+
project: Schema.String,
|
|
133
|
+
fullName: Schema.String,
|
|
134
|
+
count: Schema.Number,
|
|
135
|
+
annotations: Schema.Array(AnnotationRowSchema)
|
|
136
|
+
}).annotate({ identifier: "TestAnnotations" });
|
|
137
|
+
const TestArtifactsResult = Schema.Struct({
|
|
138
|
+
action: Schema.Literal("artifacts"),
|
|
139
|
+
project: Schema.String,
|
|
140
|
+
fullName: Schema.String,
|
|
141
|
+
count: Schema.Number,
|
|
142
|
+
artifacts: Schema.Array(ArtifactRowSchema)
|
|
143
|
+
}).annotate({ identifier: "TestArtifacts" });
|
|
79
144
|
const TestResult = Schema.Union([
|
|
80
145
|
TestListResult,
|
|
81
146
|
TestGetFound,
|
|
82
147
|
TestGetMissing,
|
|
83
148
|
TestForFileResult,
|
|
84
|
-
TestForTagResult
|
|
149
|
+
TestForTagResult,
|
|
150
|
+
TestAnnotationsResult,
|
|
151
|
+
TestArtifactsResult
|
|
85
152
|
]).annotate({
|
|
86
153
|
identifier: "TestResult",
|
|
87
154
|
title: "test result",
|
|
88
|
-
description: "Discriminate on `action`. `get` further discriminates on `found`. `list`, `for_file`, and `
|
|
155
|
+
description: "Discriminate on `action`. `get` further discriminates on `found`. `list`, `for_file`, `for_tag`, `annotations`, and `artifacts` all carry counted arrays — `list` and `for_tag` group by project; `annotations` and `artifacts` are scoped to one test and return attachment descriptors — an inline `body` comes back only when `maxBytes` is passed and the running total stays inside it."
|
|
89
156
|
});
|
|
90
157
|
const formatTestMarkdown = (data) => {
|
|
91
158
|
if (data.action === "list") {
|
|
@@ -178,6 +245,42 @@ const formatTestMarkdown = (data) => {
|
|
|
178
245
|
}
|
|
179
246
|
return lines.join("\n").trimEnd();
|
|
180
247
|
}
|
|
248
|
+
if (data.action === "annotations" || data.action === "artifacts") {
|
|
249
|
+
const noun = data.action === "annotations" ? "test annotations" : "test artifacts";
|
|
250
|
+
if (data.count === 0) return `No ${noun} recorded for \`${data.fullName}\`.`;
|
|
251
|
+
const rows = data.action === "annotations" ? data.annotations.map((a) => ({
|
|
252
|
+
type: a.type,
|
|
253
|
+
message: a.message,
|
|
254
|
+
location: a.location,
|
|
255
|
+
attachments: a.attachments
|
|
256
|
+
})) : data.artifacts.map((a) => ({
|
|
257
|
+
type: a.type,
|
|
258
|
+
message: a.message ?? a.data,
|
|
259
|
+
location: a.location,
|
|
260
|
+
attachments: a.attachments
|
|
261
|
+
}));
|
|
262
|
+
const cell = (value) => {
|
|
263
|
+
const flat = value.split("\n").join(" ").split("|").join("\\|");
|
|
264
|
+
return flat.length > 200 ? `${flat.slice(0, 200)}… (truncated)` : flat;
|
|
265
|
+
};
|
|
266
|
+
const lines = [
|
|
267
|
+
`# ${data.action === "annotations" ? "Annotations" : "Artifacts"} for \`${data.fullName}\``,
|
|
268
|
+
"",
|
|
269
|
+
`Project: \`${data.project}\` — ${data.count} row${data.count === 1 ? "" : "s"}.`,
|
|
270
|
+
"",
|
|
271
|
+
"| Type | Message | Location | Attachments |",
|
|
272
|
+
"| --- | --- | --- | --- |"
|
|
273
|
+
];
|
|
274
|
+
for (const row of rows) {
|
|
275
|
+
const location = row.location ? `\`${row.location.file}:${row.location.line}:${row.location.column}\`` : "—";
|
|
276
|
+
const attachments = row.attachments.length === 0 ? "—" : row.attachments.map((att) => {
|
|
277
|
+
return `\`${cell(att.path ?? att.contentType ?? "(inline)")}\`${att.byteSize !== null ? ` (${att.byteSize} bytes)` : ""}`;
|
|
278
|
+
}).join(", ");
|
|
279
|
+
const message = row.message === null ? "—" : cell(row.message);
|
|
280
|
+
lines.push(`| ${row.type} | ${message} | ${location} | ${attachments} |`);
|
|
281
|
+
}
|
|
282
|
+
return lines.join("\n");
|
|
283
|
+
}
|
|
181
284
|
if (data.count === 0) return `No test modules found covering \`${data.filePath}\`. Run run_tests({}) to populate the database, or check the file path.`;
|
|
182
285
|
const lines = [
|
|
183
286
|
`# Tests for \`${data.filePath}\``,
|
|
@@ -214,11 +317,27 @@ const ForTagVariant = Schema.Struct({
|
|
|
214
317
|
tag: Schema.String,
|
|
215
318
|
project: Schema.optional(Schema.String)
|
|
216
319
|
});
|
|
320
|
+
const AnnotationsVariant = Schema.Struct({
|
|
321
|
+
action: Schema.Literal("annotations"),
|
|
322
|
+
fullName: Schema.String,
|
|
323
|
+
project: Schema.optional(Schema.String),
|
|
324
|
+
modulePath: Schema.optional(Schema.String).annotate({ description: "Exact module_path match — disambiguates a fullName that exists in more than one test file." }),
|
|
325
|
+
maxBytes: Schema.optional(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)).annotate({ description: "Total byte budget for inline attachment bodies across the whole response. Must be a non-negative integer. Defaults to 0 — descriptors only, no bodies." }))
|
|
326
|
+
});
|
|
327
|
+
const ArtifactsVariant = Schema.Struct({
|
|
328
|
+
action: Schema.Literal("artifacts"),
|
|
329
|
+
fullName: Schema.String,
|
|
330
|
+
project: Schema.optional(Schema.String),
|
|
331
|
+
modulePath: Schema.optional(Schema.String).annotate({ description: "Exact module_path match — disambiguates a fullName that exists in more than one test file." }),
|
|
332
|
+
maxBytes: Schema.optional(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)).annotate({ description: "Total byte budget for inline attachment bodies across the whole response. Must be a non-negative integer. Defaults to 0 — descriptors only, no bodies." }))
|
|
333
|
+
});
|
|
217
334
|
const TestInput = Schema.Union([
|
|
218
335
|
ListVariant,
|
|
219
336
|
GetVariant,
|
|
220
337
|
ForFileVariant,
|
|
221
|
-
ForTagVariant
|
|
338
|
+
ForTagVariant,
|
|
339
|
+
AnnotationsVariant,
|
|
340
|
+
ArtifactsVariant
|
|
222
341
|
]);
|
|
223
342
|
/**
|
|
224
343
|
* Single source of truth for the `test` tool's `action` discriminant,
|
|
@@ -229,7 +348,9 @@ const TEST_ACTIONS = [
|
|
|
229
348
|
"list",
|
|
230
349
|
"get",
|
|
231
350
|
"for_file",
|
|
232
|
-
"for_tag"
|
|
351
|
+
"for_tag",
|
|
352
|
+
"annotations",
|
|
353
|
+
"artifacts"
|
|
233
354
|
];
|
|
234
355
|
const test = publicProcedure.input(Schema.toStandardSchemaV1(TestInput)).query(async ({ ctx, input }) => {
|
|
235
356
|
return ctx.runtime.runPromise(Match.value(input).pipe(Match.discriminatorsExhaustive("action")({
|
|
@@ -316,6 +437,32 @@ const test = publicProcedure.input(Schema.toStandardSchemaV1(TestInput)).query(a
|
|
|
316
437
|
count: grouped.total,
|
|
317
438
|
groups
|
|
318
439
|
};
|
|
440
|
+
}),
|
|
441
|
+
annotations: (variant) => Effect.gen(function* () {
|
|
442
|
+
const reader = yield* DataReader;
|
|
443
|
+
const project = variant.project ?? (yield* reader.getRunsByProject().pipe(Effect.map((rs) => rs[0]?.project ?? "")));
|
|
444
|
+
const rows = yield* reader.getAnnotationsForTest(project, variant.fullName, { ...variant.modulePath !== void 0 && { modulePath: variant.modulePath } });
|
|
445
|
+
const annotations = applyBodyBudget(rows, variant.maxBytes ?? 0);
|
|
446
|
+
return {
|
|
447
|
+
action: "annotations",
|
|
448
|
+
project,
|
|
449
|
+
fullName: variant.fullName,
|
|
450
|
+
count: annotations.length,
|
|
451
|
+
annotations
|
|
452
|
+
};
|
|
453
|
+
}),
|
|
454
|
+
artifacts: (variant) => Effect.gen(function* () {
|
|
455
|
+
const reader = yield* DataReader;
|
|
456
|
+
const project = variant.project ?? (yield* reader.getRunsByProject().pipe(Effect.map((rs) => rs[0]?.project ?? "")));
|
|
457
|
+
const rows = yield* reader.getArtifactsForTest(project, variant.fullName, { ...variant.modulePath !== void 0 && { modulePath: variant.modulePath } });
|
|
458
|
+
const artifacts = applyBodyBudget(rows, variant.maxBytes ?? 0);
|
|
459
|
+
return {
|
|
460
|
+
action: "artifacts",
|
|
461
|
+
project,
|
|
462
|
+
fullName: variant.fullName,
|
|
463
|
+
count: artifacts.length,
|
|
464
|
+
artifacts
|
|
465
|
+
};
|
|
319
466
|
})
|
|
320
467
|
})));
|
|
321
468
|
});
|