@vitest-agent/mcp 3.0.4 → 4.0.1
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/README.md +7 -6
- package/annotations.js +18 -0
- package/bin/vitest-agent-mcp.js +5 -140
- package/{middleware/idempotency.js → idempotency.js} +48 -49
- package/index.d.ts +5610 -1549
- package/index.js +35 -17
- package/main.d.ts +31 -0
- package/main.js +144 -0
- package/package.json +9 -6
- package/prompts/layer.js +108 -0
- package/register-toolkit.js +311 -0
- package/server.js +24 -894
- package/{context.js → session.js} +38 -22
- package/toolkit.js +86 -0
- package/tools/acceptance-metrics.js +26 -14
- package/tools/cache-health.js +28 -17
- package/tools/commit-changes.js +33 -10
- package/tools/configure.js +33 -10
- package/tools/coverage.js +34 -5
- package/tools/errors.js +36 -26
- package/tools/failure-signature-get.js +33 -10
- package/tools/file-coverage.js +37 -13
- package/tools/help.js +45 -4
- package/tools/history.js +39 -19
- package/tools/hypothesis.js +118 -102
- package/tools/inventory.js +149 -146
- package/tools/note.js +131 -113
- package/tools/overview.js +33 -10
- package/tools/ping.js +22 -10
- package/tools/register-agent.js +88 -62
- package/tools/run-tests.js +76 -23
- package/tools/settings-list.js +27 -10
- package/tools/status.js +35 -10
- package/tools/tdd-artifact.js +37 -22
- package/tools/tdd-behavior.js +120 -108
- package/tools/tdd-goal.js +98 -85
- package/tools/tdd-phase-transition-request.js +201 -166
- package/tools/tdd-progress-push.js +102 -0
- package/tools/tdd-task.js +140 -138
- package/tools/test.js +152 -138
- package/tools/trends.js +37 -18
- package/tools/triage-brief.js +34 -15
- package/tools/turn-search.js +39 -16
- package/tools/wrapup-prompt.js +37 -17
- package/utils/crash-guards.js +0 -22
- package/utils/replay-marker.js +12 -0
- package/utils/safe-format-fatal-error.js +0 -16
- package/utils/tool-error-envelope.js +3 -3
- package/version.js +13 -0
- package/layers/McpLive.js +0 -29
- package/prompts/index.js +0 -89
- package/router.js +0 -74
- package/session-env.js +0 -112
- package/utils/effect-to-zod.js +0 -158
package/tools/overview.js
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { RenderText } from "../annotations.js";
|
|
2
2
|
import { Effect, Option, Schema, SchemaGetter } from "effect";
|
|
3
|
-
import { DataReader } from "@vitest-agent/
|
|
3
|
+
import { DataReader } from "@vitest-agent/engine";
|
|
4
|
+
import { Tool } from "effect/unstable/ai";
|
|
4
5
|
|
|
5
6
|
//#region src/tools/overview.ts
|
|
6
|
-
/**
|
|
7
|
-
* `test_overview` MCP tool — Schema-driven implementation.
|
|
8
|
-
*
|
|
9
|
-
* @packageDocumentation
|
|
10
|
-
*/
|
|
11
7
|
const ProjectRunSummary = Schema.Struct({
|
|
12
8
|
project: Schema.String,
|
|
13
9
|
lastRun: Schema.NullOr(Schema.String),
|
|
@@ -34,6 +30,11 @@ const OverviewAbsent = Schema.Struct({
|
|
|
34
30
|
projectFilter: Schema.optional(Schema.String),
|
|
35
31
|
reason: Schema.Literals(["no_runs", "project_filter_empty"])
|
|
36
32
|
}).annotate({ identifier: "TestOverviewAbsent" });
|
|
33
|
+
/**
|
|
34
|
+
* The `test_overview` tool's success payload.
|
|
35
|
+
*
|
|
36
|
+
* @public
|
|
37
|
+
*/
|
|
37
38
|
const TestOverviewResult = Schema.Union([OverviewAvailable, OverviewAbsent]).annotate({
|
|
38
39
|
identifier: "TestOverviewResult",
|
|
39
40
|
title: "test_overview result",
|
|
@@ -70,7 +71,18 @@ const TestOverviewAsMarkdown = TestOverviewResult.pipe(Schema.decodeTo(Schema.St
|
|
|
70
71
|
decode: SchemaGetter.transform((data) => formatTestOverviewMarkdown(data)),
|
|
71
72
|
encode: SchemaGetter.forbidden(() => "TestOverviewAsMarkdown is one-way.")
|
|
72
73
|
}));
|
|
73
|
-
|
|
74
|
+
/**
|
|
75
|
+
* The `test_overview` tool's parameters.
|
|
76
|
+
*
|
|
77
|
+
* @public
|
|
78
|
+
*/
|
|
79
|
+
const TestOverviewInput = Schema.Struct({ project: Schema.optionalKey(Schema.String).annotate({ description: "Filter to a specific project" }) });
|
|
80
|
+
/**
|
|
81
|
+
* Handler for {@link testOverviewTool}.
|
|
82
|
+
*
|
|
83
|
+
* @public
|
|
84
|
+
*/
|
|
85
|
+
const handleTestOverview = (input) => Effect.gen(function* () {
|
|
74
86
|
const reader = yield* DataReader;
|
|
75
87
|
const [manifestOpt, runs] = yield* Effect.all([reader.getManifest(), reader.getRunsByProject()], { concurrency: "unbounded" });
|
|
76
88
|
if (Option.isNone(manifestOpt) || runs.length === 0) return {
|
|
@@ -89,7 +101,18 @@ const testOverview = publicProcedure.input(Schema.toStandardSchemaV1(Schema.Stru
|
|
|
89
101
|
...input.project !== void 0 && { projectFilter: input.project },
|
|
90
102
|
runs: filteredRuns
|
|
91
103
|
};
|
|
92
|
-
}))
|
|
104
|
+
}).pipe(Effect.orDie);
|
|
105
|
+
/**
|
|
106
|
+
* The Effect-native `test_overview` tool.
|
|
107
|
+
*
|
|
108
|
+
* @public
|
|
109
|
+
*/
|
|
110
|
+
const testOverviewTool = Tool.make("test_overview", {
|
|
111
|
+
description: "Use when you want a summary of the test landscape with per-project run metrics. Returns markdown in content[] and a typed JSON object in structuredContent ({ dataAvailable, projectFilter?, runs[] } or absent variant).",
|
|
112
|
+
parameters: TestOverviewInput,
|
|
113
|
+
success: TestOverviewResult,
|
|
114
|
+
dependencies: [DataReader]
|
|
115
|
+
}).annotate(Tool.Title, "Test overview").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true).annotate(RenderText, (encoded) => formatTestOverviewMarkdown(encoded));
|
|
93
116
|
|
|
94
117
|
//#endregion
|
|
95
|
-
export {
|
|
118
|
+
export { TestOverviewInput, TestOverviewResult, formatTestOverviewMarkdown, handleTestOverview, testOverviewTool };
|
package/tools/ping.js
CHANGED
|
@@ -1,22 +1,34 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import { Tool } from "effect/unstable/ai";
|
|
3
3
|
|
|
4
4
|
//#region src/tools/ping.ts
|
|
5
5
|
/**
|
|
6
|
-
* `ping`
|
|
6
|
+
* The `ping` tool's success payload.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
* server. Returns the canonical `pong` payload so callers can assert
|
|
10
|
-
* a healthy round-trip.
|
|
11
|
-
*
|
|
12
|
-
* @packageDocumentation
|
|
8
|
+
* @public
|
|
13
9
|
*/
|
|
14
10
|
const PingResult = Schema.Struct({ message: Schema.Literal("pong").annotate({ description: "Constant `pong`. Presence confirms the MCP server responded." }) }).annotate({
|
|
15
11
|
identifier: "PingResult",
|
|
16
12
|
title: "ping result",
|
|
17
13
|
description: "Liveness probe. Carries no data beyond the constant `pong` discriminant."
|
|
18
14
|
});
|
|
19
|
-
|
|
15
|
+
/**
|
|
16
|
+
* The Effect-native `ping` tool. No parameters (the default
|
|
17
|
+
* `Tool.EmptyParams` serves as a strict empty object; `Schema.Struct({})`
|
|
18
|
+
* would serialize to a non-object JSON Schema that MCP rejects).
|
|
19
|
+
*
|
|
20
|
+
* @public
|
|
21
|
+
*/
|
|
22
|
+
const pingTool = Tool.make("ping", {
|
|
23
|
+
description: "Ping the MCP server — returns 'pong'. Used to verify hot-patch reload.",
|
|
24
|
+
success: PingResult
|
|
25
|
+
}).annotate(Tool.Title, "Ping").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true);
|
|
26
|
+
/**
|
|
27
|
+
* Handler for {@link pingTool}.
|
|
28
|
+
*
|
|
29
|
+
* @public
|
|
30
|
+
*/
|
|
31
|
+
const handlePing = () => Effect.succeed({ message: "pong" });
|
|
20
32
|
|
|
21
33
|
//#endregion
|
|
22
|
-
export { PingResult,
|
|
34
|
+
export { PingResult, handlePing, pingTool };
|
package/tools/register-agent.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { publicProcedure } from "../context.js";
|
|
2
1
|
import { Effect, Option, Schema } from "effect";
|
|
3
|
-
import { DataReader, DataStore, deriveIdempotencyKey } from "@vitest-agent/
|
|
2
|
+
import { DataReader, DataStore, deriveIdempotencyKey } from "@vitest-agent/engine";
|
|
3
|
+
import { Tool } from "effect/unstable/ai";
|
|
4
4
|
|
|
5
5
|
//#region src/tools/register-agent.ts
|
|
6
6
|
/**
|
|
@@ -31,16 +31,21 @@ import { DataReader, DataStore, deriveIdempotencyKey } from "@vitest-agent/sdk";
|
|
|
31
31
|
* `AGENT_ALREADY_REGISTERED` here so MCP clients see one shape per
|
|
32
32
|
* outcome.
|
|
33
33
|
*/
|
|
34
|
+
/**
|
|
35
|
+
* The `register_agent` tool's parameters.
|
|
36
|
+
*
|
|
37
|
+
* @public
|
|
38
|
+
*/
|
|
34
39
|
const RegisterAgentInput = Schema.Struct({
|
|
35
|
-
chatId: Schema.String,
|
|
36
|
-
conversationId: Schema.
|
|
37
|
-
hostKind: Schema.
|
|
38
|
-
agentType: Schema.String,
|
|
39
|
-
parentAgentId: Schema.
|
|
40
|
-
clientNonce: Schema.
|
|
41
|
-
startGitBranch: Schema.
|
|
42
|
-
startGitCommitSha: Schema.
|
|
43
|
-
startWorktreeDir: Schema.
|
|
40
|
+
chatId: Schema.String.annotate({ description: "Host's chat UUID (session_id from CC hook payload, etc.)" }),
|
|
41
|
+
conversationId: Schema.optionalKey(Schema.String).annotate({ description: "Canonical conversation UUID (from session-map mapConversation)" }),
|
|
42
|
+
hostKind: Schema.optionalKey(Schema.String).annotate({ description: "Host vendor identifier; defaults to 'claude-code'" }),
|
|
43
|
+
agentType: Schema.String.annotate({ description: "Agent type; must begin with the host-kind prefix" }),
|
|
44
|
+
parentAgentId: Schema.optionalKey(Schema.String).annotate({ description: "Parent agent UUID for subagent registrations" }),
|
|
45
|
+
clientNonce: Schema.optionalKey(Schema.String).annotate({ description: "Disambiguator for sibling-subagent registrations under the same parent; the server derives a deterministic default when omitted, which collapses parallel siblings into one row" }),
|
|
46
|
+
startGitBranch: Schema.optionalKey(Schema.String),
|
|
47
|
+
startGitCommitSha: Schema.optionalKey(Schema.String),
|
|
48
|
+
startWorktreeDir: Schema.optionalKey(Schema.String)
|
|
44
49
|
});
|
|
45
50
|
const RegisterAgentSuccess = Schema.Struct({
|
|
46
51
|
ok: Schema.Literal(true).annotate({ description: "Discriminant — `true` when the agent row was inserted (or an existing one recovered)." }),
|
|
@@ -65,12 +70,24 @@ const RegisterAgentFailure = Schema.Struct({
|
|
|
65
70
|
expectedPrefix: Schema.optional(Schema.String).annotate({ description: "Present only when `code = INVALID_AGENT_TYPE_PREFIX`. The required `<hostKind>-` prefix." })
|
|
66
71
|
})
|
|
67
72
|
}).annotate({ identifier: "RegisterAgentFailure" });
|
|
73
|
+
/**
|
|
74
|
+
* The `register_agent` tool's success payload.
|
|
75
|
+
*
|
|
76
|
+
* @public
|
|
77
|
+
*/
|
|
68
78
|
const RegisterAgentResult = Schema.Union([RegisterAgentSuccess, RegisterAgentFailure]).annotate({
|
|
69
79
|
identifier: "RegisterAgentResult",
|
|
70
80
|
title: "register_agent result",
|
|
71
81
|
description: "Discriminate on `ok`. The four failure codes are documented per their `code` literal."
|
|
72
82
|
});
|
|
73
|
-
|
|
83
|
+
/**
|
|
84
|
+
* Handler for {@link registerAgentTool}. Idempotency is the tool's own
|
|
85
|
+
* business rule (the `agents` idempotency key), not the generic
|
|
86
|
+
* `withIdempotency` combinator.
|
|
87
|
+
*
|
|
88
|
+
* @public
|
|
89
|
+
*/
|
|
90
|
+
const handleRegisterAgent = (input) => Effect.gen(function* () {
|
|
74
91
|
const expectedPrefix = `${input.hostKind ?? "claude-code"}-`;
|
|
75
92
|
if (!input.agentType.startsWith(expectedPrefix)) return {
|
|
76
93
|
ok: false,
|
|
@@ -86,55 +103,64 @@ const registerAgent = publicProcedure.input(Schema.toStandardSchemaV1(RegisterAg
|
|
|
86
103
|
parentAgentId: input.parentAgentId ?? null,
|
|
87
104
|
clientNonce
|
|
88
105
|
});
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
106
|
+
const reader = yield* DataReader;
|
|
107
|
+
const store = yield* DataStore;
|
|
108
|
+
const sessionOpt = yield* reader.getSessionByChatId(input.chatId);
|
|
109
|
+
if (Option.isNone(sessionOpt)) return {
|
|
110
|
+
ok: false,
|
|
111
|
+
error: {
|
|
112
|
+
code: "SESSION_NOT_FOUND",
|
|
113
|
+
message: `chat ${input.chatId} has not been registered; the host must call its SessionStart equivalent first`
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
const sessionRowId = sessionOpt.value.id;
|
|
117
|
+
const result = yield* store.registerAgent({
|
|
118
|
+
sessionId: sessionRowId,
|
|
119
|
+
agentType: input.agentType,
|
|
120
|
+
parentAgentId: input.parentAgentId ?? null,
|
|
121
|
+
conversationId: input.conversationId ?? null,
|
|
122
|
+
startedAt: Math.floor(Date.now() / 1e3),
|
|
123
|
+
...input.startGitBranch !== void 0 && { startGitBranch: input.startGitBranch },
|
|
124
|
+
...input.startGitCommitSha !== void 0 && { startGitCommitSha: input.startGitCommitSha },
|
|
125
|
+
...input.startWorktreeDir !== void 0 && { startWorktreeDir: input.startWorktreeDir },
|
|
126
|
+
idempotencyKey
|
|
127
|
+
}).pipe(Effect.catchTag("RegistrationConflictError", (e) => Effect.succeed({
|
|
128
|
+
_tag: "Conflict",
|
|
129
|
+
reason: e.reason
|
|
130
|
+
})));
|
|
131
|
+
if ("_tag" in result && result._tag === "Conflict") return {
|
|
132
|
+
ok: false,
|
|
133
|
+
error: {
|
|
134
|
+
code: "PARENT_AGENT_NOT_FOUND",
|
|
135
|
+
message: result.reason
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
if ("_tag" in result && result._tag === "IdempotencyHit") return {
|
|
139
|
+
ok: false,
|
|
140
|
+
error: {
|
|
141
|
+
code: "AGENT_ALREADY_REGISTERED",
|
|
142
|
+
message: "agent already registered for (chatId, agentType, parentAgentId, clientNonce); use existingAgentId",
|
|
143
|
+
existingAgentId: result.existingAgentId
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
return {
|
|
147
|
+
ok: true,
|
|
148
|
+
agentId: result.agentId,
|
|
149
|
+
conversationId: result.conversationId,
|
|
150
|
+
idempotencyKey: result.idempotencyKey
|
|
151
|
+
};
|
|
152
|
+
}).pipe(Effect.orDie);
|
|
153
|
+
/**
|
|
154
|
+
* The Effect-native `register_agent` tool.
|
|
155
|
+
*
|
|
156
|
+
* @public
|
|
157
|
+
*/
|
|
158
|
+
const registerAgentTool = Tool.make("register_agent", {
|
|
159
|
+
description: "Use when an LLM-agent invocation starts and must be recorded in the per-project store. Idempotent on (chatId, agentType, parentAgentId, clientNonce). Returns ok:true with agentId on insert, or ok:false with error.code='AGENT_ALREADY_REGISTERED'/'PARENT_AGENT_NOT_FOUND'/'SESSION_NOT_FOUND'/'INVALID_AGENT_TYPE_PREFIX' on the four documented failure modes. agentType must begin with the host-kind prefix (e.g., 'claude-code-main').",
|
|
160
|
+
parameters: RegisterAgentInput,
|
|
161
|
+
success: RegisterAgentResult,
|
|
162
|
+
dependencies: [DataReader, DataStore]
|
|
163
|
+
}).annotate(Tool.Title, "Register agent").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true);
|
|
138
164
|
|
|
139
165
|
//#endregion
|
|
140
|
-
export { RegisterAgentResult,
|
|
166
|
+
export { RegisterAgentInput, RegisterAgentResult, handleRegisterAgent, registerAgentTool };
|
package/tools/run-tests.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { RenderText } from "../annotations.js";
|
|
2
|
+
import { McpSession } from "../session.js";
|
|
2
3
|
import { createRequire } from "node:module";
|
|
3
4
|
import { Data, Effect, Schema, SchemaGetter, Semaphore } from "effect";
|
|
4
|
-
import {
|
|
5
|
+
import { DataReader, DataStore } from "@vitest-agent/engine";
|
|
6
|
+
import { Tool } from "effect/unstable/ai";
|
|
7
|
+
import { AgentReport, buildAgentReport, buildConsoleLeaks, coerceErrorField, collectConsoleLeakEntries, formatScopedCoverageNote } from "@vitest-agent/sdk";
|
|
5
8
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
6
9
|
import { execFile } from "node:child_process";
|
|
7
10
|
import { existsSync, mkdtempSync, rmSync } from "node:fs";
|
|
@@ -14,9 +17,9 @@ import { promisify } from "node:util";
|
|
|
14
17
|
|
|
15
18
|
//#region src/tools/run-tests.ts
|
|
16
19
|
const TagFilter = Schema.Struct({
|
|
17
|
-
all: Schema.
|
|
18
|
-
any: Schema.
|
|
19
|
-
none: Schema.
|
|
20
|
+
all: Schema.optionalKey(Schema.Array(Schema.String)).annotate({ description: "Require every listed tag" }),
|
|
21
|
+
any: Schema.optionalKey(Schema.Array(Schema.String)).annotate({ description: "Require at least one listed tag" }),
|
|
22
|
+
none: Schema.optionalKey(Schema.Array(Schema.String)).annotate({ description: "Exclude any listed tag" })
|
|
20
23
|
}).annotate({
|
|
21
24
|
identifier: "TagFilter",
|
|
22
25
|
description: "All three sub-filters AND together with `project` and `files`. `all` requires every listed tag on the test. `any` requires at least one. `none` excludes any test carrying a listed tag."
|
|
@@ -57,6 +60,11 @@ const RunTestsNoMatch = Schema.Struct({
|
|
|
57
60
|
resolvedExpression: Schema.NullOr(Schema.String)
|
|
58
61
|
})
|
|
59
62
|
}).annotate({ identifier: "RunTestsNoMatch" });
|
|
63
|
+
/**
|
|
64
|
+
* The `run_tests` tool's success payload.
|
|
65
|
+
*
|
|
66
|
+
* @public
|
|
67
|
+
*/
|
|
60
68
|
const RunTestsResult = Schema.Union([
|
|
61
69
|
RunTestsOk,
|
|
62
70
|
RunTestsTimeout,
|
|
@@ -155,7 +163,7 @@ function ensureStdioPatched() {
|
|
|
155
163
|
/**
|
|
156
164
|
* Run `fn` with `process.stdout.write` and `process.stderr.write`
|
|
157
165
|
* diverted to `stream.write` for code executing inside the call's
|
|
158
|
-
* async context. Code in other async contexts (concurrent
|
|
166
|
+
* async context. Code in other async contexts (concurrent tool
|
|
159
167
|
* procedure handlers, the MCP stdio transport) sees the original
|
|
160
168
|
* writes unchanged.
|
|
161
169
|
*
|
|
@@ -366,9 +374,6 @@ function resolveVitestNodeEntry(root) {
|
|
|
366
374
|
*/
|
|
367
375
|
const vitestLoader = { load: (entry) => import(entry) };
|
|
368
376
|
const runTestsSemaphore = Effect.runSync(Semaphore.make(1));
|
|
369
|
-
function serializeRunTests(fn) {
|
|
370
|
-
return Effect.runPromise(Semaphore.withPermit(runTestsSemaphore, Effect.promise(() => fn())));
|
|
371
|
-
}
|
|
372
377
|
/**
|
|
373
378
|
* Coerce unknown Vitest unhandled errors into VitestModuleError shape.
|
|
374
379
|
*
|
|
@@ -511,19 +516,31 @@ function formatReportMarkdown(report, classifications, scopedNote) {
|
|
|
511
516
|
}
|
|
512
517
|
return lines.join("\n");
|
|
513
518
|
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
519
|
+
/**
|
|
520
|
+
* The `run_tests` tool's parameters. `tags` and `_sessionContext` are
|
|
521
|
+
* nested structs and are served strict at their own level (issue #243).
|
|
522
|
+
*
|
|
523
|
+
* @public
|
|
524
|
+
*/
|
|
525
|
+
const RunTestsInput = Schema.Struct({
|
|
526
|
+
files: Schema.optionalKey(Schema.Array(Schema.String)).annotate({ description: "Test file paths to run" }),
|
|
527
|
+
project: Schema.optionalKey(Schema.String).annotate({ description: "Project name to filter" }),
|
|
528
|
+
projectRoot: Schema.optionalKey(Schema.String).annotate({ description: "Explicit Vitest root for this call, used verbatim. Omit it to get the config-anchored default (walk up from the server's boot dir for a vitest/vite config, bounded at the git root). Prefer an absolute path; a relative path is resolved against ctx.cwd, not the server process's cwd. Validated: must be an existing directory in the same git repository as ctx.cwd (same git-common-dir, e.g. a sibling worktree). Rejected with { kind: 'error' } naming both paths otherwise." }),
|
|
529
|
+
tags: Schema.optionalKey(TagFilter).annotate({ description: "Structured tag filter; all/any/none AND together with each other and with project/files" }),
|
|
530
|
+
passWithNoTests: Schema.optionalKey(Schema.Boolean).annotate({ description: "Per-call override of Vitest's native test.passWithNoTests" }),
|
|
531
|
+
timeout: Schema.optionalKey(Schema.Finite).annotate({ description: "Timeout in seconds (default: 120)" }),
|
|
532
|
+
_sessionContext: Schema.optionalKey(Schema.Struct({
|
|
522
533
|
chatId: Schema.String,
|
|
523
534
|
conversationId: Schema.String,
|
|
524
535
|
mainAgentId: Schema.String
|
|
525
|
-
}))
|
|
526
|
-
})
|
|
536
|
+
})).annotate({ description: "Hook-injected session attribution UUIDs; do not pass manually." })
|
|
537
|
+
});
|
|
538
|
+
/**
|
|
539
|
+
* The run body, promise-shaped because it drives Vitest's promise API and
|
|
540
|
+
* the AsyncLocalStorage stdio capture; every failure is folded into the
|
|
541
|
+
* `{ kind: "error" }` envelope, so it never rejects.
|
|
542
|
+
*/
|
|
543
|
+
const runTestsBody = async (input, ctx) => {
|
|
527
544
|
const files = input.files ? sanitizeTestArgs(input.files) : [];
|
|
528
545
|
const project = input.project ? sanitizeTestArgs([input.project])[0] : void 0;
|
|
529
546
|
const tagsInput = input.tags;
|
|
@@ -614,7 +631,7 @@ const runTests = publicProcedure.input(Schema.toStandardSchemaV1(Schema.Struct({
|
|
|
614
631
|
} : baseReport;
|
|
615
632
|
let classifications;
|
|
616
633
|
try {
|
|
617
|
-
classifications = await ctx.
|
|
634
|
+
classifications = await ctx.runPromise(Effect.gen(function* () {
|
|
618
635
|
const reader = yield* DataReader;
|
|
619
636
|
const projects = project ? [project] : yield* reader.getRunsByProject().pipe(Effect.map((rs) => rs.map((r) => r.project)));
|
|
620
637
|
const entries = [];
|
|
@@ -626,7 +643,7 @@ const runTests = publicProcedure.input(Schema.toStandardSchemaV1(Schema.Struct({
|
|
|
626
643
|
}));
|
|
627
644
|
} catch {}
|
|
628
645
|
const chatId = ctx.currentSessionId.get();
|
|
629
|
-
if (chatId !== null) ctx.
|
|
646
|
+
if (chatId !== null) ctx.runPromise(Effect.gen(function* () {
|
|
630
647
|
yield* (yield* DataStore).associateLatestRunWithSession({
|
|
631
648
|
chatId,
|
|
632
649
|
invocationMethod: "mcp"
|
|
@@ -679,7 +696,43 @@ const runTests = publicProcedure.input(Schema.toStandardSchemaV1(Schema.Struct({
|
|
|
679
696
|
} catch {}
|
|
680
697
|
}
|
|
681
698
|
}
|
|
682
|
-
}
|
|
699
|
+
};
|
|
700
|
+
/**
|
|
701
|
+
* Handler for {@link runTestsTool}. Serialized through a one-permit
|
|
702
|
+
* semaphore: the body assigns the active attribution UUIDs into
|
|
703
|
+
* `process.env.VITEST_AGENT_*` and then awaits `createVitest` /
|
|
704
|
+
* `vitest.start`, which spawns the worker pool that snapshots env at
|
|
705
|
+
* spawn time — two interleaved calls would race, attributing one call's
|
|
706
|
+
* results to the other's agent.
|
|
707
|
+
*
|
|
708
|
+
* @public
|
|
709
|
+
*/
|
|
710
|
+
const handleRunTests = (input) => Effect.gen(function* () {
|
|
711
|
+
const session = yield* McpSession;
|
|
712
|
+
const services = yield* Effect.context();
|
|
713
|
+
const ctx = {
|
|
714
|
+
cwd: session.cwd,
|
|
715
|
+
currentSessionId: session.currentSessionId,
|
|
716
|
+
sessionContext: session.sessionContext,
|
|
717
|
+
runPromise: (effect) => Effect.runPromise(Effect.provideContext(effect, services))
|
|
718
|
+
};
|
|
719
|
+
return yield* Semaphore.withPermit(runTestsSemaphore, Effect.promise(() => runTestsBody(input, ctx)));
|
|
720
|
+
});
|
|
721
|
+
/**
|
|
722
|
+
* The Effect-native `run_tests` tool.
|
|
723
|
+
*
|
|
724
|
+
* @public
|
|
725
|
+
*/
|
|
726
|
+
const runTestsTool = Tool.make("run_tests", {
|
|
727
|
+
description: "Use to run Vitest tests, with optional file, project, and tag filters. structuredContent carries the typed AgentReport plus per-test classifications (discriminate on `kind`: ok, timeout, error, no-match). Unknown parameters are rejected — accepted keys are files, project, tags, passWithNoTests, timeout, projectRoot. When projectRoot is omitted, the server anchors the Vitest root at the directory of the vitest (or vite) config Vitest would load anyway, walking up from its boot dir and stopping at the git root — so a server booted inside a package subtree still resolves the root config's relative globalSetup/setupFiles correctly. projectRoot overrides that for this call and is used verbatim, but only after validation: it must be an existing directory belonging to the same git repository as ctx.cwd (checked via `git rev-parse --git-common-dir`, which is identical across a repo and all its worktrees, including a sibling worktree checked out from the same repo). A path in a different repository, or a non-existent path, is rejected with `{ kind: \"error\" }` naming both paths — never a silent fallback to ctx.cwd. The resolved root actually used is always echoed back on success. The legacy format=json arg is dropped — structuredContent supersedes it.",
|
|
728
|
+
parameters: RunTestsInput,
|
|
729
|
+
success: RunTestsResult,
|
|
730
|
+
dependencies: [
|
|
731
|
+
McpSession,
|
|
732
|
+
DataReader,
|
|
733
|
+
DataStore
|
|
734
|
+
]
|
|
735
|
+
}).annotate(Tool.Title, "Run tests").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, false).annotate(RenderText, (encoded) => formatRunTestsMarkdown(encoded));
|
|
683
736
|
|
|
684
737
|
//#endregion
|
|
685
|
-
export {
|
|
738
|
+
export { RunTestsInput, RunTestsResult, coerceErrors, composeTagExpression, formatNoMatchMarkdown, formatReportMarkdown, formatRunTestsMarkdown, handleRunTests, makeCoverageDirOverride, readDiscoveryLastScannedAt, resolveAnchoredConfigFile, resolveConfigAnchoredRoot, resolveGitCommonDir, resolveVitestNodeEntry, runTestsTool, sanitizeTestArgs, validateProjectRoot, vitestLoader, withStdioCaptured };
|
package/tools/settings-list.js
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { RenderText } from "../annotations.js";
|
|
2
2
|
import { Effect, Schema, SchemaGetter } from "effect";
|
|
3
|
-
import { DataReader } from "@vitest-agent/
|
|
3
|
+
import { DataReader } from "@vitest-agent/engine";
|
|
4
|
+
import { Tool } from "effect/unstable/ai";
|
|
4
5
|
|
|
5
6
|
//#region src/tools/settings-list.ts
|
|
6
|
-
/**
|
|
7
|
-
* `settings_list` MCP tool — Schema-driven implementation.
|
|
8
|
-
*
|
|
9
|
-
* @packageDocumentation
|
|
10
|
-
*/
|
|
11
7
|
const SettingsRow = Schema.Struct({
|
|
12
8
|
hash: Schema.String.annotate({ description: "Stable SHA-1 of the captured Vitest settings; FK target on test_runs." }),
|
|
13
9
|
capturedAt: Schema.String.annotate({ description: "ISO-8601 timestamp the settings row was first written." })
|
|
14
10
|
}).annotate({ identifier: "SettingsListRow" });
|
|
11
|
+
/**
|
|
12
|
+
* The `settings_list` tool's success payload.
|
|
13
|
+
*
|
|
14
|
+
* @public
|
|
15
|
+
*/
|
|
15
16
|
const SettingsListResult = Schema.Struct({
|
|
16
17
|
count: Schema.Number,
|
|
17
18
|
settings: Schema.Array(SettingsRow).annotate({ description: "Distinct captured settings hashes the reporter has written, newest first." })
|
|
@@ -35,13 +36,29 @@ const SettingsListAsMarkdown = SettingsListResult.pipe(Schema.decodeTo(Schema.St
|
|
|
35
36
|
decode: SchemaGetter.transform((data) => formatSettingsListMarkdown(data)),
|
|
36
37
|
encode: SchemaGetter.forbidden(() => "SettingsListAsMarkdown is one-way.")
|
|
37
38
|
}));
|
|
38
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Handler for {@link settingsListTool}.
|
|
41
|
+
*
|
|
42
|
+
* @public
|
|
43
|
+
*/
|
|
44
|
+
const handleSettingsList = () => Effect.gen(function* () {
|
|
39
45
|
const settings = yield* (yield* DataReader).listSettings();
|
|
40
46
|
return {
|
|
41
47
|
count: settings.length,
|
|
42
48
|
settings
|
|
43
49
|
};
|
|
44
|
-
}))
|
|
50
|
+
}).pipe(Effect.orDie);
|
|
51
|
+
/**
|
|
52
|
+
* The Effect-native `settings_list` tool. No parameters (the default
|
|
53
|
+
* `Tool.EmptyParams` serves as a strict empty object).
|
|
54
|
+
*
|
|
55
|
+
* @public
|
|
56
|
+
*/
|
|
57
|
+
const settingsListTool = Tool.make("settings_list", {
|
|
58
|
+
description: "Use when you need every captured settings snapshot and its hash. Returns markdown in content[] and a typed JSON object in structuredContent ({ count, settings[] }).",
|
|
59
|
+
success: SettingsListResult,
|
|
60
|
+
dependencies: [DataReader]
|
|
61
|
+
}).annotate(Tool.Title, "Settings list").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true).annotate(RenderText, (encoded) => formatSettingsListMarkdown(encoded));
|
|
45
62
|
|
|
46
63
|
//#endregion
|
|
47
|
-
export {
|
|
64
|
+
export { SettingsListResult, formatSettingsListMarkdown, handleSettingsList, settingsListTool };
|
package/tools/status.js
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { RenderText } from "../annotations.js";
|
|
2
2
|
import { Effect, Option, Schema, SchemaGetter } from "effect";
|
|
3
|
-
import {
|
|
3
|
+
import { DataReader } from "@vitest-agent/engine";
|
|
4
|
+
import { Tool } from "effect/unstable/ai";
|
|
5
|
+
import { CacheManifestEntry } from "@vitest-agent/sdk";
|
|
4
6
|
|
|
5
7
|
//#region src/tools/status.ts
|
|
6
|
-
/**
|
|
7
|
-
* `test_status` MCP tool — Schema-driven implementation.
|
|
8
|
-
*
|
|
9
|
-
* @packageDocumentation
|
|
10
|
-
*/
|
|
11
8
|
const StatusAvailable = Schema.Struct({
|
|
12
9
|
dataAvailable: Schema.Literal(true).annotate({ description: "Discriminant — `true` when at least one project entry exists in the manifest." }),
|
|
13
10
|
manifestUpdatedAt: Schema.String,
|
|
@@ -19,6 +16,11 @@ const StatusAbsent = Schema.Struct({
|
|
|
19
16
|
projectFilter: Schema.optional(Schema.String),
|
|
20
17
|
reason: Schema.Literals(["no_manifest", "project_filter_empty"])
|
|
21
18
|
}).annotate({ identifier: "TestStatusAbsent" });
|
|
19
|
+
/**
|
|
20
|
+
* The `test_status` tool's success payload.
|
|
21
|
+
*
|
|
22
|
+
* @public
|
|
23
|
+
*/
|
|
22
24
|
const TestStatusResult = Schema.Union([StatusAvailable, StatusAbsent]).annotate({
|
|
23
25
|
identifier: "TestStatusResult",
|
|
24
26
|
title: "test_status result",
|
|
@@ -47,7 +49,19 @@ const TestStatusAsMarkdown = TestStatusResult.pipe(Schema.decodeTo(Schema.String
|
|
|
47
49
|
decode: SchemaGetter.transform((data) => formatTestStatusMarkdown(data)),
|
|
48
50
|
encode: SchemaGetter.forbidden(() => "TestStatusAsMarkdown is one-way.")
|
|
49
51
|
}));
|
|
50
|
-
|
|
52
|
+
/**
|
|
53
|
+
* The `test_status` tool's parameters.
|
|
54
|
+
*
|
|
55
|
+
* @public
|
|
56
|
+
*/
|
|
57
|
+
const TestStatusInput = Schema.Struct({ project: Schema.optionalKey(Schema.String).annotate({ description: "Filter to a specific project" }) });
|
|
58
|
+
/**
|
|
59
|
+
* Handler for {@link testStatusTool}: the single implementation of the
|
|
60
|
+
* tool.
|
|
61
|
+
*
|
|
62
|
+
* @public
|
|
63
|
+
*/
|
|
64
|
+
const handleTestStatus = (input) => Effect.gen(function* () {
|
|
51
65
|
const manifestOpt = yield* (yield* DataReader).getManifest();
|
|
52
66
|
if (Option.isNone(manifestOpt)) return {
|
|
53
67
|
dataAvailable: false,
|
|
@@ -67,7 +81,18 @@ const testStatus = publicProcedure.input(Schema.toStandardSchemaV1(Schema.Struct
|
|
|
67
81
|
...input.project !== void 0 && { projectFilter: input.project },
|
|
68
82
|
entries
|
|
69
83
|
};
|
|
70
|
-
}))
|
|
84
|
+
}).pipe(Effect.orDie);
|
|
85
|
+
/**
|
|
86
|
+
* The Effect-native `test_status` tool.
|
|
87
|
+
*
|
|
88
|
+
* @public
|
|
89
|
+
*/
|
|
90
|
+
const testStatusTool = Tool.make("test_status", {
|
|
91
|
+
description: "Use when you need each project's current pass/fail state from the most recent run. Returns markdown in content[] and a typed JSON object in structuredContent ({ dataAvailable, manifestUpdatedAt, projectFilter?, entries[] } or absent variant).",
|
|
92
|
+
parameters: TestStatusInput,
|
|
93
|
+
success: TestStatusResult,
|
|
94
|
+
dependencies: [DataReader]
|
|
95
|
+
}).annotate(Tool.Title, "Test status").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true).annotate(RenderText, (encoded) => formatTestStatusMarkdown(encoded));
|
|
71
96
|
|
|
72
97
|
//#endregion
|
|
73
|
-
export {
|
|
98
|
+
export { TestStatusInput, TestStatusResult, formatTestStatusMarkdown, handleTestStatus, testStatusTool };
|