@vitest-agent/mcp 2.4.6 → 2.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +1 -1
- package/package.json +1 -1
- package/server.js +2 -2
- package/tools/run-tests.js +101 -11
package/index.js
CHANGED
|
@@ -12,7 +12,7 @@ import { parseSessionEnvExports, recoverSessionContextFromSessionEnv } from "./s
|
|
|
12
12
|
*
|
|
13
13
|
* @public
|
|
14
14
|
*/
|
|
15
|
-
const CURRENT_MCP_VERSION = "2.4.
|
|
15
|
+
const CURRENT_MCP_VERSION = "2.4.7";
|
|
16
16
|
|
|
17
17
|
//#endregion
|
|
18
18
|
export { CURRENT_MCP_VERSION, McpLive, appRouter, buildMcpServer, createCallerFactory, createCurrentSessionIdRef, createSessionContextRef, parseSessionEnvExports, recoverSessionContextFromSessionEnv, startMcpServer };
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -405,11 +405,11 @@ function buildMcpServer(ctx) {
|
|
|
405
405
|
};
|
|
406
406
|
});
|
|
407
407
|
server.registerTool("run_tests", {
|
|
408
|
-
description: "Use to run Vitest tests, with optional file, project, and tag filters. structuredContent carries the typed AgentReport plus per-test classifications (discriminate on `kind`: ok, timeout, error, no-match). Unknown parameters are rejected — accepted keys are files, project, tags, passWithNoTests, timeout, projectRoot.
|
|
408
|
+
description: "Use to run Vitest tests, with optional file, project, and tag filters. structuredContent carries the typed AgentReport plus per-test classifications (discriminate on `kind`: ok, timeout, error, no-match). Unknown parameters are rejected — accepted keys are files, project, tags, passWithNoTests, timeout, projectRoot. When projectRoot is omitted, the server anchors the Vitest root at the directory of the vitest (or vite) config Vitest would load anyway, walking up from its boot dir and stopping at the git root — so a server booted inside a package subtree still resolves the root config's relative globalSetup/setupFiles correctly. projectRoot overrides that for this call and is used verbatim, but only after validation: it must be an existing directory belonging to the same git repository as ctx.cwd (checked via `git rev-parse --git-common-dir`, which is identical across a repo and all its worktrees, including a sibling worktree checked out from the same repo). A path in a different repository, or a non-existent path, is rejected with `{ kind: \"error\" }` naming both paths — never a silent fallback to ctx.cwd. The resolved root actually used is always echoed back on success. The legacy format=json arg is dropped — structuredContent supersedes it.",
|
|
409
409
|
inputSchema: strict({
|
|
410
410
|
files: z.optional(z.array(z.string())).describe("Test file paths to run"),
|
|
411
411
|
project: z.optional(z.string()).describe("Project name to filter"),
|
|
412
|
-
projectRoot: z.optional(z.string()).describe("Explicit Vitest root to
|
|
412
|
+
projectRoot: z.optional(z.string()).describe("Explicit Vitest root for this call, used verbatim. Omit it to get the config-anchored default (walk up from the server's boot dir for a vitest/vite config, bounded at the git root). Prefer an absolute path; a relative path is resolved against ctx.cwd, not the server process's cwd. Validated: must be an existing directory in the same git repository as ctx.cwd (same git-common-dir, e.g. a sibling worktree). Rejected with { kind: 'error' } naming both paths otherwise."),
|
|
413
413
|
tags: z.optional(strict({
|
|
414
414
|
all: z.optional(z.array(z.string())).describe("Require every listed tag"),
|
|
415
415
|
any: z.optional(z.array(z.string())).describe("Require at least one listed tag"),
|
package/tools/run-tests.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { publicProcedure } from "../context.js";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
2
3
|
import { AgentReport, DataReader, DataStore, buildAgentReport, buildConsoleLeaks, coerceErrorField, collectConsoleLeakEntries } from "@vitest-agent/sdk";
|
|
3
4
|
import { Effect, Schema, SchemaGetter } from "effect";
|
|
4
5
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
5
6
|
import { execFile } from "node:child_process";
|
|
6
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
7
|
+
import { existsSync, mkdtempSync, rmSync } from "node:fs";
|
|
7
8
|
import { realpath, stat } from "node:fs/promises";
|
|
8
9
|
import { tmpdir } from "node:os";
|
|
9
|
-
import { join, resolve } from "node:path";
|
|
10
|
+
import { dirname, join, resolve } from "node:path";
|
|
10
11
|
import { Writable } from "node:stream";
|
|
12
|
+
import { pathToFileURL } from "node:url";
|
|
11
13
|
import { promisify } from "node:util";
|
|
12
14
|
|
|
13
15
|
//#region src/tools/run-tests.ts
|
|
@@ -192,15 +194,52 @@ async function resolveGitCommonDir(dir) {
|
|
|
192
194
|
return null;
|
|
193
195
|
}
|
|
194
196
|
}
|
|
197
|
+
const VITEST_CONFIG_EXTENSIONS = [
|
|
198
|
+
"ts",
|
|
199
|
+
"mts",
|
|
200
|
+
"cts",
|
|
201
|
+
"js",
|
|
202
|
+
"mjs",
|
|
203
|
+
"cjs"
|
|
204
|
+
];
|
|
205
|
+
function dirHasVitestOrViteConfig(dir) {
|
|
206
|
+
for (const prefix of ["vitest.config.", "vite.config."]) for (const ext of VITEST_CONFIG_EXTENSIONS) if (existsSync(join(dir, `${prefix}${ext}`))) return true;
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Walk UP from `startDir` looking for the vitest/vite config Vitest would
|
|
211
|
+
* load anyway, returning the directory that holds it. Bounded at the git
|
|
212
|
+
* root (inclusive — the directory containing `.git` is still examined
|
|
213
|
+
* before the walk stops). Returns `startDir` unchanged when no config is
|
|
214
|
+
* found in range, or when anything about the walk throws. See the
|
|
215
|
+
* issue #259 comment above `validateProjectRoot` for the full rationale.
|
|
216
|
+
*
|
|
217
|
+
* @internal exported for tests
|
|
218
|
+
*/
|
|
219
|
+
function resolveConfigAnchoredRoot(startDir) {
|
|
220
|
+
try {
|
|
221
|
+
let dir = resolve(startDir);
|
|
222
|
+
for (;;) {
|
|
223
|
+
if (dirHasVitestOrViteConfig(dir)) return dir;
|
|
224
|
+
if (existsSync(join(dir, ".git"))) return startDir;
|
|
225
|
+
const parent = dirname(dir);
|
|
226
|
+
if (parent === dir) return startDir;
|
|
227
|
+
dir = parent;
|
|
228
|
+
}
|
|
229
|
+
} catch {
|
|
230
|
+
return startDir;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
195
233
|
/**
|
|
196
234
|
* Validate an optional caller-supplied `projectRoot` against `ctxCwd`
|
|
197
|
-
* (the MCP server's boot-time root). `undefined`
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
235
|
+
* (the MCP server's boot-time root). `undefined` anchors at the directory
|
|
236
|
+
* of the vitest/vite config Vitest would load anyway (issue #259, via
|
|
237
|
+
* `resolveConfigAnchoredRoot`) — never inferred beyond that, never
|
|
238
|
+
* defaulted to anything else when no config is found in range.
|
|
201
239
|
*
|
|
202
|
-
* A supplied `projectRoot` is VALIDATED, not trusted
|
|
203
|
-
*
|
|
240
|
+
* A supplied `projectRoot` is VALIDATED, not trusted, and used VERBATIM
|
|
241
|
+
* once validated — explicit is explicit, no anchoring applied. It must
|
|
242
|
+
* resolve to an existing directory that shares a git common directory with
|
|
204
243
|
* `ctxCwd` (same repository, including across worktrees). Any failure
|
|
205
244
|
* returns `{ ok: false, message }` naming both paths — never a silent
|
|
206
245
|
* fallback to `ctxCwd`, never a raw throw.
|
|
@@ -210,7 +249,7 @@ async function resolveGitCommonDir(dir) {
|
|
|
210
249
|
async function validateProjectRoot(projectRoot, ctxCwd) {
|
|
211
250
|
if (projectRoot === void 0) return {
|
|
212
251
|
ok: true,
|
|
213
|
-
root: ctxCwd
|
|
252
|
+
root: resolveConfigAnchoredRoot(ctxCwd)
|
|
214
253
|
};
|
|
215
254
|
const resolvedRoot = resolve(ctxCwd, projectRoot);
|
|
216
255
|
let isDirectory;
|
|
@@ -236,6 +275,57 @@ async function validateProjectRoot(projectRoot, ctxCwd) {
|
|
|
236
275
|
root: resolvedRoot
|
|
237
276
|
};
|
|
238
277
|
}
|
|
278
|
+
/**
|
|
279
|
+
* Issue #303: resolve `vitest/node` anchored at the run's project root
|
|
280
|
+
* instead of the bare `"vitest/node"` specifier, which resolves relative to
|
|
281
|
+
* `@vitest-agent/mcp`'s OWN install location. `vitest` is a peerDependency
|
|
282
|
+
* of this package, and pnpm routinely materializes MORE THAN ONE physical
|
|
283
|
+
* instance of the same vitest version when peer-resolution hashes differ
|
|
284
|
+
* (e.g. `vitest@4.1.11_@types+node@26.2.0_...` alongside
|
|
285
|
+
* `vitest@4.1.11_@types+node@26.3.0_...` under `node_modules/.pnpm`). When
|
|
286
|
+
* the bare specifier resolves to a DIFFERENT physical copy than the one the
|
|
287
|
+
* project's test files import, `SnapshotClient.setup()` runs against one
|
|
288
|
+
* copy's module-level `_client` singleton while `expect(...).toMatchSnapshot()`
|
|
289
|
+
* inside the test file goes through the other copy's singleton, which has
|
|
290
|
+
* no state — every snapshot assertion then fails with "The snapshot state
|
|
291
|
+
* for '<file>' is not found. Did you call 'SnapshotClient.setup()'?" while
|
|
292
|
+
* every non-snapshot assertion still passes.
|
|
293
|
+
*
|
|
294
|
+
* `createRequire` needs a file path (not a bare directory) to anchor
|
|
295
|
+
* resolution, hence the synthetic, never-created `__vitest-agent-resolver__.js`
|
|
296
|
+
* filename joined onto `root`. vitest's package.json `exports` map for
|
|
297
|
+
* `./node` carries a `default` condition (`./dist/node.js`) and vitest ships
|
|
298
|
+
* `"type": "module"`, so `require.resolve("vitest/node")` resolves correctly
|
|
299
|
+
* even though vitest itself is ESM — the result is then converted to a
|
|
300
|
+
* `file://` URL, which is what dynamic `import()` needs.
|
|
301
|
+
*
|
|
302
|
+
* Falls back to the bare `"vitest/node"` specifier when root-anchored
|
|
303
|
+
* resolution throws (e.g. a project root with no local vitest install) so
|
|
304
|
+
* that case keeps working exactly as it did before this fix.
|
|
305
|
+
*
|
|
306
|
+
* @internal exported for tests
|
|
307
|
+
*/
|
|
308
|
+
function resolveVitestNodeEntry(root) {
|
|
309
|
+
try {
|
|
310
|
+
const req = createRequire(join(root, "__vitest-agent-resolver__.js"));
|
|
311
|
+
return pathToFileURL(req.resolve("vitest/node")).href;
|
|
312
|
+
} catch {
|
|
313
|
+
return "vitest/node";
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Indirection seam around `import(<vitest/node entry>)`. vitest's own
|
|
318
|
+
* vite-node externalizes "vitest"/"vitest/node" for every importer, and
|
|
319
|
+
* `vi.mock("vitest/node", ...)` only special-cases AST-literal
|
|
320
|
+
* `import("vitest/node")` call sites for interception — a computed
|
|
321
|
+
* specifier (unavoidable here; see `resolveVitestNodeEntry`) silently
|
|
322
|
+
* bypasses that interception and loads the real module. Tests substitute
|
|
323
|
+
* `.load` directly (mutating this shared object's property — no `vi.mock`
|
|
324
|
+
* required) instead of trying to mock the module.
|
|
325
|
+
*
|
|
326
|
+
* @internal exported for tests
|
|
327
|
+
*/
|
|
328
|
+
const vitestLoader = { load: (entry) => import(entry) };
|
|
239
329
|
let _runTestsChain = Promise.resolve();
|
|
240
330
|
function serializeRunTests(fn) {
|
|
241
331
|
const next = _runTestsChain.then(fn, fn);
|
|
@@ -416,7 +506,7 @@ const runTests = publicProcedure.input(Schema.toStandardSchemaV1(Schema.Struct({
|
|
|
416
506
|
const nullStream = new Writable({ write(_chunk, _encoding, cb) {
|
|
417
507
|
cb();
|
|
418
508
|
} });
|
|
419
|
-
const { createVitest } = await
|
|
509
|
+
const { createVitest } = await vitestLoader.load(resolveVitestNodeEntry(resolvedRoot));
|
|
420
510
|
let vitest;
|
|
421
511
|
let covOverride;
|
|
422
512
|
try {
|
|
@@ -522,4 +612,4 @@ const runTests = publicProcedure.input(Schema.toStandardSchemaV1(Schema.Struct({
|
|
|
522
612
|
}));
|
|
523
613
|
|
|
524
614
|
//#endregion
|
|
525
|
-
export { RunTestsAsMarkdown, RunTestsResult, coerceErrors, composeTagExpression, formatNoMatchMarkdown, formatReportMarkdown, formatRunTestsMarkdown, makeCoverageDirOverride, readDiscoveryLastScannedAt, resolveGitCommonDir, runTests, sanitizeTestArgs, validateProjectRoot, withStdioCaptured };
|
|
615
|
+
export { RunTestsAsMarkdown, RunTestsResult, coerceErrors, composeTagExpression, formatNoMatchMarkdown, formatReportMarkdown, formatRunTestsMarkdown, makeCoverageDirOverride, readDiscoveryLastScannedAt, resolveConfigAnchoredRoot, resolveGitCommonDir, resolveVitestNodeEntry, runTests, sanitizeTestArgs, validateProjectRoot, vitestLoader, withStdioCaptured };
|