@vitest-agent/mcp 5.2.9 → 5.3.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 +543 -450
- package/package.json +3 -3
- package/tools/help.js +4 -4
- package/tools/history.js +17 -6
- package/tools/run-tests.js +44 -7
- package/tools/status.js +22 -5
- package/tools/triage-brief.js +1 -1
- package/version.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vitest-agent/mcp",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.3.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Model Context Protocol server for vitest-agent. Exposes 30 tools for agent access to test data, TDD lifecycle, and session management.",
|
|
6
6
|
"keywords": [
|
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
"@effected/git": "^0.25.0",
|
|
50
50
|
"@effected/mcp": "^0.9.0",
|
|
51
51
|
"@effected/walker": "^0.19.0",
|
|
52
|
-
"@vitest-agent/engine": "
|
|
53
|
-
"@vitest-agent/sdk": "
|
|
52
|
+
"@vitest-agent/engine": "2.0.0",
|
|
53
|
+
"@vitest-agent/sdk": "8.0.0",
|
|
54
54
|
"effect": "^4.0.6"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
package/tools/help.js
CHANGED
|
@@ -36,11 +36,11 @@ const HELP_TEXT = `# vitest-agent MCP Tools
|
|
|
36
36
|
|
|
37
37
|
| Tool | Parameters | Description |
|
|
38
38
|
| ---- | ---------- | ----------- |
|
|
39
|
-
| \`test_status\` | \`project?\` | Per-project test pass/fail state |
|
|
39
|
+
| \`test_status\` | \`project?\` | Per-project test pass/fail state; an entry carries \`strayOutput\` when the latest run wrote straight to the terminal |
|
|
40
40
|
| \`test_overview\` | \`project?\` | Test landscape with run metrics |
|
|
41
41
|
| \`test_coverage\` | \`project?\` | Coverage gaps with uncovered lines |
|
|
42
42
|
| \`file_coverage\` | \`filePath\`, \`project?\` | Per-file coverage with uncovered lines and related tests |
|
|
43
|
-
| \`test_history\` | \`project\` | Flaky/persistent/recovered tests |
|
|
43
|
+
| \`test_history\` | \`project\`, \`testName?\`, \`modulePath?\`, \`limit?\` | Flaky/persistent/recovered tests, plus \`strayOutput\`: how many of the last 10 runs wrote straight to the terminal |
|
|
44
44
|
| \`test_trends\` | \`project\`, \`limit?\` | Coverage trajectory over time |
|
|
45
45
|
| \`test_errors\` | \`project\`, \`errorName?\` | 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)\` |
|
|
46
46
|
| \`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 |
|
|
@@ -73,7 +73,7 @@ const HELP_TEXT = `# vitest-agent MCP Tools
|
|
|
73
73
|
|
|
74
74
|
| Tool | Parameters | Description |
|
|
75
75
|
| ---- | ---------- | ----------- |
|
|
76
|
-
| \`run_tests\` | \`files?\`, \`project?\`, \`tags?\`, \`passWithNoTests?\`, \`timeout?\`, \`
|
|
76
|
+
| \`run_tests\` | \`files?\`, \`project?\`, \`tags?\`, \`passWithNoTests?\`, \`timeout?\`, \`projectRoot?\` | Run vitest with optional filters. \`tags\` is a structured \`{ all?, any?, none? }\` filter that intersects with \`project\` and \`files\` via AND. When the resolved filter set matches zero test cases the result discriminator is \`no-match\` (carrying the resolved filter context) rather than \`ok\`. \`passWithNoTests\` is a per-call override of Vitest's project-level \`test.passWithNoTests\` policy. A \`kind: "error"\` result with \`reason: "stale-install"\` means the vitest install was replaced under this long-lived server; restart the MCP server (\`/mcp\`) instead of retrying |
|
|
77
77
|
| \`register_agent\` | \`sessionId\`, \`agentType\`, \`hostKind?\`, \`parentAgentId?\`, \`clientNonce?\`, \`startGitBranch?\`, \`startGitCommitSha?\`, \`startWorktreeDir?\` | Idempotent agent invocation registration |
|
|
78
78
|
|
|
79
79
|
## Diagnostics
|
|
@@ -85,7 +85,7 @@ const HELP_TEXT = `# vitest-agent MCP Tools
|
|
|
85
85
|
| \`failure_signature_get\` | \`hash\` | Stable failure signature with recent example errors |
|
|
86
86
|
| \`turn_search\` | \`sessionId?\`, \`since?\`, \`type?\`, \`limit?\` | Search turns (default limit 100) |
|
|
87
87
|
| \`acceptance_metrics\` | _(none)_ | Four spec Annex A acceptance metrics |
|
|
88
|
-
| \`triage_brief\` | \`project?\`, \`maxLines?\` | Orientation triage |
|
|
88
|
+
| \`triage_brief\` | \`project?\`, \`maxLines?\` | Orientation triage (recent runs, stray output, sessions, metrics) |
|
|
89
89
|
| \`wrapup_prompt\` | \`sessionId?\`, \`chatId?\`, \`kind?\`, \`userPromptHint?\` | Tailored wrap-up prompt |
|
|
90
90
|
| \`commit_changes\` | \`sha?\` | Commit metadata + changed files |
|
|
91
91
|
|
package/tools/history.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Effect, Schema } from "effect";
|
|
2
2
|
import { DataReader } from "@vitest-agent/engine";
|
|
3
3
|
import { Tool } from "effect/ai";
|
|
4
|
-
import { HistoryRecord } from "@vitest-agent/sdk";
|
|
4
|
+
import { HistoryRecord, StrayOutput } from "@vitest-agent/sdk";
|
|
5
5
|
|
|
6
6
|
// src/tools/history.ts
|
|
7
7
|
const FlakyTestRow = Schema.Struct({
|
|
@@ -36,6 +36,14 @@ const RecoveredTestRow = Schema.Struct({
|
|
|
36
36
|
identifier: "RecoveredTestRow",
|
|
37
37
|
description: "A test whose latest run passed after the previous one failed."
|
|
38
38
|
});
|
|
39
|
+
const StrayOutputHistoryRow = Schema.Struct({
|
|
40
|
+
runs: Schema.Finite.annotate({ description: "Recent runs considered (the newest ones, up to 10)." }),
|
|
41
|
+
runsWithStrayOutput: Schema.Finite.annotate({ description: "How many of those runs wrote to the terminal past Vitest's console capture." }),
|
|
42
|
+
latest: Schema.NullOr(StrayOutput).annotate({ description: "The most recent run's stray output (line counts, bytes, first samples); `null` when it recorded none." })
|
|
43
|
+
}).annotate({
|
|
44
|
+
identifier: "StrayOutputHistoryRow",
|
|
45
|
+
description: "How often recent runs of the project wrote straight to the terminal (a test or a child process with inherited stdio). A run-level signal: nothing attributes it to one test."
|
|
46
|
+
});
|
|
39
47
|
/**
|
|
40
48
|
* The `test_history` tool's success payload.
|
|
41
49
|
*
|
|
@@ -47,7 +55,8 @@ const TestHistoryResult = Schema.Struct({
|
|
|
47
55
|
history: HistoryRecord.annotate({ description: "Raw per-test history record (stored in `test_runs` joins)." }),
|
|
48
56
|
flaky: Schema.Array(FlakyTestRow).annotate({ description: "Tests with mixed pass/fail outcomes recently." }),
|
|
49
57
|
persistent: Schema.Array(PersistentFailureRow).annotate({ description: "Tests failing across consecutive runs." }),
|
|
50
|
-
recovered: Schema.Array(RecoveredTestRow).annotate({ description: "Tests that just transitioned from failing to passing in the last run." })
|
|
58
|
+
recovered: Schema.Array(RecoveredTestRow).annotate({ description: "Tests that just transitioned from failing to passing in the last run." }),
|
|
59
|
+
strayOutput: StrayOutputHistoryRow
|
|
51
60
|
}).annotate({
|
|
52
61
|
identifier: "TestHistoryResult",
|
|
53
62
|
title: "test_history result",
|
|
@@ -79,10 +88,11 @@ const handleTestHistory = (input) => Effect.gen(function* () {
|
|
|
79
88
|
...scopeOptions,
|
|
80
89
|
...input.limit !== void 0 && { limit: input.limit }
|
|
81
90
|
};
|
|
82
|
-
const [history, flaky, persistent] = yield* Effect.all([
|
|
91
|
+
const [history, flaky, persistent, strayOutput] = yield* Effect.all([
|
|
83
92
|
reader.getHistory(input.project, historyOptions),
|
|
84
93
|
reader.getFlaky(input.project, scopeOptions),
|
|
85
|
-
reader.getPersistentFailures(input.project, scopeOptions)
|
|
94
|
+
reader.getPersistentFailures(input.project, scopeOptions),
|
|
95
|
+
reader.getStrayOutputHistory({ project: input.project })
|
|
86
96
|
], { concurrency: "unbounded" });
|
|
87
97
|
const recovered = history.tests.filter((t) => {
|
|
88
98
|
const runs = t.runs;
|
|
@@ -102,7 +112,8 @@ const handleTestHistory = (input) => Effect.gen(function* () {
|
|
|
102
112
|
history,
|
|
103
113
|
flaky,
|
|
104
114
|
persistent,
|
|
105
|
-
recovered
|
|
115
|
+
recovered,
|
|
116
|
+
strayOutput
|
|
106
117
|
};
|
|
107
118
|
}).pipe(Effect.orDie);
|
|
108
119
|
/**
|
|
@@ -111,7 +122,7 @@ const handleTestHistory = (input) => Effect.gen(function* () {
|
|
|
111
122
|
* @public
|
|
112
123
|
*/
|
|
113
124
|
const testHistoryTool = Tool.make("test_history", {
|
|
114
|
-
description: "Use when failures recur and you need flaky, persistent, and recovered test classifications. Returns a typed JSON object in structuredContent (project, hasData, history, flaky[], persistent[], recovered[]). Optional testName/modulePath narrow to a single test; limit caps runs kept per test (default 20) — omit all three only when you actually need the whole project's history.",
|
|
125
|
+
description: "Use when failures recur and you need flaky, persistent, and recovered test classifications. Returns a typed JSON object in structuredContent (project, hasData, history, flaky[], persistent[], recovered[], strayOutput: how many of the last 10 runs wrote straight to the terminal). Optional testName/modulePath narrow to a single test; limit caps runs kept per test (default 20) — omit all three only when you actually need the whole project's history.",
|
|
115
126
|
parameters: TestHistoryInput,
|
|
116
127
|
success: TestHistoryResult,
|
|
117
128
|
dependencies: [DataReader]
|
package/tools/run-tests.js
CHANGED
|
@@ -4,7 +4,7 @@ import { ToolOutputSchema } from "@effected/mcp";
|
|
|
4
4
|
import { Data, Effect, Layer, Option, Path, Result, Schema, Semaphore } from "effect";
|
|
5
5
|
import { DataReader, DataStore } from "@vitest-agent/engine";
|
|
6
6
|
import { Tool } from "effect/ai";
|
|
7
|
-
import { AgentReport, buildAgentReport, buildConsoleLeaks, coerceErrorField, collectConsoleLeakEntries, formatScopedCoverageNote, readStrayOutput } from "@vitest-agent/sdk";
|
|
7
|
+
import { AgentReport, SuggestedActionRecord, buildAgentReport, buildConsoleLeaks, coerceErrorField, collectConsoleLeakEntries, formatScopedCoverageNote, readStrayOutput } from "@vitest-agent/sdk";
|
|
8
8
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
9
9
|
import { existsSync, mkdtempSync, rmSync } from "node:fs";
|
|
10
10
|
import { stat } from "node:fs/promises";
|
|
@@ -43,15 +43,26 @@ const RunTestsOk = Schema.Struct({
|
|
|
43
43
|
report: AgentReport.annotate({ description: "Full AgentReport including pass/fail counts and per-module errors." }),
|
|
44
44
|
classifications: Schema.Record(Schema.String, Schema.String).annotate({ description: "Per-test classification labels: stable, new-failure, persistent, flaky, recovered." }),
|
|
45
45
|
discoveryLastScannedAt: Schema.optional(Schema.NullOr(Schema.String)).annotate({ description: "ISO timestamp of the most recent real disk scan performed by discoverProjects() in this process (issue #100). `null`/absent means discovery has not scanned disk in this process yet (e.g. a config that doesn't call AgentPlugin.discover()). A stale-looking test count is self-explaining when compared against this value." }),
|
|
46
|
-
scopedNote: Schema.optional(Schema.NullOr(Schema.String)).annotate({ description: "Set (issue #160) when this call was scoped to a subset of the project's test files (files/project/tags filter supplied). Vitest enforces coverage.thresholds against the whole-project denominator regardless of how many files ran, so a scoped run's threshold check is unreliable; this note explains why no coverage verdict should be trusted for this call. `null`/absent means the run was unscoped." })
|
|
46
|
+
scopedNote: Schema.optional(Schema.NullOr(Schema.String)).annotate({ description: "Set (issue #160) when this call was scoped to a subset of the project's test files (files/project/tags filter supplied). Vitest enforces coverage.thresholds against the whole-project denominator regardless of how many files ran, so a scoped run's threshold check is unreliable; this note explains why no coverage verdict should be trusted for this call. `null`/absent means the run was unscoped." }),
|
|
47
|
+
suggestedActions: Schema.optionalKey(Schema.Array(SuggestedActionRecord)).annotate({ description: "Run-level suggested actions the plugin raised at config time (issue #544): one entry per coverage-config diagnostic the plugin's ConfigValidation reported, such as a coverageTargets metric with no matching coverage.thresholds floor (severity warn). The same entries print on the plugin's stderr for a human and in the rendered report's Actions trailer. Absent when the project's config validated clean or does not load the plugin." })
|
|
47
48
|
}).annotate({ identifier: "RunTestsOk" });
|
|
48
49
|
const RunTestsTimeout = Schema.Struct({
|
|
49
50
|
kind: Schema.Literal("timeout"),
|
|
50
51
|
timeoutSeconds: Schema.Number
|
|
51
52
|
}).annotate({ identifier: "RunTestsTimeout" });
|
|
53
|
+
/**
|
|
54
|
+
* The machine-readable reasons a `run_tests` error result may carry. Only
|
|
55
|
+
* failures an agent must act on in a specific way get one; the rest stay
|
|
56
|
+
* message-only. `stale-install` (issue #461): the `vitest/node` entry this
|
|
57
|
+
* long-lived server resolved for the project no longer exists on disk
|
|
58
|
+
* because the dependency tree was reinstalled under it — the remedy is to
|
|
59
|
+
* restart the MCP server (`/mcp` in Claude Code), not to retry the call.
|
|
60
|
+
*/
|
|
61
|
+
const RUN_TESTS_ERROR_REASONS = ["stale-install"];
|
|
52
62
|
const RunTestsError = Schema.Struct({
|
|
53
63
|
kind: Schema.Literal("error"),
|
|
54
|
-
message: Schema.String
|
|
64
|
+
message: Schema.String,
|
|
65
|
+
reason: Schema.optionalKey(Schema.Literals(RUN_TESTS_ERROR_REASONS)).annotate({ description: "Set when the failure has a specific remedy. `stale-install`: the vitest/node entry this server resolved for the project vanished from disk (the dependency tree was reinstalled under the long-lived server); `message` names the missing path. Restart the MCP server (/mcp in Claude Code) — retrying the call cannot fix it. Absent for a message-only error." })
|
|
55
66
|
}).annotate({ identifier: "RunTestsError" });
|
|
56
67
|
const RunTestsNoMatch = Schema.Struct({
|
|
57
68
|
kind: Schema.Literal("no-match").annotate({ description: "Discriminant — the resolved filter set matched zero test cases. Tests did not run; this is independent of passWithNoTests policy." }),
|
|
@@ -115,6 +126,22 @@ function readDiscoveryLastScannedAt() {
|
|
|
115
126
|
const value = globalThis[DISCOVERY_LAST_SCAN_SYMBOL];
|
|
116
127
|
return typeof value === "string" ? value : void 0;
|
|
117
128
|
}
|
|
129
|
+
const CONFIG_SUGGESTED_ACTIONS_SYMBOL = Symbol.for("vitest-agent:config-suggested-actions");
|
|
130
|
+
/**
|
|
131
|
+
* Reads the config-derived suggested actions the plugin published on a
|
|
132
|
+
* Vitest instance. `undefined` when none were published (the project does
|
|
133
|
+
* not load the plugin, or its config validated clean). Each entry is
|
|
134
|
+
* decoded against `SuggestedActionRecord` so a foreign shape can never
|
|
135
|
+
* reach the wire.
|
|
136
|
+
*
|
|
137
|
+
* @internal
|
|
138
|
+
*/
|
|
139
|
+
function readConfigSuggestedActions(vitest) {
|
|
140
|
+
const value = vitest[CONFIG_SUGGESTED_ACTIONS_SYMBOL];
|
|
141
|
+
if (!Array.isArray(value) || value.length === 0) return void 0;
|
|
142
|
+
const decoded = Schema.decodeUnknownResult(Schema.Array(SuggestedActionRecord))(value);
|
|
143
|
+
return Result.isSuccess(decoded) ? decoded.success : void 0;
|
|
144
|
+
}
|
|
118
145
|
/**
|
|
119
146
|
* Per-invocation coverage temp directory. Vitest's v8 provider `rm -rf`s the
|
|
120
147
|
* shared `coverage/` reports directory at run start (`clean: true` default),
|
|
@@ -333,6 +360,12 @@ async function validateProjectRoot(projectRoot, ctxCwd) {
|
|
|
333
360
|
* resolution throws (e.g. a project root with no local vitest install) so
|
|
334
361
|
* that case keeps working exactly as it did before this fix.
|
|
335
362
|
*
|
|
363
|
+
* The property this buys — one module instance shared by the server and the
|
|
364
|
+
* project's test files — is pinned by `vitest-node-module-identity.test.ts`
|
|
365
|
+
* (issue #387), which asserts realpath AND `Object.is` identity against the
|
|
366
|
+
* live repo layout, so a pnpm layout change that reintroduces two copies
|
|
367
|
+
* fails there rather than as a snapshot failure in a consumer's run.
|
|
368
|
+
*
|
|
336
369
|
* @internal exported for tests
|
|
337
370
|
*/
|
|
338
371
|
function resolveVitestNodeEntry(root) {
|
|
@@ -498,7 +531,8 @@ function toOkPayload(args) {
|
|
|
498
531
|
report: args.report,
|
|
499
532
|
classifications: args.classifications ? Object.fromEntries(args.classifications) : {},
|
|
500
533
|
discoveryLastScannedAt: args.discoveryLastScannedAt ?? null,
|
|
501
|
-
scopedNote: args.scopedNote
|
|
534
|
+
scopedNote: args.scopedNote,
|
|
535
|
+
...args.suggestedActions !== void 0 && args.suggestedActions.length > 0 ? { suggestedActions: args.suggestedActions } : {}
|
|
502
536
|
};
|
|
503
537
|
}
|
|
504
538
|
/**
|
|
@@ -544,6 +578,7 @@ const runTestsBody = async (input, ctx) => {
|
|
|
544
578
|
const staleMessage = staleInstallMessage(vitestEntry);
|
|
545
579
|
if (staleMessage !== null) return {
|
|
546
580
|
kind: "error",
|
|
581
|
+
reason: "stale-install",
|
|
547
582
|
message: staleMessage
|
|
548
583
|
};
|
|
549
584
|
const timeoutMs = (input.timeout ?? 120) * 1e3;
|
|
@@ -604,6 +639,7 @@ const runTestsBody = async (input, ctx) => {
|
|
|
604
639
|
const baseReport = buildAgentReport(testModules, unhandledErrors, preliminaryReason, { omitPassingTests: true });
|
|
605
640
|
const leaks = buildConsoleLeaks(collectConsoleLeakEntries(localVitest.state.getFiles()));
|
|
606
641
|
const strayOutput = readStrayOutput(localVitest.logger);
|
|
642
|
+
const configSuggestedActions = readConfigSuggestedActions(localVitest);
|
|
607
643
|
const report = {
|
|
608
644
|
...baseReport,
|
|
609
645
|
...leaks !== void 0 && { consoleLeaks: leaks },
|
|
@@ -644,7 +680,8 @@ const runTestsBody = async (input, ctx) => {
|
|
|
644
680
|
report,
|
|
645
681
|
classifications,
|
|
646
682
|
discoveryLastScannedAt: readDiscoveryLastScannedAt(),
|
|
647
|
-
scopedNote
|
|
683
|
+
scopedNote,
|
|
684
|
+
...configSuggestedActions !== void 0 && { suggestedActions: configSuggestedActions }
|
|
648
685
|
});
|
|
649
686
|
} catch (err) {
|
|
650
687
|
let message;
|
|
@@ -702,7 +739,7 @@ const handleRunTests = (input) => Effect.gen(function* () {
|
|
|
702
739
|
* @public
|
|
703
740
|
*/
|
|
704
741
|
const runTestsTool = Tool.make("run_tests", {
|
|
705
|
-
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 --path-format=absolute --git-common-dir`, which needs git 2.31 or newer and 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.",
|
|
742
|
+
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 --path-format=absolute --git-common-dir`, which needs git 2.31 or newer and 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. An error result with `reason: \"stale-install\"` means the vitest install was replaced under this long-lived server (the lockfile was regenerated) and the resolved vitest/node entry is gone; restart the MCP server (/mcp in Claude Code) rather than retrying. The legacy format=json arg is dropped — structuredContent supersedes it.",
|
|
706
743
|
parameters: RunTestsInput,
|
|
707
744
|
success: RunTestsResult,
|
|
708
745
|
dependencies: [
|
|
@@ -712,4 +749,4 @@ const runTestsTool = Tool.make("run_tests", {
|
|
|
712
749
|
]
|
|
713
750
|
}).annotate(Tool.Title, "Run tests").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, false);
|
|
714
751
|
|
|
715
|
-
export { RunTestsInput, RunTestsResult, coerceErrors, composeTagExpression, deriveRunScope, handleRunTests, isNoMatch, makeBestEffortFork, makeCoverageDirOverride, readDiscoveryLastScannedAt, resolveAnchoredConfigFile, resolveConfigAnchoredRoot, resolveVitestNodeEntry, runTestsSemaphore, runTestsTool, sanitizeTestArgs, staleInstallMessage, toNoMatch, toOkPayload, validateProjectRoot, vitestLoader, withStdioCaptured };
|
|
752
|
+
export { RUN_TESTS_ERROR_REASONS, RunTestsInput, RunTestsResult, coerceErrors, composeTagExpression, deriveRunScope, handleRunTests, isNoMatch, makeBestEffortFork, makeCoverageDirOverride, readConfigSuggestedActions, readDiscoveryLastScannedAt, resolveAnchoredConfigFile, resolveConfigAnchoredRoot, resolveVitestNodeEntry, runTestsSemaphore, runTestsTool, sanitizeTestArgs, staleInstallMessage, toNoMatch, toOkPayload, validateProjectRoot, vitestLoader, withStdioCaptured };
|
package/tools/status.js
CHANGED
|
@@ -2,14 +2,23 @@ import { ToolOutputSchema } from "@effected/mcp";
|
|
|
2
2
|
import { Effect, Option, Schema } from "effect";
|
|
3
3
|
import { DataReader } from "@vitest-agent/engine";
|
|
4
4
|
import { Tool } from "effect/ai";
|
|
5
|
-
import { CacheManifestEntry } from "@vitest-agent/sdk";
|
|
5
|
+
import { CacheManifestEntry, StrayOutput } from "@vitest-agent/sdk";
|
|
6
6
|
|
|
7
7
|
// src/tools/status.ts
|
|
8
|
+
/**
|
|
9
|
+
* A manifest entry plus the project's latest-run stray output (issue 578),
|
|
10
|
+
* present only when that run recorded any. Derived from the sdk entry's
|
|
11
|
+
* fields so the two cannot drift.
|
|
12
|
+
*/
|
|
13
|
+
const StatusEntry = Schema.Struct({
|
|
14
|
+
...CacheManifestEntry.fields,
|
|
15
|
+
strayOutput: Schema.optionalKey(StrayOutput).annotate({ description: "Output the latest run wrote straight to the terminal past Vitest's console capture (a test or a child process with inherited stdio). Absent when it recorded none." })
|
|
16
|
+
}).annotate({ identifier: "TestStatusEntry" });
|
|
8
17
|
const StatusAvailable = Schema.Struct({
|
|
9
18
|
dataAvailable: Schema.Literal(true).annotate({ description: "Discriminant — `true` when at least one project entry exists in the manifest." }),
|
|
10
19
|
manifestUpdatedAt: Schema.String,
|
|
11
20
|
projectFilter: Schema.optional(Schema.String).annotate({ description: "Echo of the optional `project` filter." }),
|
|
12
|
-
entries: Schema.Array(
|
|
21
|
+
entries: Schema.Array(StatusEntry).annotate({ description: "Per-project last-run summary rows. Filtered by `projectFilter` when set." })
|
|
13
22
|
}).annotate({ identifier: "TestStatusAvailable" });
|
|
14
23
|
const StatusAbsent = Schema.Struct({
|
|
15
24
|
dataAvailable: Schema.Literal(false).annotate({ description: "Discriminant — `false` when no manifest exists or the project filter matched nothing." }),
|
|
@@ -39,14 +48,22 @@ const TestStatusInput = Schema.Struct({ project: Schema.optionalKey(Schema.Strin
|
|
|
39
48
|
* @public
|
|
40
49
|
*/
|
|
41
50
|
const handleTestStatus = (input) => Effect.gen(function* () {
|
|
42
|
-
const
|
|
51
|
+
const reader = yield* DataReader;
|
|
52
|
+
const manifestOpt = yield* reader.getManifest();
|
|
43
53
|
if (Option.isNone(manifestOpt)) return {
|
|
44
54
|
dataAvailable: false,
|
|
45
55
|
reason: "no_manifest",
|
|
46
56
|
...input.project !== void 0 && { projectFilter: input.project }
|
|
47
57
|
};
|
|
48
58
|
const manifest = manifestOpt.value;
|
|
49
|
-
const
|
|
59
|
+
const strayByProject = new Map((yield* reader.getRunsByProject()).map((summary) => [summary.project, summary.strayOutput]));
|
|
60
|
+
const entries = (input.project === void 0 ? manifest.projects : manifest.projects.filter((e) => e.project === input.project)).map((entry) => {
|
|
61
|
+
const strayOutput = strayByProject.get(entry.project) ?? null;
|
|
62
|
+
return strayOutput === null ? entry : {
|
|
63
|
+
...entry,
|
|
64
|
+
strayOutput
|
|
65
|
+
};
|
|
66
|
+
});
|
|
50
67
|
if (entries.length === 0) return {
|
|
51
68
|
dataAvailable: false,
|
|
52
69
|
reason: "project_filter_empty",
|
|
@@ -65,7 +82,7 @@ const handleTestStatus = (input) => Effect.gen(function* () {
|
|
|
65
82
|
* @public
|
|
66
83
|
*/
|
|
67
84
|
const testStatusTool = Tool.make("test_status", {
|
|
68
|
-
description: "Use when you need each project's current pass/fail state from the most recent run. Returns a typed JSON object in structuredContent ({ dataAvailable, manifestUpdatedAt, projectFilter?, entries[] } or absent variant).",
|
|
85
|
+
description: "Use when you need each project's current pass/fail state from the most recent run. Returns a typed JSON object in structuredContent ({ dataAvailable, manifestUpdatedAt, projectFilter?, entries[] } or absent variant); an entry carries strayOutput when the project's latest run wrote straight to the terminal.",
|
|
69
86
|
parameters: TestStatusInput,
|
|
70
87
|
success: TestStatusResult,
|
|
71
88
|
dependencies: [DataReader]
|
package/tools/triage-brief.js
CHANGED
|
@@ -49,7 +49,7 @@ const handleTriageBrief = (input) => Effect.gen(function* () {
|
|
|
49
49
|
* @public
|
|
50
50
|
*/
|
|
51
51
|
const triageBriefTool = Tool.make("triage_brief", {
|
|
52
|
-
description: "Use when you need to orient on the current test landscape: failing tests, flaky tests, open TDD sessions, and suggested next actions. Returns a typed envelope in structuredContent ({ hasContent, markdown }).",
|
|
52
|
+
description: "Use when you need to orient on the current test landscape: failing tests, flaky tests, runs that wrote stray output past Vitest's console capture, open TDD sessions, and suggested next actions. Returns a typed envelope in structuredContent ({ hasContent, markdown }).",
|
|
53
53
|
parameters: TriageBriefInput,
|
|
54
54
|
success: TriageBriefResult,
|
|
55
55
|
dependencies: [DataReader]
|