@vitest-agent/mcp 4.2.2 → 4.3.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.
@@ -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, SchemaGetter, Semaphore } from "effect";
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).annotate(RenderText, (encoded) => formatRunTestsMarkdown(encoded));
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, formatNoMatchMarkdown, formatReportMarkdown, formatRunTestsMarkdown, handleRunTests, makeBestEffortFork, makeCoverageDirOverride, readDiscoveryLastScannedAt, resolveAnchoredConfigFile, resolveConfigAnchoredRoot, resolveGitCommonDir, resolveVitestNodeEntry, runTestsTool, sanitizeTestArgs, validateProjectRoot, vitestLoader, withStdioCaptured };
618
+ export { RunTestsInput, RunTestsResult, coerceErrors, composeTagExpression, handleRunTests, makeBestEffortFork, makeCoverageDirOverride, readDiscoveryLastScannedAt, resolveAnchoredConfigFile, resolveConfigAnchoredRoot, resolveGitCommonDir, resolveVitestNodeEntry, runTestsTool, sanitizeTestArgs, validateProjectRoot, vitestLoader, withStdioCaptured };
@@ -1,5 +1,4 @@
1
- import { RenderText } from "../annotations.js";
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 markdown in content[] and a typed JSON object in structuredContent ({ count, settings[] }).",
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).annotate(RenderText, (encoded) => formatSettingsListMarkdown(encoded));
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, formatSettingsListMarkdown, handleSettingsList, settingsListTool };
48
+ export { SettingsListResult, handleSettingsList, settingsListTool };
package/tools/status.js CHANGED
@@ -1,5 +1,4 @@
1
- import { RenderText } from "../annotations.js";
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 markdown in content[] and a typed JSON object in structuredContent ({ dataAvailable, manifestUpdatedAt, projectFilter?, entries[] } or absent variant).",
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).annotate(RenderText, (encoded) => formatTestStatusMarkdown(encoded));
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, formatTestStatusMarkdown, handleTestStatus, testStatusTool };
74
+ export { TestStatusInput, TestStatusResult, handleTestStatus, testStatusTool };
@@ -1,5 +1,4 @@
1
- import { RenderText } from "../annotations.js";
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).annotate(RenderText, (encoded) => formatTddArtifactListMarkdown(encoded));
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, formatTddArtifactListMarkdown, handleTddArtifactList, tddArtifactListTool };
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).annotate(RenderText, (encoded) => JSON.stringify(encoded, null, 2));
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).annotate(RenderText, (encoded) => JSON.stringify(encoded, null, 2));
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, SchemaGetter } from "effect";
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 markdown details; action='resume' (tddTaskId) returns a compact digest.",
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).annotate(RenderText, (encoded) => formatTddTaskMarkdown(encoded));
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, formatTddTaskMarkdown, handleTddTask, tddTaskTool };
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, SchemaGetter } from "effect";
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).annotate(RenderText, (encoded) => formatTestMarkdown(encoded));
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, formatTestMarkdown, handleTest, testTool };
346
+ export { TestInput, TestResult, handleTest, testTool };