@vitest-agent/mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +44 -0
- package/bin/vitest-agent-mcp.js +93 -0
- package/context.js +72 -0
- package/index.d.ts +1577 -0
- package/index.js +19 -0
- package/layers/McpLive.js +30 -0
- package/middleware/idempotency.js +128 -0
- package/package.json +58 -0
- package/prompts/explain-failure.js +27 -0
- package/prompts/index.js +89 -0
- package/prompts/regression-since-pass.js +28 -0
- package/prompts/tdd-resume.js +28 -0
- package/prompts/triage.js +24 -0
- package/prompts/why-flaky.js +30 -0
- package/prompts/wrapup.js +19 -0
- package/resources/index.js +155 -0
- package/resources/indexes.js +77 -0
- package/resources/manifest-schema.js +46 -0
- package/resources/paths.js +20 -0
- package/resources/patterns.js +22 -0
- package/resources/upstream-docs.js +22 -0
- package/router.js +74 -0
- package/server.js +838 -0
- package/tools/_tdd-error-envelope.js +98 -0
- package/tools/acceptance-metrics.js +75 -0
- package/tools/cache-health.js +83 -0
- package/tools/commit-changes.js +64 -0
- package/tools/configure.js +107 -0
- package/tools/coverage.js +76 -0
- package/tools/errors.js +151 -0
- package/tools/failure-signature-get.js +73 -0
- package/tools/file-coverage.js +106 -0
- package/tools/help.js +146 -0
- package/tools/history.js +121 -0
- package/tools/hypothesis.js +127 -0
- package/tools/inventory.js +377 -0
- package/tools/note.js +208 -0
- package/tools/overview.js +92 -0
- package/tools/ping.js +22 -0
- package/tools/register-agent.js +135 -0
- package/tools/run-tests.js +359 -0
- package/tools/settings-list.js +48 -0
- package/tools/status.js +74 -0
- package/tools/tdd-artifact.js +101 -0
- package/tools/tdd-behavior.js +177 -0
- package/tools/tdd-goal.js +147 -0
- package/tools/tdd-phase-transition-request.js +212 -0
- package/tools/tdd-task.js +278 -0
- package/tools/test.js +281 -0
- package/tools/trends.js +112 -0
- package/tools/triage-brief.js +42 -0
- package/tools/turn-search.js +60 -0
- package/tools/wrapup-prompt.js +49 -0
- package/tsdoc-metadata.json +11 -0
- package/utils/effect-to-zod.js +81 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { BehaviorNotFoundError, GoalNotFoundError, IllegalStatusTransitionError, TddTaskAlreadyEndedError, TddTaskNotFoundError } from "@vitest-agent/sdk";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
|
|
4
|
+
//#region src/tools/_tdd-error-envelope.ts
|
|
5
|
+
/**
|
|
6
|
+
* Shared error-to-success-shape envelope conversion for goal / behavior CRUD
|
|
7
|
+
* tools. Maps each tagged TDD error to an ok:false response matching the
|
|
8
|
+
* tdd_phase_transition_request accept/deny envelope shape, so the agent
|
|
9
|
+
* sees a normal tool response instead of a transport error.
|
|
10
|
+
*
|
|
11
|
+
* `Effect.catchTags` returns `Effect.succeed(envelope)` so the success
|
|
12
|
+
* channel carries the union `Success | ErrorEnvelope`.
|
|
13
|
+
*/
|
|
14
|
+
const goalNotFound = (e) => ({
|
|
15
|
+
ok: false,
|
|
16
|
+
error: {
|
|
17
|
+
_tag: e._tag,
|
|
18
|
+
id: e.id,
|
|
19
|
+
reason: e.reason,
|
|
20
|
+
remediation: {
|
|
21
|
+
suggestedTool: "tdd_goal",
|
|
22
|
+
suggestedArgs: { action: "list" },
|
|
23
|
+
humanHint: `No tdd_session_goals row with id=${e.id}. Call tdd_goal({ action: "list", tddTaskId }) to find the correct goal id.`
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
const behaviorNotFound = (e) => ({
|
|
28
|
+
ok: false,
|
|
29
|
+
error: {
|
|
30
|
+
_tag: e._tag,
|
|
31
|
+
id: e.id,
|
|
32
|
+
reason: e.reason,
|
|
33
|
+
remediation: {
|
|
34
|
+
suggestedTool: "tdd_behavior",
|
|
35
|
+
suggestedArgs: { action: "list_by_goal" },
|
|
36
|
+
humanHint: `No tdd_session_behaviors row with id=${e.id}. Call tdd_behavior({ action: "list_by_goal", goalId }) or tdd_behavior({ action: "list_by_tdd_task", tddTaskId }) to find the correct behavior id.`
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
const tddTaskNotFound = (e) => ({
|
|
41
|
+
ok: false,
|
|
42
|
+
error: {
|
|
43
|
+
_tag: e._tag,
|
|
44
|
+
id: e.id,
|
|
45
|
+
reason: e.reason,
|
|
46
|
+
remediation: {
|
|
47
|
+
suggestedTool: "tdd_task",
|
|
48
|
+
suggestedArgs: { action: "start" },
|
|
49
|
+
humanHint: `No tdd_tasks row with id=${e.id}. Call tdd_task({ action: "start" }) to open a TDD task before creating goals or behaviors.`
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
const tddTaskAlreadyEnded = (e) => ({
|
|
54
|
+
ok: false,
|
|
55
|
+
error: {
|
|
56
|
+
_tag: e._tag,
|
|
57
|
+
id: e.id,
|
|
58
|
+
endedAt: e.endedAt,
|
|
59
|
+
outcome: e.outcome,
|
|
60
|
+
remediation: {
|
|
61
|
+
suggestedTool: "tdd_task",
|
|
62
|
+
suggestedArgs: { action: "start" },
|
|
63
|
+
humanHint: `tdd_tasks row id=${e.id} is already ended (outcome=${e.outcome}). Open a new TDD task if you need to add more goals or behaviors.`
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
const illegalStatusTransition = (e) => ({
|
|
68
|
+
ok: false,
|
|
69
|
+
error: {
|
|
70
|
+
_tag: e._tag,
|
|
71
|
+
entity: e.entity,
|
|
72
|
+
id: e.id,
|
|
73
|
+
from: e.from,
|
|
74
|
+
to: e.to,
|
|
75
|
+
reason: e.reason,
|
|
76
|
+
remediation: {
|
|
77
|
+
suggestedTool: e.entity === "goal" ? "tdd_goal" : "tdd_behavior",
|
|
78
|
+
suggestedArgs: {
|
|
79
|
+
action: "update",
|
|
80
|
+
id: e.id,
|
|
81
|
+
status: "abandoned"
|
|
82
|
+
},
|
|
83
|
+
humanHint: `Cannot transition ${e.entity} id=${e.id} from ${e.from} to ${e.to}. Use status:'abandoned' to drop work; do not delete unless the entity was created by mistake.`
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
const isKnownTddError = (e) => e instanceof GoalNotFoundError || e instanceof BehaviorNotFoundError || e instanceof TddTaskNotFoundError || e instanceof TddTaskAlreadyEndedError || e instanceof IllegalStatusTransitionError;
|
|
88
|
+
const tddErrorToEnvelope = (e) => {
|
|
89
|
+
if (e instanceof GoalNotFoundError) return goalNotFound(e);
|
|
90
|
+
if (e instanceof BehaviorNotFoundError) return behaviorNotFound(e);
|
|
91
|
+
if (e instanceof TddTaskNotFoundError) return tddTaskNotFound(e);
|
|
92
|
+
if (e instanceof TddTaskAlreadyEndedError) return tddTaskAlreadyEnded(e);
|
|
93
|
+
return illegalStatusTransition(e);
|
|
94
|
+
};
|
|
95
|
+
const catchTddErrorsAsEnvelope = (effect) => effect.pipe(Effect.catchAll((e) => isKnownTddError(e) ? Effect.succeed(tddErrorToEnvelope(e)) : Effect.fail(e)));
|
|
96
|
+
|
|
97
|
+
//#endregion
|
|
98
|
+
export { catchTddErrorsAsEnvelope };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { publicProcedure } from "../context.js";
|
|
2
|
+
import { DataReader } from "@vitest-agent/sdk";
|
|
3
|
+
import { Effect, ParseResult, Schema } from "effect";
|
|
4
|
+
|
|
5
|
+
//#region src/tools/acceptance-metrics.ts
|
|
6
|
+
/**
|
|
7
|
+
* `acceptance_metrics` MCP tool — Schema-driven implementation.
|
|
8
|
+
*
|
|
9
|
+
* Mirrors `DataReader.AcceptanceMetrics` as an Effect Schema so the
|
|
10
|
+
* structured payload the agent receives, the markdown rendering on
|
|
11
|
+
* the text channel, and the SDK-side `outputSchema` all derive from
|
|
12
|
+
* one canonical contract.
|
|
13
|
+
*
|
|
14
|
+
* @packageDocumentation
|
|
15
|
+
*/
|
|
16
|
+
const totalAnnotation = { description: "Sample size — number of observations the metric ratio is computed over." };
|
|
17
|
+
const ratioAnnotation = { description: "Compliance ratio in [0, 1]. Multiply by 100 for the percentage form rendered in the markdown view." };
|
|
18
|
+
const AcceptanceMetricsResult = Schema.Struct({
|
|
19
|
+
phaseEvidenceIntegrity: Schema.Struct({
|
|
20
|
+
total: Schema.Number.annotations(totalAnnotation),
|
|
21
|
+
compliant: Schema.Number.annotations({ description: "Phase transitions that cited a valid artifact and passed binding-rule validation." }),
|
|
22
|
+
ratio: Schema.Number.annotations(ratioAnnotation)
|
|
23
|
+
}).annotations({
|
|
24
|
+
title: "Phase-evidence integrity",
|
|
25
|
+
description: "Fraction of accepted TDD phase transitions whose cited artifact satisfied the D2 binding rules. Spec target ≥80%."
|
|
26
|
+
}),
|
|
27
|
+
complianceHookResponsiveness: Schema.Struct({
|
|
28
|
+
total: Schema.Number.annotations(totalAnnotation),
|
|
29
|
+
withFollowup: Schema.Number.annotations({ description: "PreToolUse denials / `additionalContext` reminders the orchestrator acknowledged in the next turn." }),
|
|
30
|
+
ratio: Schema.Number.annotations(ratioAnnotation)
|
|
31
|
+
}).annotations({
|
|
32
|
+
title: "Compliance-hook responsiveness",
|
|
33
|
+
description: "Fraction of compliance signals from PreToolUse hooks the orchestrator acted on. Spec target ≥40%."
|
|
34
|
+
}),
|
|
35
|
+
orientationUsefulness: Schema.Struct({
|
|
36
|
+
total: Schema.Number.annotations(totalAnnotation),
|
|
37
|
+
referencedCount: Schema.Number.annotations({ description: "Sessions where `triage_brief` / `wrapup_prompt` content was referenced in subsequent decisions." }),
|
|
38
|
+
ratio: Schema.Number.annotations(ratioAnnotation)
|
|
39
|
+
}).annotations({
|
|
40
|
+
title: "Orientation usefulness",
|
|
41
|
+
description: "Fraction of sessions where orientation prompts measurably steered orchestrator behaviour. Spec target ≥50%."
|
|
42
|
+
}),
|
|
43
|
+
antiPatternDetectionRate: Schema.Struct({
|
|
44
|
+
total: Schema.Number.annotations(totalAnnotation),
|
|
45
|
+
cleanSessions: Schema.Number.annotations({ description: "Sessions that produced no `tdd_artifacts(kind='test_weakened')` rows or DATABASE_BYPASS notes." }),
|
|
46
|
+
ratio: Schema.Number.annotations(ratioAnnotation)
|
|
47
|
+
}).annotations({
|
|
48
|
+
title: "Anti-pattern detection rate",
|
|
49
|
+
description: "Fraction of sessions free of weakening edits or sqlite3 bypass attempts. Spec target ≥95%."
|
|
50
|
+
})
|
|
51
|
+
}).annotations({
|
|
52
|
+
identifier: "AcceptanceMetricsResult",
|
|
53
|
+
title: "Acceptance metrics",
|
|
54
|
+
description: "The four spec Annex A metrics computed from the current database. Each carries a sample size, a count, and a ratio."
|
|
55
|
+
});
|
|
56
|
+
const fmtBucket = (r) => r.total === 0 ? "no data" : `${(r.ratio * 100).toFixed(1)}% (n=${r.total})`;
|
|
57
|
+
const formatAcceptanceMetricsMarkdown = (m) => [
|
|
58
|
+
"# Acceptance metrics",
|
|
59
|
+
"",
|
|
60
|
+
`1. Phase-evidence integrity: ${fmtBucket(m.phaseEvidenceIntegrity)} — target ≥80%`,
|
|
61
|
+
`2. Compliance-hook responsiveness: ${fmtBucket(m.complianceHookResponsiveness)} — target ≥40%`,
|
|
62
|
+
`3. Orientation usefulness: ${fmtBucket(m.orientationUsefulness)} — target ≥50%`,
|
|
63
|
+
`4. Anti-pattern detection rate: ${fmtBucket(m.antiPatternDetectionRate)} — target ≥95%`
|
|
64
|
+
].join("\n");
|
|
65
|
+
const AcceptanceMetricsAsMarkdown = Schema.transformOrFail(AcceptanceMetricsResult, Schema.String, {
|
|
66
|
+
strict: true,
|
|
67
|
+
decode: (data) => ParseResult.succeed(formatAcceptanceMetricsMarkdown(data)),
|
|
68
|
+
encode: (text, _options, ast) => ParseResult.fail(new ParseResult.Forbidden(ast, text, "AcceptanceMetricsAsMarkdown is one-way: markdown cannot be parsed back to AcceptanceMetricsResult."))
|
|
69
|
+
});
|
|
70
|
+
const acceptanceMetrics = publicProcedure.input(Schema.standardSchemaV1(Schema.Struct({}))).query(async ({ ctx }) => ctx.runtime.runPromise(Effect.gen(function* () {
|
|
71
|
+
return yield* (yield* DataReader).computeAcceptanceMetrics();
|
|
72
|
+
})));
|
|
73
|
+
|
|
74
|
+
//#endregion
|
|
75
|
+
export { AcceptanceMetricsAsMarkdown, AcceptanceMetricsResult, acceptanceMetrics };
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { publicProcedure } from "../context.js";
|
|
2
|
+
import { CacheManifest, DataReader } from "@vitest-agent/sdk";
|
|
3
|
+
import { Effect, Option, ParseResult, Schema } from "effect";
|
|
4
|
+
|
|
5
|
+
//#region src/tools/cache-health.ts
|
|
6
|
+
/**
|
|
7
|
+
* `cache_health` MCP tool — Schema-driven implementation.
|
|
8
|
+
*
|
|
9
|
+
* Wraps the cache manifest in a `CacheHealthResult` Schema that
|
|
10
|
+
* captures both the present and absent cases. The text channel
|
|
11
|
+
* renders the same markdown the previous implementation produced;
|
|
12
|
+
* the structured payload now exposes `manifestPresent` plus the
|
|
13
|
+
* computed `ageMs` so agents can branch on freshness without parsing
|
|
14
|
+
* prose.
|
|
15
|
+
*
|
|
16
|
+
* @packageDocumentation
|
|
17
|
+
*/
|
|
18
|
+
const ManifestPresent = Schema.Struct({
|
|
19
|
+
manifestPresent: Schema.Literal(true).annotations({ description: "Discriminant — `true` when a cache manifest exists." }),
|
|
20
|
+
manifest: CacheManifest.annotations({ description: "Full cache manifest content as written by the reporter." }),
|
|
21
|
+
ageMs: Schema.Number.annotations({ description: "Milliseconds since the manifest was last updated. Computed at query time, not stored." }),
|
|
22
|
+
stale: Schema.Boolean.annotations({ description: "Convenience flag — `true` when `ageMs` exceeds 24 hours, otherwise `false`." })
|
|
23
|
+
}).annotations({
|
|
24
|
+
identifier: "CacheHealthPresent",
|
|
25
|
+
title: "Cache manifest present"
|
|
26
|
+
});
|
|
27
|
+
const ManifestAbsent = Schema.Struct({ manifestPresent: Schema.Literal(false).annotations({ description: "Discriminant — `false` when no manifest has been written yet (run tests to populate the cache)." }) }).annotations({
|
|
28
|
+
identifier: "CacheHealthAbsent",
|
|
29
|
+
title: "Cache manifest absent"
|
|
30
|
+
});
|
|
31
|
+
const CacheHealthResult = Schema.Union(ManifestPresent, ManifestAbsent).annotations({
|
|
32
|
+
identifier: "CacheHealthResult",
|
|
33
|
+
title: "cache_health result",
|
|
34
|
+
description: "Cache health snapshot. Discriminate on `manifestPresent` to see whether the manifest exists."
|
|
35
|
+
});
|
|
36
|
+
const STALE_AFTER_MS = 1440 * 60 * 1e3;
|
|
37
|
+
const iconForResult = (r) => {
|
|
38
|
+
if (r === "passed") return "✅";
|
|
39
|
+
if (r === "failed") return "❌";
|
|
40
|
+
if (r === "interrupted") return "⚠️";
|
|
41
|
+
return "⬜";
|
|
42
|
+
};
|
|
43
|
+
const formatCacheHealthMarkdown = (data) => {
|
|
44
|
+
const lines = ["# Cache Health", ""];
|
|
45
|
+
if (data.manifestPresent === false) {
|
|
46
|
+
lines.push("- ❌ **Manifest:** not found — run tests to populate the cache");
|
|
47
|
+
return lines.join("\n");
|
|
48
|
+
}
|
|
49
|
+
const { manifest, ageMs, stale } = data;
|
|
50
|
+
const ageHours = ageMs / (1e3 * 60 * 60);
|
|
51
|
+
lines.push("- ✅ **Manifest:** present");
|
|
52
|
+
lines.push(`- ℹ️ **Projects:** ${manifest.projects.length}`);
|
|
53
|
+
lines.push(`- ℹ️ **Cache directory:** \`${manifest.cacheDir}\``);
|
|
54
|
+
lines.push(`- ℹ️ **Last updated:** ${manifest.updatedAt}`);
|
|
55
|
+
if (stale) lines.push(`- ⚠️ **Staleness:** cache is ${Math.round(ageHours)} hours old — consider re-running tests`);
|
|
56
|
+
else lines.push(`- ✅ **Staleness:** cache is ${Math.round(ageHours * 60)} minutes old`);
|
|
57
|
+
lines.push("", "## Projects", "");
|
|
58
|
+
for (const entry of manifest.projects) {
|
|
59
|
+
const lastRun = entry.lastRun ? new Date(entry.lastRun).toLocaleString() : "never";
|
|
60
|
+
lines.push(`- ${iconForResult(entry.lastResult)} **${entry.project}** — last run: ${lastRun}`);
|
|
61
|
+
}
|
|
62
|
+
return lines.join("\n");
|
|
63
|
+
};
|
|
64
|
+
const CacheHealthAsMarkdown = Schema.transformOrFail(CacheHealthResult, Schema.String, {
|
|
65
|
+
strict: true,
|
|
66
|
+
decode: (data) => ParseResult.succeed(formatCacheHealthMarkdown(data)),
|
|
67
|
+
encode: (text, _options, ast) => ParseResult.fail(new ParseResult.Forbidden(ast, text, "CacheHealthAsMarkdown is one-way: markdown cannot be parsed back to CacheHealthResult."))
|
|
68
|
+
});
|
|
69
|
+
const cacheHealth = publicProcedure.query(async ({ ctx }) => ctx.runtime.runPromise(Effect.gen(function* () {
|
|
70
|
+
const manifestOpt = yield* (yield* DataReader).getManifest();
|
|
71
|
+
if (Option.isNone(manifestOpt)) return { manifestPresent: false };
|
|
72
|
+
const manifest = manifestOpt.value;
|
|
73
|
+
const ageMs = Date.now() - new Date(manifest.updatedAt).getTime();
|
|
74
|
+
return {
|
|
75
|
+
manifestPresent: true,
|
|
76
|
+
manifest,
|
|
77
|
+
ageMs,
|
|
78
|
+
stale: ageMs > STALE_AFTER_MS
|
|
79
|
+
};
|
|
80
|
+
})));
|
|
81
|
+
|
|
82
|
+
//#endregion
|
|
83
|
+
export { CacheHealthAsMarkdown, CacheHealthResult, cacheHealth };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { publicProcedure } from "../context.js";
|
|
2
|
+
import { DataReader } from "@vitest-agent/sdk";
|
|
3
|
+
import { Effect, ParseResult, Schema } from "effect";
|
|
4
|
+
|
|
5
|
+
//#region src/tools/commit-changes.ts
|
|
6
|
+
/**
|
|
7
|
+
* `commit_changes` MCP tool — Schema-driven implementation.
|
|
8
|
+
*
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*/
|
|
11
|
+
const FileRow = Schema.Struct({
|
|
12
|
+
filePath: Schema.String.annotations({ description: "Repo-relative path of the changed file." }),
|
|
13
|
+
changeKind: Schema.Literal("added", "modified", "deleted", "renamed", "untracked-modified").annotations({ description: "How the file changed in this commit (or `untracked-modified` for working-tree changes attributed to a commit)." })
|
|
14
|
+
}).annotations({ identifier: "CommitFileRow" });
|
|
15
|
+
const CommitRow = Schema.Struct({
|
|
16
|
+
sha: Schema.String.annotations({ description: "Full git commit SHA-1." }),
|
|
17
|
+
parentSha: Schema.NullOr(Schema.String).annotations({ description: "Parent commit SHA, or `null` for the root commit / when no parent was recorded." }),
|
|
18
|
+
message: Schema.NullOr(Schema.String).annotations({ description: "Commit message subject + body, or `null` if not captured." }),
|
|
19
|
+
author: Schema.NullOr(Schema.String).annotations({ description: "Commit author in `Name <email>` form when captured." }),
|
|
20
|
+
committedAt: Schema.NullOr(Schema.String).annotations({ description: "ISO-8601 commit timestamp." }),
|
|
21
|
+
branch: Schema.NullOr(Schema.String).annotations({ description: "Branch the commit was recorded on at hook fire time." }),
|
|
22
|
+
files: Schema.Array(FileRow).annotations({ description: "Files this commit changed, with per-file change kinds." })
|
|
23
|
+
}).annotations({ identifier: "CommitRow" });
|
|
24
|
+
const CommitChangesResult = Schema.Struct({
|
|
25
|
+
filterSha: Schema.optional(Schema.String).annotations({ description: "Echo of the optional `sha` filter the caller passed; absent when no filter was applied (recent commits returned)." }),
|
|
26
|
+
count: Schema.Number.annotations({ description: "Number of commit rows returned." }),
|
|
27
|
+
commits: Schema.Array(CommitRow).annotations({ description: "Matching commits, newest first when `sha` was omitted; up to 20 rows." })
|
|
28
|
+
}).annotations({
|
|
29
|
+
identifier: "CommitChangesResult",
|
|
30
|
+
title: "commit_changes result",
|
|
31
|
+
description: "Commit metadata + per-file changes captured by the post-commit Bash hook."
|
|
32
|
+
});
|
|
33
|
+
const formatCommitChangesMarkdown = (data) => {
|
|
34
|
+
if (data.commits.length === 0) return data.filterSha !== void 0 ? `No commit recorded with sha ${data.filterSha}.` : "No commits recorded yet. The PostToolUse hook on `git commit` populates this table.";
|
|
35
|
+
const lines = [];
|
|
36
|
+
for (const e of data.commits) {
|
|
37
|
+
lines.push(`## ${e.sha.slice(0, 8)} ${e.message ?? "(no message)"}`);
|
|
38
|
+
if (e.author !== null) lines.push(`- Author: ${e.author}`);
|
|
39
|
+
if (e.committedAt !== null) lines.push(`- When: ${e.committedAt}`);
|
|
40
|
+
if (e.branch !== null) lines.push(`- Branch: ${e.branch}`);
|
|
41
|
+
if (e.files.length > 0) {
|
|
42
|
+
lines.push("- Changed files:");
|
|
43
|
+
for (const f of e.files) lines.push(` - \`${f.filePath}\` (${f.changeKind})`);
|
|
44
|
+
}
|
|
45
|
+
lines.push("");
|
|
46
|
+
}
|
|
47
|
+
return lines.join("\n").trim();
|
|
48
|
+
};
|
|
49
|
+
const CommitChangesAsMarkdown = Schema.transformOrFail(CommitChangesResult, Schema.String, {
|
|
50
|
+
strict: true,
|
|
51
|
+
decode: (data) => ParseResult.succeed(formatCommitChangesMarkdown(data)),
|
|
52
|
+
encode: (text, _options, ast) => ParseResult.fail(new ParseResult.Forbidden(ast, text, "CommitChangesAsMarkdown is one-way: markdown cannot be parsed back to CommitChangesResult."))
|
|
53
|
+
});
|
|
54
|
+
const commitChanges = publicProcedure.input(Schema.standardSchemaV1(Schema.Struct({ sha: Schema.optional(Schema.String) }))).query(async ({ ctx, input }) => ctx.runtime.runPromise(Effect.gen(function* () {
|
|
55
|
+
const entries = yield* (yield* DataReader).getCommitChanges(input.sha);
|
|
56
|
+
return {
|
|
57
|
+
...input.sha !== void 0 && { filterSha: input.sha },
|
|
58
|
+
count: entries.length,
|
|
59
|
+
commits: entries
|
|
60
|
+
};
|
|
61
|
+
})));
|
|
62
|
+
|
|
63
|
+
//#endregion
|
|
64
|
+
export { CommitChangesAsMarkdown, CommitChangesResult, commitChanges };
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { publicProcedure } from "../context.js";
|
|
2
|
+
import { DataReader } from "@vitest-agent/sdk";
|
|
3
|
+
import { Effect, Option, ParseResult, Schema } from "effect";
|
|
4
|
+
|
|
5
|
+
//#region src/tools/configure.ts
|
|
6
|
+
/**
|
|
7
|
+
* `configure` MCP tool — Schema-driven implementation.
|
|
8
|
+
*
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*/
|
|
11
|
+
const SettingsRowSchema = Schema.Struct({
|
|
12
|
+
hash: Schema.String.annotations({ description: "Stable SHA-1 of the captured Vitest settings; `test_runs.settings_hash` foreign key." }),
|
|
13
|
+
reporters: Schema.NullOr(Schema.String).annotations({ description: "Comma-separated reporter list as resolved from the user's vitest config." }),
|
|
14
|
+
coverageEnabled: Schema.Boolean.annotations({ description: "Whether coverage was on for this run." }),
|
|
15
|
+
coverageProvider: Schema.NullOr(Schema.String).annotations({ description: "Coverage provider (`v8` or `istanbul`)." }),
|
|
16
|
+
coverageThresholds: Schema.NullOr(Schema.String).annotations({ description: "JSON-encoded threshold table when present; `null` when no thresholds were configured." }),
|
|
17
|
+
coverageTargets: Schema.NullOr(Schema.String).annotations({ description: "JSON-encoded aspirational target table when present." }),
|
|
18
|
+
pool: Schema.NullOr(Schema.String).annotations({ description: "Vitest pool (`forks` / `threads` / `vmThreads`)." }),
|
|
19
|
+
shard: Schema.NullOr(Schema.String).annotations({ description: "Shard descriptor when running sharded (`1/4` form)." }),
|
|
20
|
+
project: Schema.NullOr(Schema.String).annotations({ description: "Project name within a multi-project setup." }),
|
|
21
|
+
environment: Schema.NullOr(Schema.String).annotations({ description: "Test environment (`node`, `jsdom`, etc.)." }),
|
|
22
|
+
envVars: Schema.Record({
|
|
23
|
+
key: Schema.String,
|
|
24
|
+
value: Schema.String
|
|
25
|
+
}).annotations({ description: "Captured CI / test env vars associated with this settings hash." }),
|
|
26
|
+
capturedAt: Schema.String.annotations({ description: "ISO-8601 timestamp the settings row was first written." })
|
|
27
|
+
}).annotations({
|
|
28
|
+
identifier: "SettingsRowSchema",
|
|
29
|
+
title: "Vitest settings snapshot"
|
|
30
|
+
});
|
|
31
|
+
const SettingsFound = Schema.Struct({
|
|
32
|
+
found: Schema.Literal(true).annotations({ description: "Discriminant — `true` when settings were located." }),
|
|
33
|
+
source: Schema.Literal("requested", "latest").annotations({ description: "`requested` when the caller supplied `settingsHash`; `latest` when the most-recent row was returned." }),
|
|
34
|
+
settings: SettingsRowSchema
|
|
35
|
+
}).annotations({ identifier: "ConfigureFound" });
|
|
36
|
+
const SettingsAbsent = Schema.Struct({
|
|
37
|
+
found: Schema.Literal(false).annotations({ description: "Discriminant — `false` when no settings matched." }),
|
|
38
|
+
source: Schema.Literal("requested", "latest"),
|
|
39
|
+
requestedHash: Schema.optional(Schema.String).annotations({ description: "Echo of the hash the caller passed; absent when the empty `latest` lookup found nothing." })
|
|
40
|
+
}).annotations({ identifier: "ConfigureAbsent" });
|
|
41
|
+
const ConfigureResult = Schema.Union(SettingsFound, SettingsAbsent).annotations({
|
|
42
|
+
identifier: "ConfigureResult",
|
|
43
|
+
title: "configure result",
|
|
44
|
+
description: "Captured Vitest settings for a run, or an absence record when the lookup found nothing."
|
|
45
|
+
});
|
|
46
|
+
const formatSettings = (s) => {
|
|
47
|
+
const lines = [`# Settings — \`${s.hash}\``, ""];
|
|
48
|
+
lines.push(`**Captured:** ${s.capturedAt}`);
|
|
49
|
+
if (s.project !== null) lines.push(`**Project:** ${s.project}`);
|
|
50
|
+
if (s.environment !== null) lines.push(`**Environment:** ${s.environment}`);
|
|
51
|
+
if (s.pool !== null) lines.push(`**Pool:** ${s.pool}`);
|
|
52
|
+
if (s.shard !== null) lines.push(`**Shard:** ${s.shard}`);
|
|
53
|
+
lines.push("", "## Coverage", `- **Enabled:** ${s.coverageEnabled ? "yes" : "no"}`);
|
|
54
|
+
if (s.coverageProvider !== null) lines.push(`- **Provider:** ${s.coverageProvider}`);
|
|
55
|
+
if (s.coverageThresholds !== null) lines.push(`- **Thresholds:** \`${s.coverageThresholds}\``);
|
|
56
|
+
if (s.coverageTargets !== null) lines.push(`- **Targets:** \`${s.coverageTargets}\``);
|
|
57
|
+
if (s.reporters !== null) lines.push("", "## Reporters", `\`${s.reporters}\``);
|
|
58
|
+
const envKeys = Object.keys(s.envVars);
|
|
59
|
+
if (envKeys.length > 0) {
|
|
60
|
+
lines.push("", "## Environment Variables");
|
|
61
|
+
for (const key of envKeys) lines.push(`- \`${key}\`: \`${s.envVars[key]}\``);
|
|
62
|
+
}
|
|
63
|
+
return lines.join("\n");
|
|
64
|
+
};
|
|
65
|
+
const formatConfigureMarkdown = (data) => {
|
|
66
|
+
if (data.found) return formatSettings(data.settings);
|
|
67
|
+
if (data.source === "latest") return [
|
|
68
|
+
"# Configure",
|
|
69
|
+
"",
|
|
70
|
+
"No settings captured yet. Run tests first.",
|
|
71
|
+
"",
|
|
72
|
+
"Configuration is written automatically by `AgentPlugin` when tests run."
|
|
73
|
+
].join("\n");
|
|
74
|
+
return `No settings found for hash \`${data.requestedHash ?? "(unknown)"}\`.`;
|
|
75
|
+
};
|
|
76
|
+
const ConfigureAsMarkdown = Schema.transformOrFail(ConfigureResult, Schema.String, {
|
|
77
|
+
strict: true,
|
|
78
|
+
decode: (data) => ParseResult.succeed(formatConfigureMarkdown(data)),
|
|
79
|
+
encode: (text, _options, ast) => ParseResult.fail(new ParseResult.Forbidden(ast, text, "ConfigureAsMarkdown is one-way: markdown cannot be parsed back to ConfigureResult."))
|
|
80
|
+
});
|
|
81
|
+
const configure = publicProcedure.input(Schema.standardSchemaV1(Schema.Struct({ settingsHash: Schema.optional(Schema.String) }))).query(async ({ ctx, input }) => ctx.runtime.runPromise(Effect.gen(function* () {
|
|
82
|
+
const reader = yield* DataReader;
|
|
83
|
+
if (input.settingsHash === void 0) {
|
|
84
|
+
const latestOpt = yield* reader.getLatestSettings();
|
|
85
|
+
return Option.isNone(latestOpt) ? {
|
|
86
|
+
found: false,
|
|
87
|
+
source: "latest"
|
|
88
|
+
} : {
|
|
89
|
+
found: true,
|
|
90
|
+
source: "latest",
|
|
91
|
+
settings: latestOpt.value
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
const settingsOpt = yield* reader.getSettings(input.settingsHash);
|
|
95
|
+
return Option.isNone(settingsOpt) ? {
|
|
96
|
+
found: false,
|
|
97
|
+
source: "requested",
|
|
98
|
+
requestedHash: input.settingsHash
|
|
99
|
+
} : {
|
|
100
|
+
found: true,
|
|
101
|
+
source: "requested",
|
|
102
|
+
settings: settingsOpt.value
|
|
103
|
+
};
|
|
104
|
+
})));
|
|
105
|
+
|
|
106
|
+
//#endregion
|
|
107
|
+
export { ConfigureAsMarkdown, ConfigureResult, configure };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { publicProcedure } from "../context.js";
|
|
2
|
+
import { CoverageReport, DataReader } from "@vitest-agent/sdk";
|
|
3
|
+
import { Effect, Option, ParseResult, Schema } from "effect";
|
|
4
|
+
|
|
5
|
+
//#region src/tools/coverage.ts
|
|
6
|
+
/**
|
|
7
|
+
* `test_coverage` MCP tool — Schema-driven implementation.
|
|
8
|
+
*
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*/
|
|
11
|
+
const CoverageAvailable = Schema.Struct({
|
|
12
|
+
dataAvailable: Schema.Literal(true),
|
|
13
|
+
project: Schema.String,
|
|
14
|
+
coverage: CoverageReport
|
|
15
|
+
}).annotations({ identifier: "TestCoverageAvailable" });
|
|
16
|
+
const CoverageAbsent = Schema.Struct({
|
|
17
|
+
dataAvailable: Schema.Literal(false),
|
|
18
|
+
project: Schema.String
|
|
19
|
+
}).annotations({ identifier: "TestCoverageAbsent" });
|
|
20
|
+
const TestCoverageResult = Schema.Union(CoverageAvailable, CoverageAbsent).annotations({
|
|
21
|
+
identifier: "TestCoverageResult",
|
|
22
|
+
title: "test_coverage result",
|
|
23
|
+
description: "Per-project coverage report. Discriminate on `dataAvailable` for cold-start handling."
|
|
24
|
+
});
|
|
25
|
+
const formatTestCoverageMarkdown = (data) => {
|
|
26
|
+
if (!data.dataAvailable) return "No coverage data available. Run tests with coverage enabled.";
|
|
27
|
+
const lines = ["# Coverage Report", ""];
|
|
28
|
+
const { totals, thresholds } = data.coverage;
|
|
29
|
+
lines.push("## Totals", "", "| Metric | Value | Threshold |", "| --- | --- | --- |");
|
|
30
|
+
const metrics = [
|
|
31
|
+
"statements",
|
|
32
|
+
"branches",
|
|
33
|
+
"functions",
|
|
34
|
+
"lines"
|
|
35
|
+
];
|
|
36
|
+
for (const metric of metrics) {
|
|
37
|
+
const value = totals[metric];
|
|
38
|
+
const threshold = thresholds.global[metric];
|
|
39
|
+
const thresholdStr = threshold !== void 0 ? `${threshold}%` : "—";
|
|
40
|
+
const icon = threshold !== void 0 && value < threshold ? "❌" : "✅";
|
|
41
|
+
lines.push(`| ${metric} | ${icon} ${value.toFixed(2)}% | ${thresholdStr} |`);
|
|
42
|
+
}
|
|
43
|
+
lines.push("");
|
|
44
|
+
if (data.coverage.lowCoverage.length > 0) {
|
|
45
|
+
lines.push("## Coverage Gaps", "", "Files below coverage threshold:", "");
|
|
46
|
+
for (const fileCoverage of data.coverage.lowCoverage) {
|
|
47
|
+
lines.push(`### \`${fileCoverage.file}\``, "", "| Metric | Value |", "| --- | --- |");
|
|
48
|
+
for (const metric of metrics) lines.push(`| ${metric} | ${fileCoverage.summary[metric].toFixed(2)}% |`);
|
|
49
|
+
if (fileCoverage.uncoveredLines) lines.push(`| Uncovered lines | \`${fileCoverage.uncoveredLines}\` |`);
|
|
50
|
+
lines.push("");
|
|
51
|
+
}
|
|
52
|
+
} else lines.push("✅ All files meet coverage thresholds.", "");
|
|
53
|
+
return lines.join("\n");
|
|
54
|
+
};
|
|
55
|
+
const TestCoverageAsMarkdown = Schema.transformOrFail(TestCoverageResult, Schema.String, {
|
|
56
|
+
strict: true,
|
|
57
|
+
decode: (data) => ParseResult.succeed(formatTestCoverageMarkdown(data)),
|
|
58
|
+
encode: (text, _options, ast) => ParseResult.fail(new ParseResult.Forbidden(ast, text, "TestCoverageAsMarkdown is one-way."))
|
|
59
|
+
});
|
|
60
|
+
const testCoverage = publicProcedure.input(Schema.standardSchemaV1(Schema.Struct({ project: Schema.optional(Schema.String) }))).query(async ({ ctx, input }) => ctx.runtime.runPromise(Effect.gen(function* () {
|
|
61
|
+
const reader = yield* DataReader;
|
|
62
|
+
const project = input.project ?? "default";
|
|
63
|
+
const coverageOpt = yield* reader.getCoverage(project);
|
|
64
|
+
if (Option.isNone(coverageOpt)) return {
|
|
65
|
+
dataAvailable: false,
|
|
66
|
+
project
|
|
67
|
+
};
|
|
68
|
+
return {
|
|
69
|
+
dataAvailable: true,
|
|
70
|
+
project,
|
|
71
|
+
coverage: coverageOpt.value
|
|
72
|
+
};
|
|
73
|
+
})));
|
|
74
|
+
|
|
75
|
+
//#endregion
|
|
76
|
+
export { TestCoverageAsMarkdown, TestCoverageResult, testCoverage };
|
package/tools/errors.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { publicProcedure } from "../context.js";
|
|
2
|
+
import { DataReader } from "@vitest-agent/sdk";
|
|
3
|
+
import { Effect, ParseResult, Schema } from "effect";
|
|
4
|
+
|
|
5
|
+
//#region src/tools/errors.ts
|
|
6
|
+
/**
|
|
7
|
+
* `test_errors` MCP tool — Schema-driven implementation.
|
|
8
|
+
*
|
|
9
|
+
* The Effect Schema `TestErrorsResult` is the canonical contract for
|
|
10
|
+
* the tool's output. The same Schema:
|
|
11
|
+
* - types the procedure's return value;
|
|
12
|
+
* - drives `formatTestErrorsMarkdown` (input typed via `Schema.Type`);
|
|
13
|
+
* - composes into `TestErrorsAsMarkdown`, a one-way
|
|
14
|
+
* `Schema.transformOrFail` whose `encode` direction renders the
|
|
15
|
+
* markdown the text channel carries (decode is forbidden because
|
|
16
|
+
* markdown rendering is lossy);
|
|
17
|
+
* - bridges to zod via `effectToZodSchema` for the SDK's
|
|
18
|
+
* `outputSchema` field, so the structured shape we declare to MCP
|
|
19
|
+
* stays in lockstep with what the procedure actually emits.
|
|
20
|
+
*
|
|
21
|
+
* @packageDocumentation
|
|
22
|
+
*/
|
|
23
|
+
/** One row in the structured `errors[]` array. */
|
|
24
|
+
const TestErrorRow = Schema.Struct({
|
|
25
|
+
id: Schema.Number.annotations({
|
|
26
|
+
title: "test_errors.id",
|
|
27
|
+
description: "Numeric primary key of this error row. Pass as `citedTestErrorId` when calling `hypothesis (action: record)`."
|
|
28
|
+
}),
|
|
29
|
+
topStackFrameId: Schema.NullOr(Schema.Number).annotations({
|
|
30
|
+
title: "stack_frames.id (top frame)",
|
|
31
|
+
description: "`stack_frames.id` of the top frame (ordinal=0); `null` when no frames were captured. Pass as `citedStackFrameId` to `hypothesis (action: record)`."
|
|
32
|
+
}),
|
|
33
|
+
name: Schema.NullOr(Schema.String).annotations({ description: "Error class name (e.g. `AssertionError`, `TypeError`); `null` when the underlying throw provided no name." }),
|
|
34
|
+
message: Schema.String.annotations({ description: "Error message text as the test framework reported it." }),
|
|
35
|
+
diff: Schema.NullOr(Schema.String).annotations({ description: "Unified-diff representation of expected vs. actual when the assertion produced one; `null` otherwise." }),
|
|
36
|
+
actual: Schema.NullOr(Schema.String).annotations({ description: "Actual value the assertion received, when captured." }),
|
|
37
|
+
expected: Schema.NullOr(Schema.String).annotations({ description: "Expected value the assertion compared against, when captured." }),
|
|
38
|
+
stack: Schema.NullOr(Schema.String).annotations({ description: "Newline-joined stack frames as the framework formatted them; structured frames live in `stack_frames`." }),
|
|
39
|
+
scope: Schema.Literal("test", "suite", "module", "unhandled").annotations({ description: "Where the error fired: `test` (a single test case), `suite` (a `describe` setup), `module` (collection / import time), or `unhandled` (uncaught from a background context)." }),
|
|
40
|
+
testFullName: Schema.NullOr(Schema.String).annotations({ description: "Full hierarchical test name (`describe > it`); `null` for non-test scopes (`module`, `unhandled`)." }),
|
|
41
|
+
moduleFile: Schema.NullOr(Schema.String).annotations({ description: "Repo-relative path of the test module the error originated in." })
|
|
42
|
+
}).annotations({
|
|
43
|
+
identifier: "TestErrorRow",
|
|
44
|
+
title: "Test error row",
|
|
45
|
+
description: "Single error captured during a test run, joined with stack frame and source-location context."
|
|
46
|
+
});
|
|
47
|
+
/** Top-level structured payload — populates `structuredContent`. */
|
|
48
|
+
const TestErrorsResult = Schema.Struct({
|
|
49
|
+
project: Schema.String.annotations({
|
|
50
|
+
title: "Project name",
|
|
51
|
+
description: "Workspace project key the run was attributed to (e.g. `playground`, `@org/pkg`).",
|
|
52
|
+
examples: ["playground", "@org/pkg"]
|
|
53
|
+
}),
|
|
54
|
+
errorName: Schema.optional(Schema.String).annotations({ description: "Echo of the optional `errorName` filter the caller passed; absent when no filter was applied." }),
|
|
55
|
+
count: Schema.Number.annotations({ description: "Total error rows in `errors`." }),
|
|
56
|
+
errors: Schema.Array(TestErrorRow).annotations({ description: "Errors from the most recent test run for this project, optionally filtered by `errorName`. Empty when no errors matched." })
|
|
57
|
+
}).annotations({
|
|
58
|
+
identifier: "TestErrorsResult",
|
|
59
|
+
title: "test_errors result",
|
|
60
|
+
description: "Structured payload of the `test_errors` MCP tool. Carries the cite-able test_errors.id and stack_frames.id values agents need for `hypothesis (action: record)`."
|
|
61
|
+
});
|
|
62
|
+
const TRUNCATION_LIMIT = 500;
|
|
63
|
+
const truncate = (s) => s.length <= TRUNCATION_LIMIT ? {
|
|
64
|
+
value: s,
|
|
65
|
+
truncated: false
|
|
66
|
+
} : {
|
|
67
|
+
value: s.slice(0, TRUNCATION_LIMIT),
|
|
68
|
+
truncated: true
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Pure markdown renderer. Exposed so the formatter tests can exercise
|
|
72
|
+
* it without rebuilding a `Schema.encode` runtime.
|
|
73
|
+
*/
|
|
74
|
+
const formatTestErrorsMarkdown = (data) => {
|
|
75
|
+
if (data.errors.length === 0) return `No errors found for project \`${data.project}\`.`;
|
|
76
|
+
const lines = [`# Test Errors — ${data.project}`, ""];
|
|
77
|
+
for (const error of data.errors) {
|
|
78
|
+
const name = error.name ?? "(unnamed)";
|
|
79
|
+
const idTokens = `[testErrorId=${error.id}${error.topStackFrameId !== null ? ` topStackFrameId=${error.topStackFrameId}` : ""}]`;
|
|
80
|
+
lines.push(`## ${name} ${idTokens}`);
|
|
81
|
+
lines.push("");
|
|
82
|
+
lines.push(`**Scope:** ${error.scope}`);
|
|
83
|
+
if (error.testFullName !== null) lines.push(`**Test:** ${error.testFullName}`);
|
|
84
|
+
if (error.moduleFile !== null) lines.push(`**File:** \`${error.moduleFile}\``);
|
|
85
|
+
lines.push("");
|
|
86
|
+
lines.push("**Cite-able IDs (for `hypothesis (action: record)`):**");
|
|
87
|
+
lines.push(`- citedTestErrorId: ${error.id}`);
|
|
88
|
+
if (error.topStackFrameId !== null) lines.push(`- citedStackFrameId: ${error.topStackFrameId}`);
|
|
89
|
+
else lines.push("- citedStackFrameId: (none — no stack frames recorded for this error)");
|
|
90
|
+
lines.push("");
|
|
91
|
+
lines.push("**Message:**");
|
|
92
|
+
lines.push(`> ${error.message.split("\n").join("\n> ")}`);
|
|
93
|
+
if (error.diff !== null) {
|
|
94
|
+
lines.push("");
|
|
95
|
+
lines.push("**Diff:**");
|
|
96
|
+
lines.push("```diff");
|
|
97
|
+
const t = truncate(error.diff);
|
|
98
|
+
lines.push(t.value);
|
|
99
|
+
if (t.truncated) lines.push("... (truncated)");
|
|
100
|
+
lines.push("```");
|
|
101
|
+
}
|
|
102
|
+
if (error.stack !== null && error.diff === null) {
|
|
103
|
+
lines.push("");
|
|
104
|
+
lines.push("**Stack:**");
|
|
105
|
+
lines.push("```");
|
|
106
|
+
const t = truncate(error.stack);
|
|
107
|
+
lines.push(t.value);
|
|
108
|
+
if (t.truncated) lines.push("... (truncated)");
|
|
109
|
+
lines.push("```");
|
|
110
|
+
}
|
|
111
|
+
lines.push("");
|
|
112
|
+
}
|
|
113
|
+
return lines.join("\n");
|
|
114
|
+
};
|
|
115
|
+
/**
|
|
116
|
+
* One-way codec: structured `TestErrorsResult` → markdown text.
|
|
117
|
+
*
|
|
118
|
+
* `Schema.transformOrFail`'s `decode` direction goes
|
|
119
|
+
* `From.Type → To.Encoded`; in this transform `From = TestErrorsResult`
|
|
120
|
+
* and `To = Schema.String`, so the resulting schema is
|
|
121
|
+
* `Schema<string, TestErrorsResultType>` — its parsed Type is the
|
|
122
|
+
* markdown string and its Encoded form is the structured row. That
|
|
123
|
+
* means `Schema.decode(TestErrorsAsMarkdown)(data)` produces markdown
|
|
124
|
+
* (the rendering direction) and `Schema.encode(...)` would attempt
|
|
125
|
+
* the lossy reverse, which is forbidden here.
|
|
126
|
+
*
|
|
127
|
+
* Boundary callers should use
|
|
128
|
+
* `Schema.decodeSync(TestErrorsAsMarkdown)(data)` to render. Test
|
|
129
|
+
* suites can drive the same path without mocking anything — the
|
|
130
|
+
* transform IS the rendering contract.
|
|
131
|
+
*/
|
|
132
|
+
const TestErrorsAsMarkdown = Schema.transformOrFail(TestErrorsResult, Schema.String, {
|
|
133
|
+
strict: true,
|
|
134
|
+
decode: (data) => ParseResult.succeed(formatTestErrorsMarkdown(data)),
|
|
135
|
+
encode: (text, _options, ast) => ParseResult.fail(new ParseResult.Forbidden(ast, text, "TestErrorsAsMarkdown is one-way: markdown cannot be parsed back to TestErrorsResult. Consume the procedure's structured output (or MCP structuredContent) directly."))
|
|
136
|
+
});
|
|
137
|
+
const testErrors = publicProcedure.input(Schema.standardSchemaV1(Schema.Struct({
|
|
138
|
+
project: Schema.String,
|
|
139
|
+
errorName: Schema.optional(Schema.String)
|
|
140
|
+
}))).query(async ({ ctx, input }) => ctx.runtime.runPromise(Effect.gen(function* () {
|
|
141
|
+
const errors = yield* (yield* DataReader).getErrors(input.project, input.errorName);
|
|
142
|
+
return {
|
|
143
|
+
project: input.project,
|
|
144
|
+
...input.errorName !== void 0 && { errorName: input.errorName },
|
|
145
|
+
count: errors.length,
|
|
146
|
+
errors
|
|
147
|
+
};
|
|
148
|
+
})));
|
|
149
|
+
|
|
150
|
+
//#endregion
|
|
151
|
+
export { TestErrorsAsMarkdown, TestErrorsResult, testErrors };
|