@vitest-agent/mcp 4.2.2 → 4.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/annotations.js +7 -7
- package/index.d.ts +7 -7
- package/package.json +1 -1
- package/prompts/layer.js +6 -0
- package/register-toolkit.js +28 -58
- package/server.js +1 -1
- package/tools/acceptance-metrics.js +5 -19
- package/tools/cache-health.js +4 -36
- package/tools/commit-changes.js +4 -25
- package/tools/configure.js +4 -39
- package/tools/coverage.js +4 -54
- package/tools/errors.js +4 -83
- package/tools/failure-signature-get.js +4 -27
- package/tools/file-coverage.js +4 -36
- package/tools/help.js +6 -8
- package/tools/history.js +4 -40
- package/tools/hypothesis.js +3 -16
- package/tools/inventory.js +3 -96
- package/tools/note.js +3 -43
- package/tools/overview.js +4 -36
- package/tools/run-tests.js +3 -129
- package/tools/settings-list.js +4 -20
- package/tools/status.js +4 -28
- package/tools/tdd-artifact.js +3 -27
- package/tools/tdd-phase-transition-request.js +1 -2
- package/tools/tdd-progress-push.js +1 -2
- package/tools/tdd-task.js +4 -64
- package/tools/test.js +3 -145
- package/tools/trends.js +4 -70
- package/tools/triage-brief.js +3 -5
- package/tools/turn-search.js +4 -15
- package/tools/wrapup-prompt.js +3 -5
- package/version.js +1 -1
package/tools/run-tests.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { RenderText } from "../annotations.js";
|
|
2
1
|
import { McpSession } from "../session.js";
|
|
3
2
|
import { createRequire } from "node:module";
|
|
4
|
-
import { Data, Effect, Schema,
|
|
3
|
+
import { Data, Effect, Schema, Semaphore } from "effect";
|
|
5
4
|
import { DataReader, DataStore } from "@vitest-agent/engine";
|
|
6
5
|
import { Tool } from "effect/unstable/ai";
|
|
7
6
|
import { AgentReport, buildAgentReport, buildConsoleLeaks, coerceErrorField, collectConsoleLeakEntries, formatScopedCoverageNote } from "@vitest-agent/sdk";
|
|
@@ -386,131 +385,6 @@ function coerceErrors(errors) {
|
|
|
386
385
|
});
|
|
387
386
|
}
|
|
388
387
|
/**
|
|
389
|
-
* Render the full structured `RunTestsResult` as markdown for the
|
|
390
|
-
* text channel. Discriminates on `kind` then defers to the existing
|
|
391
|
-
* AgentReport rendering for the `ok` case.
|
|
392
|
-
*/
|
|
393
|
-
function formatRunTestsMarkdown(data) {
|
|
394
|
-
if (data.kind === "timeout") return `Test run timed out after ${data.timeoutSeconds} seconds.`;
|
|
395
|
-
if (data.kind === "error") return `Test run failed: ${data.message}`;
|
|
396
|
-
const rootLine = `\nProject root: \`${data.projectRoot}\``;
|
|
397
|
-
if (data.kind === "no-match") return `${formatNoMatchMarkdown(data.filter)}${rootLine}`;
|
|
398
|
-
const classMap = new Map(Object.entries(data.classifications));
|
|
399
|
-
return `${formatReportMarkdown(data.report, classMap, data.scopedNote ?? void 0)}${rootLine}`;
|
|
400
|
-
}
|
|
401
|
-
/**
|
|
402
|
-
* Render the `no-match` filter context plus a remediation pointer aimed at
|
|
403
|
-
* tag introspection. Pure helper; called by {@link formatRunTestsMarkdown}.
|
|
404
|
-
*
|
|
405
|
-
* @internal
|
|
406
|
-
*/
|
|
407
|
-
function formatNoMatchMarkdown(filter) {
|
|
408
|
-
const lines = ["## No tests matched the filter", ""];
|
|
409
|
-
const parts = [];
|
|
410
|
-
if (filter.project !== null) parts.push(`project: \`${filter.project}\``);
|
|
411
|
-
if (filter.files.length > 0) parts.push(`files: ${filter.files.map((f) => `\`${f}\``).join(", ")}`);
|
|
412
|
-
if (filter.tags !== null) {
|
|
413
|
-
const t = filter.tags;
|
|
414
|
-
if (t.all && t.all.length > 0) parts.push(`tags.all: ${t.all.map((s) => `\`${s}\``).join(", ")}`);
|
|
415
|
-
if (t.any && t.any.length > 0) parts.push(`tags.any: ${t.any.map((s) => `\`${s}\``).join(", ")}`);
|
|
416
|
-
if (t.none && t.none.length > 0) parts.push(`tags.none: ${t.none.map((s) => `\`${s}\``).join(", ")}`);
|
|
417
|
-
}
|
|
418
|
-
if (filter.resolvedExpression !== null) parts.push(`resolved expression: \`${filter.resolvedExpression}\``);
|
|
419
|
-
if (parts.length === 0) lines.push("- (no filter recorded)");
|
|
420
|
-
else for (const p of parts) lines.push(`- ${p}`);
|
|
421
|
-
lines.push("");
|
|
422
|
-
lines.push("### Next steps");
|
|
423
|
-
if (filter.tags !== null) {
|
|
424
|
-
lines.push("- Confirm the tag exists: `inventory({ kind: \"tag\" })`");
|
|
425
|
-
lines.push("- List tests for a specific tag: `test({ action: \"for_tag\", tag: \"<name>\" })`");
|
|
426
|
-
}
|
|
427
|
-
if (filter.files.length > 0) lines.push("- Verify file paths exist or list tests in a file with `test({ action: \"for_file\", filePath })`");
|
|
428
|
-
if (filter.project !== null) lines.push("- Verify the project name with `inventory({ kind: \"project\" })`");
|
|
429
|
-
return lines.join("\n");
|
|
430
|
-
}
|
|
431
|
-
const RunTestsAsMarkdown = RunTestsResult.pipe(Schema.decodeTo(Schema.String, {
|
|
432
|
-
decode: SchemaGetter.transform((data) => formatRunTestsMarkdown(data)),
|
|
433
|
-
encode: SchemaGetter.forbidden(() => "RunTestsAsMarkdown is one-way.")
|
|
434
|
-
}));
|
|
435
|
-
/**
|
|
436
|
-
* Format an AgentReport as concise markdown suitable for MCP tool output.
|
|
437
|
-
*
|
|
438
|
-
* Classifications map test fullName to labels like "new-failure",
|
|
439
|
-
* "persistent", "flaky", "recovered", "stable". Populated from DB
|
|
440
|
-
* after the reporter writes history.
|
|
441
|
-
*
|
|
442
|
-
* @internal
|
|
443
|
-
*/
|
|
444
|
-
function formatReportMarkdown(report, classifications, scopedNote) {
|
|
445
|
-
const lines = [];
|
|
446
|
-
const { summary } = report;
|
|
447
|
-
const collectionFailedFiles = report.failed.filter((m) => m.errors !== void 0 && m.errors.length > 0 && !m.tests.some((t) => t.state === "failed")).map((m) => m.file);
|
|
448
|
-
const hasCollectionFailures = collectionFailedFiles.length > 0;
|
|
449
|
-
const isFailing = summary.failed > 0 || report.unhandledErrors.length > 0 || hasCollectionFailures;
|
|
450
|
-
const status = isFailing ? "❌" : "✅";
|
|
451
|
-
const headlineParts = [];
|
|
452
|
-
if (summary.failed > 0) headlineParts.push(`${summary.failed} failed`);
|
|
453
|
-
if (hasCollectionFailures) headlineParts.push(`${collectionFailedFiles.length} failed to load`);
|
|
454
|
-
headlineParts.push(`${summary.passed} passed`);
|
|
455
|
-
if (summary.skipped > 0) headlineParts.push(`${summary.skipped} skipped`);
|
|
456
|
-
lines.push(`## ${status} Vitest -- ${headlineParts.join(", ")} (${summary.duration}ms)`);
|
|
457
|
-
if (report.project) lines.push(`\nProject: ${report.project}`);
|
|
458
|
-
if (scopedNote !== void 0) lines.push(`\n${scopedNote}`);
|
|
459
|
-
if (report.consoleLeaks !== void 0) {
|
|
460
|
-
const cl = report.consoleLeaks;
|
|
461
|
-
if (cl.total > 0) {
|
|
462
|
-
const writes = `${cl.total} stray console write${cl.total === 1 ? "" : "s"}`;
|
|
463
|
-
const plural = cl.byFile.length !== 1 || cl.truncated === true;
|
|
464
|
-
const files = `${cl.byFile.length}${cl.truncated === true ? "+" : ""} file${plural ? "s" : ""}`;
|
|
465
|
-
lines.push(`\n⚠ ${writes} across ${files} (see consoleLeaks)`);
|
|
466
|
-
}
|
|
467
|
-
if (cl.fromFailingTests !== void 0) {
|
|
468
|
-
const { total: failingTotal } = cl.fromFailingTests;
|
|
469
|
-
lines.push(`\n${failingTotal} console write${failingTotal === 1 ? "" : "s"} from failing tests (not counted as leaks)`);
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
for (const mod of report.failed) {
|
|
473
|
-
lines.push(`\n### \u274C \`${mod.file}\``);
|
|
474
|
-
if (mod.errors) for (const err of mod.errors) {
|
|
475
|
-
lines.push(`\n- \u274C **Module failed to load**: ${err.message}`);
|
|
476
|
-
if (err.stack) lines.push(`\n \`\`\`\n ${err.stack}\n \`\`\``);
|
|
477
|
-
}
|
|
478
|
-
for (const test of mod.tests) {
|
|
479
|
-
if (test.state !== "failed") continue;
|
|
480
|
-
const badge = classifications?.get(test.fullName);
|
|
481
|
-
const label = badge ? ` [${badge}]` : "";
|
|
482
|
-
lines.push(`\n- \u274C **${test.fullName}**${label}`);
|
|
483
|
-
if (test.errors) for (const err of test.errors) {
|
|
484
|
-
lines.push(` ${err.message}`);
|
|
485
|
-
if (err.diff) {
|
|
486
|
-
const diff = err.diff.length > 1e3 ? `${err.diff.slice(0, 1e3)}\n... (truncated, ${err.diff.length} chars total)` : err.diff;
|
|
487
|
-
lines.push(`\n \`\`\`diff\n ${diff}\n \`\`\``);
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
if (report.unhandledErrors.length > 0) {
|
|
493
|
-
lines.push("\n### Unhandled Errors");
|
|
494
|
-
for (const err of report.unhandledErrors) {
|
|
495
|
-
lines.push(`\n- ${err.message}`);
|
|
496
|
-
if (err.stack) lines.push(` \`\`\`\n ${err.stack}\n \`\`\``);
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
if (isFailing) {
|
|
500
|
-
const newFailures = classifications ? [...classifications.values()].filter((c) => c === "new-failure").length : 0;
|
|
501
|
-
const persistent = classifications ? [...classifications.values()].filter((c) => c === "persistent").length : 0;
|
|
502
|
-
const flaky = classifications ? [...classifications.values()].filter((c) => c === "flaky").length : 0;
|
|
503
|
-
lines.push("\n### Next steps\n");
|
|
504
|
-
if (newFailures > 0) lines.push(`- ${newFailures} new failure${newFailures > 1 ? "s" : ""} since last run`);
|
|
505
|
-
if (persistent > 0) lines.push(`- ${persistent} persistent failure${persistent > 1 ? "s" : ""} (pre-existing)`);
|
|
506
|
-
if (flaky > 0) lines.push(`- ${flaky} flaky test${flaky > 1 ? "s" : ""} -- consider retrying`);
|
|
507
|
-
lines.push("- Use test_errors for detailed error analysis");
|
|
508
|
-
lines.push("- Use test_history to check failure patterns");
|
|
509
|
-
if (report.failedFiles.length > 0) lines.push(`- Re-run failed: run_tests({ files: ${JSON.stringify(report.failedFiles)} })`);
|
|
510
|
-
}
|
|
511
|
-
return lines.join("\n");
|
|
512
|
-
}
|
|
513
|
-
/**
|
|
514
388
|
* The `run_tests` tool's parameters. `tags` and `_sessionContext` are
|
|
515
389
|
* nested structs and are served strict at their own level (issue #243).
|
|
516
390
|
*
|
|
@@ -738,7 +612,7 @@ const runTestsTool = Tool.make("run_tests", {
|
|
|
738
612
|
DataReader,
|
|
739
613
|
DataStore
|
|
740
614
|
]
|
|
741
|
-
}).annotate(Tool.Title, "Run tests").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, false)
|
|
615
|
+
}).annotate(Tool.Title, "Run tests").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, false);
|
|
742
616
|
|
|
743
617
|
//#endregion
|
|
744
|
-
export { RunTestsInput, RunTestsResult, coerceErrors, composeTagExpression,
|
|
618
|
+
export { RunTestsInput, RunTestsResult, coerceErrors, composeTagExpression, handleRunTests, makeBestEffortFork, makeCoverageDirOverride, readDiscoveryLastScannedAt, resolveAnchoredConfigFile, resolveConfigAnchoredRoot, resolveGitCommonDir, resolveVitestNodeEntry, runTestsTool, sanitizeTestArgs, validateProjectRoot, vitestLoader, withStdioCaptured };
|
package/tools/settings-list.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Effect, Schema, SchemaGetter } from "effect";
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
3
2
|
import { DataReader } from "@vitest-agent/engine";
|
|
4
3
|
import { Tool } from "effect/unstable/ai";
|
|
5
4
|
|
|
@@ -21,21 +20,6 @@ const SettingsListResult = Schema.Struct({
|
|
|
21
20
|
title: "settings_list result",
|
|
22
21
|
description: "Roster of distinct Vitest settings hashes the reporter has captured."
|
|
23
22
|
});
|
|
24
|
-
const formatSettingsListMarkdown = (data) => {
|
|
25
|
-
if (data.settings.length === 0) return "No settings found. Run tests first.";
|
|
26
|
-
const lines = [
|
|
27
|
-
"## Settings",
|
|
28
|
-
"",
|
|
29
|
-
"| Hash | Timestamp |",
|
|
30
|
-
"| --- | --- |"
|
|
31
|
-
];
|
|
32
|
-
for (const s of data.settings) lines.push(`| ${s.hash} | ${s.capturedAt} |`);
|
|
33
|
-
return lines.join("\n");
|
|
34
|
-
};
|
|
35
|
-
const SettingsListAsMarkdown = SettingsListResult.pipe(Schema.decodeTo(Schema.String, {
|
|
36
|
-
decode: SchemaGetter.transform((data) => formatSettingsListMarkdown(data)),
|
|
37
|
-
encode: SchemaGetter.forbidden(() => "SettingsListAsMarkdown is one-way.")
|
|
38
|
-
}));
|
|
39
23
|
/**
|
|
40
24
|
* Handler for {@link settingsListTool}.
|
|
41
25
|
*
|
|
@@ -55,10 +39,10 @@ const handleSettingsList = () => Effect.gen(function* () {
|
|
|
55
39
|
* @public
|
|
56
40
|
*/
|
|
57
41
|
const settingsListTool = Tool.make("settings_list", {
|
|
58
|
-
description: "Use when you need every captured settings snapshot and its hash. Returns
|
|
42
|
+
description: "Use when you need every captured settings snapshot and its hash. Returns a typed JSON object in structuredContent ({ count, settings[] }).",
|
|
59
43
|
success: SettingsListResult,
|
|
60
44
|
dependencies: [DataReader]
|
|
61
|
-
}).annotate(Tool.Title, "Settings list").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true)
|
|
45
|
+
}).annotate(Tool.Title, "Settings list").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true);
|
|
62
46
|
|
|
63
47
|
//#endregion
|
|
64
|
-
export { SettingsListResult,
|
|
48
|
+
export { SettingsListResult, handleSettingsList, settingsListTool };
|
package/tools/status.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Effect, Option, Schema, SchemaGetter } from "effect";
|
|
1
|
+
import { Effect, Option, Schema } from "effect";
|
|
3
2
|
import { DataReader } from "@vitest-agent/engine";
|
|
4
3
|
import { Tool } from "effect/unstable/ai";
|
|
5
4
|
import { CacheManifestEntry } from "@vitest-agent/sdk";
|
|
@@ -26,29 +25,6 @@ const TestStatusResult = Schema.Union([StatusAvailable, StatusAbsent]).annotate(
|
|
|
26
25
|
title: "test_status result",
|
|
27
26
|
description: "Per-project last-run summary. Discriminate on `dataAvailable` for cold-start handling."
|
|
28
27
|
});
|
|
29
|
-
const iconForResult = (r) => {
|
|
30
|
-
if (r === "passed") return "✅";
|
|
31
|
-
if (r === "failed") return "❌";
|
|
32
|
-
if (r === "interrupted") return "⚠️";
|
|
33
|
-
return "⬜";
|
|
34
|
-
};
|
|
35
|
-
const formatTestStatusMarkdown = (data) => {
|
|
36
|
-
if (!data.dataAvailable) {
|
|
37
|
-
if (data.reason === "project_filter_empty") return `No test data found for project \`${data.projectFilter ?? "(unknown)"}\`. Run tests first.`;
|
|
38
|
-
return "No test data available. Run tests first.";
|
|
39
|
-
}
|
|
40
|
-
const lines = ["# Test Status", ""];
|
|
41
|
-
for (const entry of data.entries) {
|
|
42
|
-
const lastRun = entry.lastRun ? new Date(entry.lastRun).toLocaleString() : "never";
|
|
43
|
-
lines.push(`- ${iconForResult(entry.lastResult)} **${entry.project}** — last run: ${lastRun}, result: ${entry.lastResult ?? "unknown"}`);
|
|
44
|
-
}
|
|
45
|
-
lines.push("", `_Cache updated: ${data.manifestUpdatedAt}_`);
|
|
46
|
-
return lines.join("\n");
|
|
47
|
-
};
|
|
48
|
-
const TestStatusAsMarkdown = TestStatusResult.pipe(Schema.decodeTo(Schema.String, {
|
|
49
|
-
decode: SchemaGetter.transform((data) => formatTestStatusMarkdown(data)),
|
|
50
|
-
encode: SchemaGetter.forbidden(() => "TestStatusAsMarkdown is one-way.")
|
|
51
|
-
}));
|
|
52
28
|
/**
|
|
53
29
|
* The `test_status` tool's parameters.
|
|
54
30
|
*
|
|
@@ -88,11 +64,11 @@ const handleTestStatus = (input) => Effect.gen(function* () {
|
|
|
88
64
|
* @public
|
|
89
65
|
*/
|
|
90
66
|
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
|
|
67
|
+
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).",
|
|
92
68
|
parameters: TestStatusInput,
|
|
93
69
|
success: TestStatusResult,
|
|
94
70
|
dependencies: [DataReader]
|
|
95
|
-
}).annotate(Tool.Title, "Test status").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true)
|
|
71
|
+
}).annotate(Tool.Title, "Test status").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true);
|
|
96
72
|
|
|
97
73
|
//#endregion
|
|
98
|
-
export { TestStatusInput, TestStatusResult,
|
|
74
|
+
export { TestStatusInput, TestStatusResult, handleTestStatus, testStatusTool };
|
package/tools/tdd-artifact.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Effect, Schema, SchemaGetter } from "effect";
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
3
2
|
import { DataReader } from "@vitest-agent/engine";
|
|
4
3
|
import { Tool } from "effect/unstable/ai";
|
|
5
4
|
|
|
@@ -57,29 +56,6 @@ const TddArtifactListResult = Schema.Struct({
|
|
|
57
56
|
title: "tdd_artifact_list result",
|
|
58
57
|
description: "Newest-first artifact rows for a TDD task. Echoes the filters that were applied so callers can reason about what is/isn't included."
|
|
59
58
|
});
|
|
60
|
-
const describeFilters = (filters) => {
|
|
61
|
-
const parts = [];
|
|
62
|
-
if (filters.artifactKind !== void 0) parts.push(`artifactKind=${filters.artifactKind}`);
|
|
63
|
-
if (filters.phaseId !== void 0) parts.push(`phaseId=${filters.phaseId}`);
|
|
64
|
-
if (filters.behaviorId !== void 0) parts.push(`behaviorId=${filters.behaviorId}`);
|
|
65
|
-
return parts.length > 0 ? ` matching ${parts.join(", ")}` : "";
|
|
66
|
-
};
|
|
67
|
-
const formatTddArtifactListMarkdown = (data) => {
|
|
68
|
-
if (data.count === 0) return `No artifacts recorded for tdd_task ${data.tddTaskId}${describeFilters(data.filters)}.`;
|
|
69
|
-
const lines = [`# Artifacts for tdd_task ${data.tddTaskId} (newest first, ${data.count} shown)`, ""];
|
|
70
|
-
for (const r of data.artifacts) {
|
|
71
|
-
const extras = [`phase=${r.phaseName} [phaseId=${r.phaseId}]`, `suite=${r.suite}`];
|
|
72
|
-
if (r.behaviorId !== null) extras.push(`behaviorId=${r.behaviorId}`);
|
|
73
|
-
if (r.testCaseId !== null) extras.push(`testCaseId=${r.testCaseId}`);
|
|
74
|
-
if (r.testRunId !== null) extras.push(`testRunId=${r.testRunId}`);
|
|
75
|
-
lines.push(`- **${r.artifactKind}** [id=${r.id}] at=${r.recordedAt} ${extras.join(" ")}`);
|
|
76
|
-
}
|
|
77
|
-
return lines.join("\n");
|
|
78
|
-
};
|
|
79
|
-
const TddArtifactListAsMarkdown = TddArtifactListResult.pipe(Schema.decodeTo(Schema.String, {
|
|
80
|
-
decode: SchemaGetter.transform((data) => formatTddArtifactListMarkdown(data)),
|
|
81
|
-
encode: SchemaGetter.forbidden(() => "TddArtifactListAsMarkdown is one-way.")
|
|
82
|
-
}));
|
|
83
59
|
/**
|
|
84
60
|
* The `tdd_artifact_list` tool's parameters.
|
|
85
61
|
*
|
|
@@ -126,7 +102,7 @@ const tddArtifactListTool = Tool.make("tdd_artifact_list", {
|
|
|
126
102
|
parameters: TddArtifactListInput,
|
|
127
103
|
success: TddArtifactListResult,
|
|
128
104
|
dependencies: [DataReader]
|
|
129
|
-
}).annotate(Tool.Title, "TDD artifact list").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true)
|
|
105
|
+
}).annotate(Tool.Title, "TDD artifact list").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true);
|
|
130
106
|
|
|
131
107
|
//#endregion
|
|
132
|
-
export { TddArtifactListInput, TddArtifactListResult,
|
|
108
|
+
export { TddArtifactListInput, TddArtifactListResult, handleTddArtifactList, tddArtifactListTool };
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { RenderText } from "../annotations.js";
|
|
2
1
|
import { Effect, Option, Schema } from "effect";
|
|
3
2
|
import { DataReader, DataStore } from "@vitest-agent/engine";
|
|
4
3
|
import { Tool } from "effect/unstable/ai";
|
|
@@ -294,7 +293,7 @@ const tddPhaseTransitionRequestTool = Tool.make("tdd_phase_transition_request",
|
|
|
294
293
|
parameters: PhaseTransitionInput,
|
|
295
294
|
success: PhaseTransitionResult,
|
|
296
295
|
dependencies: [DataReader, DataStore]
|
|
297
|
-
}).annotate(Tool.Title, "TDD phase transition request").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true)
|
|
296
|
+
}).annotate(Tool.Title, "TDD phase transition request").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true);
|
|
298
297
|
|
|
299
298
|
//#endregion
|
|
300
299
|
export { PhaseTransitionInput, PhaseTransitionResult, handlePhaseTransitionRequest, tddPhaseTransitionRequestTool };
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { RenderText } from "../annotations.js";
|
|
2
1
|
import { Effect, Option, Schema } from "effect";
|
|
3
2
|
import { DataReader } from "@vitest-agent/engine";
|
|
4
3
|
import { McpServer, Tool } from "effect/unstable/ai";
|
|
@@ -96,7 +95,7 @@ const tddProgressPushTool = Tool.make("tdd_progress_push", {
|
|
|
96
95
|
parameters: TddProgressPushInput,
|
|
97
96
|
success: TddProgressPushResult,
|
|
98
97
|
dependencies: [DataReader, McpServer.McpServer]
|
|
99
|
-
}).annotate(Tool.Title, "TDD progress push").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, false)
|
|
98
|
+
}).annotate(Tool.Title, "TDD progress push").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, false);
|
|
100
99
|
|
|
101
100
|
//#endregion
|
|
102
101
|
export { CHANNEL_LOGGER, TddProgressPushInput, TddProgressPushResult, handleTddProgressPush, resolveChannelEvent, tddProgressPushTool };
|
package/tools/tdd-task.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { RenderText } from "../annotations.js";
|
|
2
1
|
import { IdempotentReplayMarker } from "../utils/replay-marker.js";
|
|
3
|
-
import { Effect, Match, Option, Schema
|
|
2
|
+
import { Effect, Match, Option, Schema } from "effect";
|
|
4
3
|
import { DataReader, DataStore } from "@vitest-agent/engine";
|
|
5
4
|
import { Tool } from "effect/unstable/ai";
|
|
6
5
|
import { GoalDetail } from "@vitest-agent/sdk";
|
|
@@ -95,65 +94,6 @@ const TddTaskResult = Schema.Union([
|
|
|
95
94
|
title: "tdd_task result",
|
|
96
95
|
description: "Discriminate on `action`. `get` and `resume` further discriminate on `found`. `get` carries the full nested task tree."
|
|
97
96
|
});
|
|
98
|
-
const formatTddTaskMarkdown = (data) => {
|
|
99
|
-
if (data.action === "start" || data.action === "end") return JSON.stringify(data, null, 2);
|
|
100
|
-
if (data.action === "get") {
|
|
101
|
-
if (!data.found) return `No TDD task with tddTaskId=${data.tddTaskId}.`;
|
|
102
|
-
const s = data.task;
|
|
103
|
-
const currentPhaseLine = data.currentPhase === null ? "- current phase: (none — no open phase)" : `- current phase: ${data.currentPhase.phase} [phaseId=${data.currentPhase.id}]${data.currentPhase.behaviorId !== null ? ` behaviorId=${data.currentPhase.behaviorId}` : ""}`;
|
|
104
|
-
const lines = [
|
|
105
|
-
`# TDD Task ${s.tddTaskId}`,
|
|
106
|
-
"",
|
|
107
|
-
`- goal: ${s.goal}`,
|
|
108
|
-
`- run_id: ${s.runId ?? "(none — run_id not recorded)"}`,
|
|
109
|
-
`- sessionId: ${s.sessionId}`,
|
|
110
|
-
`- started: ${s.startedAt}`,
|
|
111
|
-
`- ended: ${s.endedAt ?? "still open"}`,
|
|
112
|
-
`- outcome: ${s.outcome ?? "pending"}`,
|
|
113
|
-
currentPhaseLine
|
|
114
|
-
];
|
|
115
|
-
if (s.phases.length > 0) {
|
|
116
|
-
lines.push("", "## Phases", "");
|
|
117
|
-
for (const p of s.phases) {
|
|
118
|
-
const duration = p.endedAt ? ` -> ${p.endedAt}` : " (current)";
|
|
119
|
-
lines.push(`- **${p.phase}** [id=${p.id}] ${p.startedAt}${duration}`);
|
|
120
|
-
if (p.transitionReason !== null) lines.push(` - reason: ${p.transitionReason}`);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
if (s.artifacts.length > 0) {
|
|
124
|
-
lines.push("", "## Artifacts", "");
|
|
125
|
-
for (const a of s.artifacts) lines.push(`- **${a.artifactKind}** [id=${a.id}, phase=${a.phaseId}] at=${a.recordedAt}${a.testRunId !== null ? ` run=${a.testRunId}` : ""}`);
|
|
126
|
-
}
|
|
127
|
-
if (s.goals.length > 0) {
|
|
128
|
-
lines.push("", "## Goals and Behaviors", "");
|
|
129
|
-
for (const g of s.goals) {
|
|
130
|
-
lines.push(`### Goal ${g.ordinal + 1}: ${g.goal} [${g.status}]`);
|
|
131
|
-
if (g.behaviors.length > 0) {
|
|
132
|
-
lines.push("");
|
|
133
|
-
for (const b of g.behaviors) lines.push(`- **${b.behavior}** [${b.status}]`);
|
|
134
|
-
}
|
|
135
|
-
lines.push("");
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
return lines.join("\n");
|
|
139
|
-
}
|
|
140
|
-
if (!data.found) return `No TDD task with tddTaskId=${data.tddTaskId}.`;
|
|
141
|
-
const lines = [
|
|
142
|
-
`# TDD task #${data.tddTaskId}: ${data.goal}`,
|
|
143
|
-
"",
|
|
144
|
-
`**Status:** ${data.status}`
|
|
145
|
-
];
|
|
146
|
-
if (data.currentPhase !== null) lines.push(`**Current phase:** ${data.currentPhase.phase} (started ${data.currentPhase.startedAt})`);
|
|
147
|
-
else lines.push("**Current phase:** none (TDD cycle not yet entered)");
|
|
148
|
-
lines.push("", `**Phases recorded:** ${data.phasesRecorded}`);
|
|
149
|
-
if (data.artifactsRecorded > 0) lines.push(`**Artifacts:** ${data.artifactsRecorded}`);
|
|
150
|
-
lines.push("", `Use \`tdd_task({ action: "get", tddTaskId: ${data.tddTaskId} })\` for the full detail tree, or call \`tdd_phase_transition_request\` to advance.`);
|
|
151
|
-
return lines.join("\n");
|
|
152
|
-
};
|
|
153
|
-
const TddTaskAsMarkdown = TddTaskResult.pipe(Schema.decodeTo(Schema.String, {
|
|
154
|
-
decode: SchemaGetter.transform((data) => formatTddTaskMarkdown(data)),
|
|
155
|
-
encode: SchemaGetter.forbidden(() => "TddTaskAsMarkdown is one-way.")
|
|
156
|
-
}));
|
|
157
97
|
const StartVariant = Schema.Struct({
|
|
158
98
|
action: Schema.Literal("start").annotate({ description: "Lifecycle discriminator" }),
|
|
159
99
|
goal: Schema.String.annotate({ description: "start: goal text" }),
|
|
@@ -301,11 +241,11 @@ const handleTddTask = (input) => Match.value(input).pipe(Match.discriminatorsExh
|
|
|
301
241
|
* @public
|
|
302
242
|
*/
|
|
303
243
|
const tddTaskTool = Tool.make("tdd_task", {
|
|
304
|
-
description: "Use to manage a TDD task lifecycle, with an action discriminator: action='start' (goal, sessionId|chatId, parentTddTaskId?, startedAt?, runId?) opens a new task; action='end' (tddTaskId, outcome, summaryNoteId?) closes one; action='get' (tddTaskId) returns
|
|
244
|
+
description: "Use to manage a TDD task lifecycle, with an action discriminator: action='start' (goal, sessionId|chatId, parentTddTaskId?, startedAt?, runId?) opens a new task; action='end' (tddTaskId, outcome, summaryNoteId?) closes one; action='get' (tddTaskId) returns the full task detail; action='resume' (tddTaskId) returns a compact digest.",
|
|
305
245
|
parameters: TddTaskInput,
|
|
306
246
|
success: TddTaskResult,
|
|
307
247
|
dependencies: [DataReader, DataStore]
|
|
308
|
-
}).annotate(Tool.Title, "TDD task").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true)
|
|
248
|
+
}).annotate(Tool.Title, "TDD task").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true);
|
|
309
249
|
|
|
310
250
|
//#endregion
|
|
311
|
-
export { TddTaskInput, TddTaskResult,
|
|
251
|
+
export { TddTaskInput, TddTaskResult, handleTddTask, tddTaskTool };
|
package/tools/test.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { RenderText } from "../annotations.js";
|
|
2
1
|
import { collectProjectRows, resolveProjectTargets } from "./_project-groups.js";
|
|
3
|
-
import { Effect, Match, Option, Schema
|
|
2
|
+
import { Effect, Match, Option, Schema } from "effect";
|
|
4
3
|
import { DataReader } from "@vitest-agent/engine";
|
|
5
4
|
import { Tool } from "effect/unstable/ai";
|
|
6
5
|
|
|
@@ -163,147 +162,6 @@ const TestResult = Schema.Union([
|
|
|
163
162
|
title: "test result",
|
|
164
163
|
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."
|
|
165
164
|
});
|
|
166
|
-
const formatTestMarkdown = (data) => {
|
|
167
|
-
if (data.action === "list") {
|
|
168
|
-
if (data.count === 0) return "No tests found. Run run_tests({}) to execute tests and populate the database.";
|
|
169
|
-
const lines = ["## Tests", ""];
|
|
170
|
-
for (const g of data.groups) {
|
|
171
|
-
lines.push(`### ${g.project}`, "", "| ID | Full Name | State | Duration | Module | Classification |", "| --- | --- | --- | --- | --- | --- |");
|
|
172
|
-
for (const t of g.tests) {
|
|
173
|
-
const duration = t.duration !== null ? `${t.duration}ms` : "—";
|
|
174
|
-
const classification = t.classification ?? "—";
|
|
175
|
-
lines.push(`| ${t.id} | ${t.fullName} | ${t.state} | ${duration} | ${t.module} | ${classification} |`);
|
|
176
|
-
}
|
|
177
|
-
lines.push("");
|
|
178
|
-
}
|
|
179
|
-
return lines.join("\n").trimEnd();
|
|
180
|
-
}
|
|
181
|
-
if (data.action === "get") {
|
|
182
|
-
if (!data.found) {
|
|
183
|
-
if (data.ambiguous === true) {
|
|
184
|
-
const modules = data.candidateModules ?? [];
|
|
185
|
-
return [
|
|
186
|
-
`Ambiguous test name: \`${data.fullName}\` matches ${modules.length} modules in project \`${data.project}\`.`,
|
|
187
|
-
"",
|
|
188
|
-
...modules.map((m) => `- \`${m}\``),
|
|
189
|
-
"",
|
|
190
|
-
`Re-run with a modulePath, e.g. test({ action: "get", fullName: "${data.fullName}", modulePath: "${modules[0] ?? ""}" }).`
|
|
191
|
-
].join("\n");
|
|
192
|
-
}
|
|
193
|
-
return `Test not found: \`${data.fullName}\`\n\nUse test({ action: "list" }) to discover available tests (format: "Suite > test name").`;
|
|
194
|
-
}
|
|
195
|
-
const t = data.test;
|
|
196
|
-
const lines = [
|
|
197
|
-
`# Test: ${t.fullName}`,
|
|
198
|
-
"",
|
|
199
|
-
"## Details",
|
|
200
|
-
"",
|
|
201
|
-
"| Field | Value |",
|
|
202
|
-
"| --- | --- |",
|
|
203
|
-
`| State | ${t.state} |`,
|
|
204
|
-
`| Duration | ${t.duration !== null ? `${t.duration}ms` : "—"} |`,
|
|
205
|
-
`| Module | \`${t.module}\` |`,
|
|
206
|
-
`| Classification | ${t.classification ?? "—"} |`,
|
|
207
|
-
""
|
|
208
|
-
];
|
|
209
|
-
if (data.errors.length > 0) {
|
|
210
|
-
lines.push("## Errors", "");
|
|
211
|
-
for (const err of data.errors) {
|
|
212
|
-
lines.push(`**${err.name ?? "(unnamed)"}**`);
|
|
213
|
-
lines.push(`> ${err.message.split("\n").join("\n> ")}`);
|
|
214
|
-
if (err.diff !== null) {
|
|
215
|
-
lines.push("", "```diff", err.diff.slice(0, 1e3));
|
|
216
|
-
if (err.diff.length > 1e3) lines.push(`... (truncated, ${err.diff.length} chars total)`);
|
|
217
|
-
lines.push("```");
|
|
218
|
-
}
|
|
219
|
-
if (err.stack !== null && err.diff === null) {
|
|
220
|
-
lines.push("", "```", err.stack.slice(0, 1e3));
|
|
221
|
-
if (err.stack.length > 1e3) lines.push(`... (truncated, ${err.stack.length} chars total)`);
|
|
222
|
-
lines.push("```");
|
|
223
|
-
}
|
|
224
|
-
lines.push("");
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
if (data.runs.length > 0) {
|
|
228
|
-
const viz = data.runs.map((r) => r.state === "passed" ? "P" : "F").join("");
|
|
229
|
-
const passCount = data.runs.filter((r) => r.state === "passed").length;
|
|
230
|
-
const failCount = data.runs.filter((r) => r.state === "failed").length;
|
|
231
|
-
lines.push("## Run History", "", `Pass rate: ${passCount}/${data.runs.length} (${Math.round(passCount / data.runs.length * 100)}%)`, `Recent runs: \`${viz}\` (P=passed F=failed S=skipped, newest last)`);
|
|
232
|
-
if (failCount > 0 && passCount > 0) lines.push("Pattern: **flaky** (mixed pass/fail)");
|
|
233
|
-
else if (failCount > 0) lines.push(`Pattern: **persistent failure** (${failCount} consecutive)`);
|
|
234
|
-
lines.push("");
|
|
235
|
-
}
|
|
236
|
-
if (t.state === "failed") lines.push("## Next steps", "", `- Re-run: run_tests({ files: ["${t.module}"] })`, `- Use test({ action: "for_file", filePath: "${t.module}" }) to find related tests`, "- Use note({ action: \"create\", ... }) to record debugging findings");
|
|
237
|
-
return lines.join("\n");
|
|
238
|
-
}
|
|
239
|
-
if (data.action === "for_tag") {
|
|
240
|
-
if (data.count === 0) return `No tests found tagged \`${data.tag}\`. Use \`inventory({ kind: "tag" })\` to discover available tags.`;
|
|
241
|
-
const lines = [
|
|
242
|
-
`# Tests tagged \`${data.tag}\``,
|
|
243
|
-
"",
|
|
244
|
-
`Found ${data.count} test${data.count === 1 ? "" : "s"} across ${data.groups.length} project${data.groups.length === 1 ? "" : "s"}:`,
|
|
245
|
-
""
|
|
246
|
-
];
|
|
247
|
-
for (const g of data.groups) {
|
|
248
|
-
lines.push(`### ${g.project}`, "", "| ID | Full Name | State | Duration | Module |", "| --- | --- | --- | --- | --- |");
|
|
249
|
-
for (const t of g.tests) {
|
|
250
|
-
const duration = t.duration !== null ? `${t.duration}ms` : "—";
|
|
251
|
-
lines.push(`| ${t.id} | ${t.fullName} | ${t.state} | ${duration} | ${t.module} |`);
|
|
252
|
-
}
|
|
253
|
-
lines.push("");
|
|
254
|
-
}
|
|
255
|
-
return lines.join("\n").trimEnd();
|
|
256
|
-
}
|
|
257
|
-
if (data.action === "annotations" || data.action === "artifacts") {
|
|
258
|
-
const noun = data.action === "annotations" ? "test annotations" : "test artifacts";
|
|
259
|
-
if (data.count === 0) return `No ${noun} recorded for \`${data.fullName}\`.`;
|
|
260
|
-
const rows = data.action === "annotations" ? data.annotations.map((a) => ({
|
|
261
|
-
type: a.type,
|
|
262
|
-
message: a.message,
|
|
263
|
-
location: a.location,
|
|
264
|
-
attachments: a.attachments
|
|
265
|
-
})) : data.artifacts.map((a) => ({
|
|
266
|
-
type: a.type,
|
|
267
|
-
message: a.message ?? a.data,
|
|
268
|
-
location: a.location,
|
|
269
|
-
attachments: a.attachments
|
|
270
|
-
}));
|
|
271
|
-
const cell = (value) => {
|
|
272
|
-
const flat = value.split("\n").join(" ").split("|").join("\\|");
|
|
273
|
-
return flat.length > 200 ? `${flat.slice(0, 200)}… (truncated)` : flat;
|
|
274
|
-
};
|
|
275
|
-
const lines = [
|
|
276
|
-
`# ${data.action === "annotations" ? "Annotations" : "Artifacts"} for \`${data.fullName}\``,
|
|
277
|
-
"",
|
|
278
|
-
`Project: \`${data.project}\` — ${data.count} row${data.count === 1 ? "" : "s"}.`,
|
|
279
|
-
"",
|
|
280
|
-
"| Type | Message | Location | Attachments |",
|
|
281
|
-
"| --- | --- | --- | --- |"
|
|
282
|
-
];
|
|
283
|
-
for (const row of rows) {
|
|
284
|
-
const location = row.location ? `\`${row.location.file}:${row.location.line}:${row.location.column}\`` : "—";
|
|
285
|
-
const attachments = row.attachments.length === 0 ? "—" : row.attachments.map((att) => {
|
|
286
|
-
return `\`${cell(att.path ?? att.contentType ?? "(inline)")}\`${att.byteSize !== null ? ` (${att.byteSize} bytes)` : ""}`;
|
|
287
|
-
}).join(", ");
|
|
288
|
-
const message = row.message === null ? "—" : cell(row.message);
|
|
289
|
-
lines.push(`| ${row.type} | ${message} | ${location} | ${attachments} |`);
|
|
290
|
-
}
|
|
291
|
-
return lines.join("\n");
|
|
292
|
-
}
|
|
293
|
-
if (data.count === 0) return `No test modules found covering \`${data.filePath}\`. Run run_tests({}) to populate the database, or check the file path.`;
|
|
294
|
-
const lines = [
|
|
295
|
-
`# Tests for \`${data.filePath}\``,
|
|
296
|
-
"",
|
|
297
|
-
`Found ${data.count} test module${data.count === 1 ? "" : "s"}:`,
|
|
298
|
-
""
|
|
299
|
-
];
|
|
300
|
-
for (const f of data.testFiles) lines.push(`- \`${f}\``);
|
|
301
|
-
return lines.join("\n");
|
|
302
|
-
};
|
|
303
|
-
const TestAsMarkdown = TestResult.pipe(Schema.decodeTo(Schema.String, {
|
|
304
|
-
decode: SchemaGetter.transform((data) => formatTestMarkdown(data)),
|
|
305
|
-
encode: SchemaGetter.forbidden(() => "TestAsMarkdown is one-way.")
|
|
306
|
-
}));
|
|
307
165
|
const ListVariant = Schema.Struct({
|
|
308
166
|
action: Schema.Literal("list"),
|
|
309
167
|
project: Schema.optionalKey(Schema.String),
|
|
@@ -482,7 +340,7 @@ const testTool = Tool.make("test", {
|
|
|
482
340
|
parameters: TestInput,
|
|
483
341
|
success: TestResult,
|
|
484
342
|
dependencies: [DataReader]
|
|
485
|
-
}).annotate(Tool.Title, "Test").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true)
|
|
343
|
+
}).annotate(Tool.Title, "Test").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true);
|
|
486
344
|
|
|
487
345
|
//#endregion
|
|
488
|
-
export { TestInput, TestResult,
|
|
346
|
+
export { TestInput, TestResult, handleTest, testTool };
|