test(cli,tui): add in-process source coverage gates (#1484) (#1502)

* test(cli,tui): add in-process source coverage gates (#1484)

CLI and TUI source had no coverage gate: CLI tests spawned the built
binary out-of-process (coverage never saw clients/cli/src) and the TUI
had only two smoke tests. Regressions there were invisible to CI.

CLI:
- Rewrite __tests__/helpers/cli-runner.ts to invoke runCli() in-process
  (capture process.std*.write, map thrown errors to an exit code),
  preserving the CliResult shape so all existing tests run unchanged and
  clients/cli/src is now measured under v8 instrumentation.
- cli.ts: program.exitOverride() so a commander usage error throws
  (catchable in-process) instead of tearing down the worker; --help /
  --version still exit cleanly. Drop the dead, never-imported main().
- Add a thin out-of-process e2e.test.ts that spawns the built binary for
  the shebang / process.exit paths the in-process runner can't reach.
- Add @vitest/coverage-v8, a per-file gate (90/85/80/50) excluding only
  src/index.ts (binary bootstrap), and pool: 'forks' for capture
  isolation. cli.ts lands at ~98% lines / 100% functions.

TUI:
- Add @vitest/coverage-v8 and an interim per-file gate over the
  non-React logic (logger.ts, components/tabsConfig.ts, utils/*); the
  Ink components, App.tsx, and hooks/ are excluded with a documented
  follow-up (#1501). Add tests covering the included modules (100%).

Wiring + docs:
- CI runs each client's test:coverage; root test:cli / test:tui (used by
  validate) and root test:coverage now enforce the gates.
- gitignore clients/**/coverage; update AGENTS.md and client READMEs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(cli): clarify in-process runner footguns from PR #1502 review

Review notes on the in-process CLI test runner (no behavior change):
- Warn in runCli's docblock that --help/--version must NOT go through the
  in-process runner: exitOverride only re-throws non-zero exits, so exitCode
  0 falls through to process.exit(0) and would kill the vitest worker. Those
  belong in the out-of-process e2e.test.ts layer.
- Document that a timeout orphans the uncancellable invokeCli promise (its
  stdio subprocess stays alive; later writes hit the restored stdout) — a
  known, acceptable trade-off on the hang-only failure path.
- Fix a stale reference to the removed main() (errors now route through
  index.ts).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(cli): fix remaining stale main() reference in vitest exclusion comment

PR #1502 re-review caught a second stale `main()` mention (the first was
fixed in 52c23e41). src/index.ts inlines the bootstrap as
runCli(process.argv).then(...).catch(handleError) — there is no main().
Comment-only; no behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: compose root test:coverage from per-client scripts

Replace the brittle inline cd-chain with a composition of the existing
test:cli / test:tui scripts plus a new matching test:web. Behavior is
unchanged (web → cli → tui coverage gates) but there's a single source of
truth for each client's coverage invocation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Cliff Hall 2026-06-20 15:05:58 -04:00 • committed by GitHub
commit 388ea5a7bd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 1304 additions and 197 deletions

View file

@ -38,13 +38,18 @@ jobs:
working-directory: ./clients/web
run: npm run test:coverage
- name: Run CLI tests
- name: Run CLI tests with coverage
# In-process runCli() tests under v8 instrumentation + a thin
# out-of-process E2E layer; enforces the per-file gate for
# clients/cli/src (#1484).
working-directory: ./clients/cli
run: npm run test
run: npm run test:coverage
- name: Run TUI tests
- name: Run TUI tests with coverage
# Enforces the per-file gate for the TUI's non-React logic modules
# (interim scope — see clients/tui/vitest.config.ts and #1501).
working-directory: ./clients/tui
run: npm run test
run: npm run test:coverage
- name: Build CLI, TUI, and launcher
run: npm run build:cli && npm run build:tui && npm run build:launcher

1
.gitignore vendored
View file

@ -6,6 +6,7 @@ node_modules
dist
clients/web/dist
clients/**/build
clients/**/coverage
test-servers/build
.env
/.playwright-mcp/

View file

@ -121,7 +121,10 @@ All work should be driven by items on the project board.
- Run CLI tests with `npm run test` from `clients/cli/` (builds test-servers + CLI bin first via `pretest`)
- Run TUI tests with `npm run test` from `clients/tui/`
- From repo root, `npm run test` runs web unit tests, then CLI and TUI tests
- Run `npm run test:coverage` to verify the per-file gate: lines ≥ 90, statements ≥ 85, functions ≥ 80, branches ≥ 50 (CI enforces this gate). Branches is intentionally relaxed because Mantine portal/media-query branches are not exercisable under happy-dom; new business-logic branches should still be covered.
- Run `npm run test:coverage` to verify the per-file gate: lines ≥ 90, statements ≥ 85, functions ≥ 80, branches ≥ 50 (CI enforces this gate). Branches is intentionally relaxed because Mantine portal/media-query branches are not exercisable under happy-dom; new business-logic branches should still be covered. From the repo root, `npm run test:coverage` runs the gate for web **and** CLI **and** TUI; each client also exposes its own `test:coverage`.
- The **same per-file gate** is enforced for the CLI and TUI (#1484), not just web:
- **CLI** (`clients/cli`): tests run **in-process** by importing `runCli()` (see `__tests__/helpers/cli-runner.ts`) so `clients/cli/src` is measured under v8 instrumentation. A thin out-of-process layer (`__tests__/e2e.test.ts` + `scripts/smoke-cli.mjs`) still spawns the built binary for the shebang/`process.exit` paths; `src/index.ts` (binary bootstrap) is the only coverage exclusion. `commander` uses `.exitOverride()` so a parse error throws instead of tearing down the test worker.
- **TUI** (`clients/tui`): the gate covers the **non-React logic** only — `logger.ts`, `components/tabsConfig.ts`, and `utils/*` (server resolution lives in `core/` and is measured by the web suite). The Ink components, `App.tsx`, and `hooks/` are an **interim exclusion** in `clients/tui/vitest.config.ts` pending the renderer-based follow-up (#1501). When adding new **non-React** logic under `clients/tui/src`, it falls under the gate automatically — add tests for it.
- Run `npm run test:integration` (also from `clients/web/`) for the v1.5-ported InspectorClient + transport + auth integration suite. It runs under a separate `integration` vitest project in node env (no happy-dom) with 30s timeouts. The script builds `test-servers/` first via `tsc -p ../../test-servers --noCheck` so the stdio MCP test server can be spawned as a real subprocess. CI runs it as its own step after unit tests.
- Test files live alongside the source as `<Name>.test.tsx` (or `.test.ts` for non-React modules). v1.5-ported integration tests live under `clients/web/src/test/integration/`, mirroring the `core/` source layout (`mcp/`, `mcp/node/`, `mcp/remote/`, `auth/`, `auth/node/`, `storage/`). Any test file under that folder is automatically picked up by the `integration` vitest project (node env, 30s timeouts) via the folder glob in `vite.config.ts` — placement is the manifest, there is no enumeration to keep in sync. Tests outside the folder run in the `unit` project (happy-dom). When adding a new test for, e.g., `core/mcp/remote/foo.ts`, put it at `src/test/integration/mcp/remote/foo.test.ts`.
- Use `renderWithMantine` from `src/test/renderWithMantine.tsx` to render components — it wraps in `MantineProvider` with the project theme
@ -134,7 +137,7 @@ All work should be driven by items on the project board.
### Lint-fixed, Formatted code
- ALWAYS do `npm run format` before committing — it auto-fixes any Prettier issues. `validate` runs `format:check` (the non-fixing variant) and will fail in CI on any unformatted file, so always run the auto-fixer first rather than letting `format:check` catch it.
- ALWAYS do `npm run validate` before pushing any changes — from the repo root it mirrors what CI enforces across every client: web `format:check` + `lint` + `build` + `test:coverage` (`validate:web`), then the CLI and TUI test suites (`test:cli` / `test:tui`), then a build of CLI + TUI + launcher, the launcher `--help` smoke (`smoke:launcher`), the prod CLI and TUI end-to-end smokes (`smoke:cli` / `smoke:tui`), and the prod web end-to-end smoke (`smoke:web`) — together `validate:launcher`. The sub-scripts (`validate:web`, `validate:launcher`, `smoke:launcher`, `smoke:cli`, `smoke:tui`, `smoke:web`) can be run individually when iterating on one client. Storybook is the only CI step left out (see below).
- ALWAYS do `npm run validate` before pushing any changes — from the repo root it mirrors what CI enforces across every client: web `format:check` + `lint` + `build` + `test:coverage` (`validate:web`), then the CLI and TUI suites **with their coverage gates** (`test:cli` / `test:tui`, which now run each client's `test:coverage`), then a build of CLI + TUI + launcher, the launcher `--help` smoke (`smoke:launcher`), the prod CLI and TUI end-to-end smokes (`smoke:cli` / `smoke:tui`), and the prod web end-to-end smoke (`smoke:web`) — together `validate:launcher`. The sub-scripts (`validate:web`, `validate:launcher`, `smoke:launcher`, `smoke:cli`, `smoke:tui`, `smoke:web`) can be run individually when iterating on one client. Storybook is the only CI step left out (see below).
- `smoke:web` (`scripts/smoke-web.mjs`) starts `mcp-inspector --web` (prod, no `--dev`) against the built `clients/web/dist` and asserts `GET /` serves the SPA (HTTP 200) with the injected `__INSPECTOR_API_TOKEN__`. Prod `--web` serves from `clients/web/dist`, which ships in the published package but is absent in a fresh checkout — the runner builds it on demand (`build:client` = `vite build`) on first launch, or exits with an actionable error if that build can't run (see `clients/web/server/ensure-web-build.ts` and the launcher README). `--dev` runs Vite directly and never needs `dist`.
- `smoke:cli` (`scripts/smoke-cli.mjs`) drives `mcp-inspector --cli` through the built launcher against the bundled stdio test server via a temp `--catalog`: it asserts `tools/list` returns the server's tools (real connect over stdio), the default writable catalog is seeded empty on first run, a missing read-only `--config` errors without seeding, and `--catalog` + `--config` is rejected. `smoke:tui` (`scripts/smoke-tui.mjs`) launches `mcp-inspector --tui --catalog <temp>` and asserts the Ink app renders its first frame (the "MCP Servers" panel) within a timeout, then SIGTERMs it — a shallow boot/render check, not full interaction. Both build `test-servers/build` on demand if it's missing.
- Also run `npm run test:storybook` before pushing — it executes every story's `play` function in headless Chromium via `@vitest/browser-playwright` (~10s). CI runs this as a separate step after the unit/lint/build checks; failures block merge. It is kept out of `validate` because it needs the Playwright browser binary and is much slower than the unit suite.

View file

@ -107,3 +107,16 @@ While the Web Client provides a rich visual interface, the CLI is designed for:
- **Automation**: Ideal for CI/CD pipelines and batch processing.
- **AI Coding Assistants**: Provides a direct, machine-readable interface (JSON) for tools like Cursor or Claude to verify changes immediately.
- **Log Analysis**: Easier integration with command-line utilities (like `jq`) to process and analyze MCP server output.
## Development
Run the test suite (and coverage gate) from `clients/cli/`:
```bash
npm test # build test-servers + binary, then run all tests
npm run test:coverage # same, under the per-file coverage gate
```
Tests run the CLI **in-process** (importing `runCli()`) so `src/` is measured
under coverage, with a thin out-of-process spawn layer for the real binary. See
[`__tests__/README.md`](./__tests__/README.md) for details.

View file

@ -3,10 +3,13 @@
## Running Tests
```bash
# Run all tests
# Run all tests (pretest builds test-servers + the CLI binary)
npm test
# Run in watch mode (useful for test file changes; won't work on CLI source changes without rebuild)
# Run all tests under the per-file coverage gate
npm run test:coverage
# Run in watch mode (useful for test file changes)
npm run test:watch
# Run specific test file
@ -16,29 +19,51 @@ npm run test:cli-headers # headers.test.ts
npm run test:cli-metadata # metadata.test.ts
```
## How the CLI is exercised
Most tests run the CLI **in-process**: `helpers/cli-runner.ts` imports `runCli()`
from `../src/cli.ts` and invokes it directly, capturing `process.stdout`/`stderr`
and mapping thrown errors to an exit code. This is what lets vitest's v8 coverage
instrument `clients/cli/src` (#1484). Because `commander` uses `.exitOverride()`,
a parse/usage error throws instead of calling `process.exit` and tearing down the
worker.
`e2e.test.ts` is the deliberately thin **out-of-process** layer: it spawns the
**built** `build/index.js` as a real subprocess to cover the shebang, the
`index.ts` `isMain` bootstrap, and the actual `process.exit` codes — the only
parts the in-process runner can't reach. (`scripts/smoke-cli.mjs` at the repo
root provides a further end-to-end check of the binary.)
## Test Files
- `cli.test.ts` - Basic CLI functionality: CLI mode, environment variables, config files, resources, prompts, logging, transport types
- `tools.test.ts` - Tool-related tests: Tool discovery, JSON argument parsing, error handling, prompts
- `headers.test.ts` - Header parsing and validation
- `metadata.test.ts` - Metadata functionality: General metadata, tool-specific metadata, parsing, merging, validation
- `methods.test.ts` - Method/option-validation paths not covered elsewhere (resource templates, missing/invalid options, the `--` target separator)
- `error-handler.test.ts` - The binary's `handleError` error sink, exercised in-process with `process.exit` stubbed
- `e2e.test.ts` - Out-of-process spawn of the built binary (exit codes + boot)
## Helpers
The `helpers/` directory contains shared utilities:
- `cli-runner.ts` - Spawns CLI as subprocess and captures output
- `test-mcp-server.ts` - Standalone stdio MCP server script for stdio transport testing
- `instrumented-server.ts` - In-process MCP test server for HTTP/SSE transports with request recording
- `cli-runner.ts` - Invokes `runCli()` in-process and captures stdout/stderr + exit code
- `assertions.ts` - Custom assertion helpers for CLI output validation
- `fixtures.ts` - Test config file generators and temporary directory management
- `fixtures.ts` - Test config/catalog file generators and temporary directory management
HTTP/SSE test servers (for transport tests) come from the
`@modelcontextprotocol/inspector-test-server` package (`createTestServerHttp`),
and the bundled stdio server is launched via `getTestMcpServerCommand()`.
## Notes
- Tests run in parallel across files (Vitest default)
- Tests within a file run sequentially (we have isolated config files and ports, so we could get more aggressive if desired)
- Config files use `crypto.randomUUID()` for uniqueness in parallel execution
- Test files run in separate forked processes (`pool: 'forks'`), so the in-process
runner's `process.std*.write` patching never overlaps across files; tests within
a file run sequentially
- Config files use `crypto.randomUUID()` for uniqueness
- HTTP/SSE servers use dynamic port allocation to avoid conflicts
- Coverage is not used because much of the code that we want to measure is run by a spawned process, so it can't be tracked by Vitest
- /sample-config.json is no longer used by tests - not clear if this file serves some other purpose so leaving it for now
- All tests now use built-in MCP test servers, there are no external dependencies on servers from a registry
- Coverage is enforced per-file (lines ≥ 90, statements ≥ 85, functions ≥ 80,
branches ≥ 50). `src/index.ts` (the binary bootstrap) is excluded because it
only runs in the spawned binary, which coverage can't instrument
- All tests use the built-in MCP test servers — no external/registry dependencies

View file

@ -0,0 +1,70 @@
import { describe, it, expect } from "vitest";
import { spawn } from "node:child_process";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { getTestMcpServerCommand } from "@modelcontextprotocol/inspector-test-server";
const here = dirname(fileURLToPath(import.meta.url));
const BIN = resolve(here, "../build/index.js");
interface SpawnResult {
exitCode: number | null;
stdout: string;
stderr: string;
}
/**
* Spawn the **built** CLI binary as a real subprocess. This is the deliberately
* thin out-of-process layer that the in-process suite (cli-runner.ts) cannot
* reach: the shebang, `index.ts`'s `isMain` bootstrap, and the actual
* `process.exit` codes. Functional behavior is covered in-process under the
* coverage gate; this only asserts the binary boots and exits correctly. The
* binary is built by the `pretest` / `test:coverage` scripts before tests run.
*/
function spawnCli(args: string[]): Promise<SpawnResult> {
return new Promise((resolvePromise, reject) => {
const child = spawn("node", [BIN, ...args], {
stdio: ["pipe", "pipe", "pipe"],
detached: process.platform !== "win32",
});
let stdout = "";
let stderr = "";
const timer = setTimeout(() => {
if (process.platform !== "win32" && child.pid != null) {
process.kill(-child.pid, "SIGTERM");
} else {
child.kill("SIGTERM");
}
reject(new Error("E2E CLI timed out"));
}, 15000);
child.stdout.on("data", (d) => (stdout += d.toString()));
child.stderr.on("data", (d) => (stderr += d.toString()));
child.on("error", (err) => {
clearTimeout(timer);
reject(err);
});
child.on("close", (exitCode) => {
clearTimeout(timer);
resolvePromise({ exitCode, stdout, stderr });
});
});
}
describe("CLI binary (out-of-process E2E)", () => {
const { command, args } = getTestMcpServerCommand();
it("exits 0 and prints tools JSON on a successful run", async () => {
const result = await spawnCli([command, ...args, "--method", "tools/list"]);
expect(result.exitCode).toBe(0);
const json = JSON.parse(result.stdout);
expect(Array.isArray(json.tools)).toBe(true);
});
it("exits non-zero when required --method is missing", async () => {
const result = await spawnCli([command, ...args]);
expect(result.exitCode).not.toBe(0);
expect(result.stderr).toContain("Method is required");
});
});

View file

@ -0,0 +1,45 @@
import { describe, it, expect, vi, afterEach } from "vitest";
import { handleError } from "../src/error-handler.js";
/**
* `handleError` is the binary's last-resort error sink (wired up in
* `src/index.ts`). It is exercised in-process here — rather than only through
* the spawned binary — so its source is measured under the CLI coverage gate
* (#1484). `process.exit` is stubbed so asserting on it doesn't tear down the
* test worker.
*/
describe("handleError", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("logs an Error's message and exits with code 1", () => {
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
const exitSpy = vi
.spyOn(process, "exit")
.mockImplementation((() => undefined) as never);
handleError(new Error("boom"));
expect(errorSpy).toHaveBeenCalledWith("boom");
expect(exitSpy).toHaveBeenCalledWith(1);
});
it("logs a string error verbatim", () => {
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
vi.spyOn(process, "exit").mockImplementation((() => undefined) as never);
handleError("plain failure");
expect(errorSpy).toHaveBeenCalledWith("plain failure");
});
it("falls back to 'Unknown error' for non-Error, non-string values", () => {
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
vi.spyOn(process, "exit").mockImplementation((() => undefined) as never);
handleError({ unexpected: true });
expect(errorSpy).toHaveBeenCalledWith("Unknown error");
});
});

View file

@ -1,10 +1,4 @@
import { spawn } from "child_process";
import { resolve } from "path";
import { fileURLToPath } from "url";
import { dirname } from "path";
const __dirname = dirname(fileURLToPath(import.meta.url));
const CLI_PATH = resolve(__dirname, "../../build/index.js");
import { runCli as invokeCli } from "../../src/cli.js";
export interface CliResult {
exitCode: number | null;
@ -15,83 +9,142 @@ export interface CliResult {
export interface CliOptions {
timeout?: number;
/**
* Accepted for source-compatibility with the previous out-of-process runner.
* The in-process runner shares the test worker's working directory, so it is
* ignored — no current test sets it. The out-of-process E2E layer
* (`e2e.test.ts`) still honors a real cwd.
*/
cwd?: string;
env?: Record<string, string>;
signal?: AbortSignal;
}
type WriteArgs = [
chunk: unknown,
encoding?: unknown,
callback?: (() => void) | undefined,
];
/**
* Run the CLI with given arguments and capture output
* Build a `process.std{out,err}.write` replacement that appends every chunk to
* a buffer and still honors the optional write callback. `awaitableLog` (the
* CLI's stdout writer) passes its resolve handler as that callback, so it MUST
* be invoked or `runCli` would hang forever waiting for the flush.
*/
function captureWrite(append: (text: string) => void) {
return (...args: WriteArgs): boolean => {
const [chunk, encoding, callback] = args;
append(typeof chunk === "string" ? chunk : String(chunk));
const cb = typeof encoding === "function" ? encoding : callback;
if (typeof cb === "function") cb();
return true;
};
}
/**
* Run the CLI **in-process** by importing and invoking `runCli` directly, so
* its source (`clients/cli/src/**`) is measured under vitest's coverage
* instrumentation. The previous implementation spawned `build/index.js` as a
* subprocess, which left CLI source invisible to coverage (#1484).
*
* The returned shape matches the old out-of-process runner exactly
* (`exitCode` / `stdout` / `stderr` / `output`) so every existing test and the
* shared assertion helpers keep working unchanged:
* - stdout/stderr are captured by temporarily patching `process.std*.write`
* (and `console.error`/`console.warn`, which commander and error paths use).
* - A thrown error (bad args, failed connect, unsupported method) maps to
* `exitCode: 1` with the message appended to stderr — mirroring how the real
* binary (`index.ts`) routes errors through `handleError` → `process.exit(1)`.
* - `options.env` is applied to `process.env` for the duration of the call and
* fully restored afterward.
*
* The real binary, its shebang, and actual `process.exit` codes are covered by
* the thin out-of-process layer in `e2e.test.ts` and `scripts/smoke-cli.mjs`.
*
* DO NOT drive `--help` / `--version` through this runner. `cli.ts` sets
* `program.exitOverride((err) => { if (err.exitCode !== 0) throw err })`, so for
* those (exitCode 0) commander does NOT throw — it falls through to its normal
* `process.exit(0)`, which run in-process would terminate the vitest worker.
* Cover `--help` / `--version` in the out-of-process `e2e.test.ts` layer instead.
*/
export async function runCli(
args: string[],
options: CliOptions = {},
): Promise<CliResult> {
return new Promise((resolve, reject) => {
const child = spawn("node", [CLI_PATH, ...args], {
stdio: ["pipe", "pipe", "pipe"],
cwd: options.cwd,
env: { ...process.env, ...options.env },
signal: options.signal,
// Own process group on Unix so timeout can kill CLI + stdio grandchildren.
detached: process.platform !== "win32",
});
let stdout = "";
let stderr = "";
let stdout = "";
let stderr = "";
let resolved = false;
const originalStdoutWrite = process.stdout.write;
const originalStderrWrite = process.stderr.write;
const originalConsoleError = console.error;
const originalConsoleWarn = console.warn;
// Default timeout of 10 seconds (less than vitest's 15s)
const timeoutMs = options.timeout ?? 10000;
const timeout = setTimeout(() => {
if (!resolved) {
resolved = true;
// Kill the process and all its children
try {
if (process.platform !== "win32" && child.pid != null) {
process.kill(-child.pid, "SIGTERM");
} else {
child.kill("SIGTERM");
}
} catch {
// Process might already be dead, try direct kill
try {
child.kill("SIGKILL");
} catch {
// Process is definitely dead
}
}
reject(new Error(`CLI command timed out after ${timeoutMs}ms`));
}
}, timeoutMs);
// Snapshot and apply env overrides; `undefined` records keys that did not
// exist so they can be deleted (not set to the string "undefined") on restore.
const envBackup: Record<string, string | undefined> = {};
if (options.env) {
for (const [key, value] of Object.entries(options.env)) {
envBackup[key] = process.env[key];
process.env[key] = value;
}
}
child.stdout.on("data", (data) => {
stdout += data.toString();
});
process.stdout.write = captureWrite((text) => {
stdout += text;
}) as typeof process.stdout.write;
process.stderr.write = captureWrite((text) => {
stderr += text;
}) as typeof process.stderr.write;
console.error = (...parts: unknown[]) => {
stderr += parts.map(String).join(" ") + "\n";
};
console.warn = (...parts: unknown[]) => {
stderr += parts.map(String).join(" ") + "\n";
};
child.stderr.on("data", (data) => {
stderr += data.toString();
});
// `runCli` reads `process.argv.slice(2)`, so prepend two placeholder entries
// ([node, script]) to mirror how the binary is launched.
const argv = ["node", "inspector-cli", ...args];
child.on("close", (code) => {
if (!resolved) {
resolved = true;
clearTimeout(timeout);
resolve({
exitCode: code,
stdout,
stderr,
output: stdout + stderr,
});
}
});
child.on("error", (error) => {
if (!resolved) {
resolved = true;
clearTimeout(timeout);
reject(error);
}
});
// Safety net for a genuinely hung CLI. Note: when the timeout wins the race
// below, `invokeCli` is NOT cancellable in-process — it keeps running (its
// stdio MCP subprocess stays alive) and any later writes land on the
// *restored* real stdout, since `finally` has already unpatched it. The old
// out-of-process runner could SIGTERM the child group; this one structurally
// cannot. Acceptable because it only fires on the failure path (a hang),
// which no passing test should reach.
const timeoutMs = options.timeout ?? 10000;
let timer: ReturnType<typeof setTimeout> | undefined;
const timeout = new Promise<never>((_, reject) => {
timer = setTimeout(
() => reject(new Error(`CLI command timed out after ${timeoutMs}ms`)),
timeoutMs,
);
});
let exitCode = 0;
try {
await Promise.race([invokeCli(argv), timeout]);
} catch (error) {
exitCode = 1;
stderr += (error instanceof Error ? error.message : String(error)) + "\n";
} finally {
if (timer) clearTimeout(timer);
process.stdout.write = originalStdoutWrite;
process.stderr.write = originalStderrWrite;
console.error = originalConsoleError;
console.warn = originalConsoleWarn;
for (const [key, value] of Object.entries(envBackup)) {
if (value === undefined) delete process.env[key];
else process.env[key] = value;
}
}
return {
exitCode,
stdout,
stderr,
output: stdout + stderr,
};
}

View file

@ -0,0 +1,86 @@
import { describe, it, expect } from "vitest";
import { getTestMcpServerCommand } from "@modelcontextprotocol/inspector-test-server";
import { runCli } from "./helpers/cli-runner.js";
import {
expectCliSuccess,
expectCliFailure,
expectJsonStructure,
expectOutputContains,
} from "./helpers/assertions.js";
/**
* Covers the CLI method and option-validation branches that the broader
* integration suites don't reach, keeping `src/cli.ts` above the per-file
* coverage gate (#1484). All connect to the bundled stdio test server, whose
* default config advertises resource templates, prompts, and logging.
*/
describe("CLI method coverage", () => {
const { command, args } = getTestMcpServerCommand();
it("lists resource templates", async () => {
const result = await runCli([
command,
...args,
"--cli",
"--method",
"resources/templates/list",
]);
expectCliSuccess(result);
const json = expectJsonStructure(result, ["resourceTemplates"]);
expect(Array.isArray(json.resourceTemplates)).toBe(true);
expect(json.resourceTemplates.length).toBeGreaterThan(0);
});
it("rejects logging/setLevel without a --log-level", async () => {
const result = await runCli([
command,
...args,
"--cli",
"--method",
"logging/setLevel",
]);
expectCliFailure(result);
expectOutputContains(result, "Log level is required");
});
it("treats args before a `--` separator as the server target", async () => {
const result = await runCli([
command,
...args,
"--",
"--method",
"tools/list",
]);
expectCliSuccess(result);
const json = expectJsonStructure(result, ["tools"]);
expect(Array.isArray(json.tools)).toBe(true);
});
it("rejects an invalid --transport value before connecting", async () => {
const result = await runCli([
command,
...args,
"--cli",
"--method",
"tools/list",
"--transport",
"carrier-pigeon",
]);
expectCliFailure(result);
expectOutputContains(result, "Invalid transport type");
});
it("surfaces a commander usage error (missing option argument) as a failure", async () => {
// `--method` with no value makes commander call its own error-exit path;
// exitOverride re-throws it (exitCode !== 0) so the in-process runner can
// catch it instead of the worker being torn down.
const result = await runCli([command, ...args, "--method"]);
expectCliFailure(result);
expectOutputContains(result, "argument missing");
});
});

View file

@ -22,11 +22,72 @@
},
"devDependencies": {
"@types/node": "^24.12.4",
"@vitest/coverage-v8": "^4.1.0",
"tsup": "^8.5.0",
"typescript": "~5.9.3",
"vitest": "^4.1.0"
}
},
"node_modules/@babel/helper-string-parser": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
"integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
"integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
"integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.29.7"
},
"bin": {
"parser": "bin/babel-parser.js"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@babel/types": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
"integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.29.7",
"@babel/helper-validator-identifier": "^7.29.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@bcoe/v8-coverage": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz",
"integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@emnapi/core": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
@ -595,14 +656,14 @@
}
},
"node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
"integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz",
"integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@tybys/wasm-util": "^0.10.1"
"@tybys/wasm-util": "^0.10.2"
},
"funding": {
"type": "github",
@ -722,6 +783,9 @@
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@ -739,6 +803,9 @@
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@ -756,6 +823,9 @@
"ppc64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@ -773,6 +843,9 @@
"s390x"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@ -790,6 +863,9 @@
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@ -807,6 +883,9 @@
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@ -1296,17 +1375,48 @@
"undici-types": "~7.18.0"
}
},
"node_modules/@vitest/coverage-v8": {
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.9.tgz",
"integrity": "sha512-G9/lgqibheLVBDRuya45EbsEXTYcWoSG+TLg7i2axuzx0Eq62eXn+aWXyaVdV5vKvFSWd6ywcX8hA7la9Pvu8g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@bcoe/v8-coverage": "^1.0.2",
"@vitest/utils": "4.1.9",
"ast-v8-to-istanbul": "^1.0.0",
"istanbul-lib-coverage": "^3.2.2",
"istanbul-lib-report": "^3.0.1",
"istanbul-reports": "^3.2.0",
"magicast": "^0.5.2",
"obug": "^2.1.1",
"std-env": "^4.0.0-rc.1",
"tinyrainbow": "^3.1.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"@vitest/browser": "4.1.9",
"vitest": "4.1.9"
},
"peerDependenciesMeta": {
"@vitest/browser": {
"optional": true
}
}
},
"node_modules/@vitest/expect": {
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.8.tgz",
"integrity": "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==",
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz",
"integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.1.0",
"@types/chai": "^5.2.2",
"@vitest/spy": "4.1.8",
"@vitest/utils": "4.1.8",
"@vitest/spy": "4.1.9",
"@vitest/utils": "4.1.9",
"chai": "^6.2.2",
"tinyrainbow": "^3.1.0"
},
@ -1315,13 +1425,13 @@
}
},
"node_modules/@vitest/mocker": {
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.8.tgz",
"integrity": "sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==",
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz",
"integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/spy": "4.1.8",
"@vitest/spy": "4.1.9",
"estree-walker": "^3.0.3",
"magic-string": "^0.30.21"
},
@ -1342,9 +1452,9 @@
}
},
"node_modules/@vitest/pretty-format": {
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.8.tgz",
"integrity": "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==",
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz",
"integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -1355,13 +1465,13 @@
}
},
"node_modules/@vitest/runner": {
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.8.tgz",
"integrity": "sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==",
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz",
"integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/utils": "4.1.8",
"@vitest/utils": "4.1.9",
"pathe": "^2.0.3"
},
"funding": {
@ -1369,14 +1479,14 @@
}
},
"node_modules/@vitest/snapshot": {
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.8.tgz",
"integrity": "sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==",
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz",
"integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "4.1.8",
"@vitest/utils": "4.1.8",
"@vitest/pretty-format": "4.1.9",
"@vitest/utils": "4.1.9",
"magic-string": "^0.30.21",
"pathe": "^2.0.3"
},
@ -1385,9 +1495,9 @@
}
},
"node_modules/@vitest/spy": {
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.8.tgz",
"integrity": "sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==",
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz",
"integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==",
"dev": true,
"license": "MIT",
"funding": {
@ -1395,13 +1505,13 @@
}
},
"node_modules/@vitest/utils": {
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.8.tgz",
"integrity": "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==",
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz",
"integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "4.1.8",
"@vitest/pretty-format": "4.1.9",
"convert-source-map": "^2.0.0",
"tinyrainbow": "^3.1.0"
},
@ -1485,6 +1595,18 @@
"node": ">=12"
}
},
"node_modules/ast-v8-to-istanbul": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.4.tgz",
"integrity": "sha512-0bC0/4bTSrnwdhU3IsZDwEdojvuPrSg59OYZfKsLRtJZ0u8VBx9DebfqqG8bRdCC0I7vjgxmPi41P0lpkhJHtA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.31",
"estree-walker": "^3.0.3",
"js-tokens": "^10.0.0"
}
},
"node_modules/atomic-sleep": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
@ -2147,6 +2269,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
@ -2180,6 +2312,13 @@
"node": ">=16.9.0"
}
},
"node_modules/html-escaper": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
"dev": true,
"license": "MIT"
},
"node_modules/http-errors": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
@ -2252,6 +2391,45 @@
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"license": "ISC"
},
"node_modules/istanbul-lib-coverage": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
"integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=8"
}
},
"node_modules/istanbul-lib-report": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
"integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"istanbul-lib-coverage": "^3.0.0",
"make-dir": "^4.0.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/istanbul-reports": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
"integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"html-escaper": "^2.0.0",
"istanbul-lib-report": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/jose": {
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz",
@ -2271,6 +2449,13 @@
"node": ">=10"
}
},
"node_modules/js-tokens": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz",
"integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==",
"dev": true,
"license": "MIT"
},
"node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
@ -2426,6 +2611,9 @@
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@ -2447,6 +2635,9 @@
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@ -2468,6 +2659,9 @@
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@ -2489,6 +2683,9 @@
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@ -2584,6 +2781,34 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"node_modules/magicast": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.3.tgz",
"integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.29.3",
"@babel/types": "^7.29.0",
"source-map-js": "^1.2.1"
}
},
"node_modules/make-dir": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
"integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
"dev": true,
"license": "MIT",
"dependencies": {
"semver": "^7.5.3"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@ -3184,6 +3409,19 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/semver": {
"version": "7.8.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/send": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
@ -3444,6 +3682,19 @@
"node": ">= 6"
}
},
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/thenify": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
@ -3760,19 +4011,19 @@
}
},
"node_modules/vitest": {
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.8.tgz",
"integrity": "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==",
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz",
"integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/expect": "4.1.8",
"@vitest/mocker": "4.1.8",
"@vitest/pretty-format": "4.1.8",
"@vitest/runner": "4.1.8",
"@vitest/snapshot": "4.1.8",
"@vitest/spy": "4.1.8",
"@vitest/utils": "4.1.8",
"@vitest/expect": "4.1.9",
"@vitest/mocker": "4.1.9",
"@vitest/pretty-format": "4.1.9",
"@vitest/runner": "4.1.9",
"@vitest/snapshot": "4.1.9",
"@vitest/spy": "4.1.9",
"@vitest/utils": "4.1.9",
"es-module-lexer": "^2.0.0",
"expect-type": "^1.3.0",
"magic-string": "^0.30.21",
@ -3800,12 +4051,12 @@
"@edge-runtime/vm": "*",
"@opentelemetry/api": "^1.9.0",
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
"@vitest/browser-playwright": "4.1.8",
"@vitest/browser-preview": "4.1.8",
"@vitest/browser-webdriverio": "4.1.8",
"@vitest/coverage-istanbul": "4.1.8",
"@vitest/coverage-v8": "4.1.8",
"@vitest/ui": "4.1.8",
"@vitest/browser-playwright": "4.1.9",
"@vitest/browser-preview": "4.1.9",
"@vitest/browser-webdriverio": "4.1.9",
"@vitest/coverage-istanbul": "4.1.9",
"@vitest/coverage-v8": "4.1.9",
"@vitest/ui": "4.1.9",
"happy-dom": "*",
"jsdom": "*",
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"

View file

@ -20,6 +20,7 @@
"pretest": "npm run test-servers:build && npm run build",
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "npm run test-servers:build && npm run build && vitest run --coverage",
"test-servers:build": "tsc -p ../../test-servers --noCheck",
"test:cli": "vitest run cli.test.ts",
"test:cli-tools": "vitest run tools.test.ts",
@ -37,6 +38,7 @@
},
"devDependencies": {
"@types/node": "^24.12.4",
"@vitest/coverage-v8": "^4.1.0",
"tsup": "^8.5.0",
"typescript": "~5.9.3",
"vitest": "^4.1.0"

View file

@ -2,7 +2,6 @@ import { dirname, join } from "path";
import { fileURLToPath, pathToFileURL } from "url";
import { Command } from "commander";
type McpResponse = Record<string, unknown>;
import { handleError } from "./error-handler.js";
import { awaitableLog } from "./utils/awaitable-log.js";
import type {
InspectorServerSettings,
@ -242,6 +241,17 @@ function parseArgs(argv?: string[]): {
methodArgs: MethodArgs & { method: string };
} {
const program = new Command();
// On a parse/usage ERROR (exitCode !== 0), throw the CommanderError instead
// of letting commander call process.exit(). The binary entry (index.ts) still
// routes any thrown error through handleError → process.exit, so external
// behavior is unchanged — but in-process callers (the test harness in
// __tests__/helpers/cli-runner.ts) can now catch the error instead of having
// commander tear down the whole test worker. For --help / --version
// (exitCode 0) we return without throwing, so commander falls through to its
// normal clean process.exit(0) after printing — preserving that UX. See #1484.
program.exitOverride((err) => {
if (err.exitCode !== 0) throw err;
});
const rawArgs = argv ?? process.argv;
const scriptArgs = rawArgs.slice(2);
const dashDashIndex = scriptArgs.indexOf("--");
@ -442,16 +452,3 @@ export async function runCli(argv?: string[]): Promise<void> {
);
await callMethod(serverConfig, serverSettings, methodArgs);
}
export async function main(): Promise<void> {
process.on("uncaughtException", (error) => {
handleError(error);
});
try {
await runCli();
process.exit(0);
} catch (error) {
handleError(error);
}
}

View file

@ -13,5 +13,32 @@ export default defineConfig({
environment: 'node',
include: ['__tests__/**/*.test.ts'],
testTimeout: 15000,
// The in-process runner (__tests__/helpers/cli-runner.ts) patches
// process.std{out,err}.write to capture CLI output. Test files run in
// separate forked processes (and tests within a file run sequentially), so
// those global patches never overlap. `forks` is vitest's default, but pin
// it explicitly so the capture isolation can't regress to a shared-thread
// pool. See #1484.
pool: 'forks',
coverage: {
provider: 'v8',
reporter: ['text', 'html', 'json-summary'],
include: ['src/**/*.ts'],
exclude: [
// Binary bootstrap: shebang + `isMain` guard + `runCli()`/`process.exit`
// wiring that only runs when launched as the real binary. Exercised by
// the out-of-process layer (__tests__/e2e.test.ts + scripts/smoke-cli.mjs),
// which spawns build/index.js and so can't be measured under in-process
// coverage. Mirrors web's `**/index.{ts,tsx}` exclusion.
'src/index.ts',
],
thresholds: {
perFile: true,
lines: 90,
statements: 85,
functions: 80,
branches: 50,
},
},
},
});

View file

@ -58,3 +58,17 @@ The TUI provides terminal-native tabs and panes for interacting with your MCP se
- Use the **Arrow Keys** (Up/Down) to scroll through lists of items.
- Press **Enter** to select an item, execute a tool, or fetch a resource.
- Press **Escape** or `Ctrl+C` to exit the application.
## Development
Run the test suite from `clients/tui/`:
```bash
npm test # run all tests
npm run test:coverage # run tests under the per-file coverage gate
```
Tests live in `__tests__/`. The coverage gate currently covers the TUI's
non-React logic (server resolution, logger, tab metadata, and the `utils/`
form/URL helpers); the Ink components and `App.tsx` are an interim exclusion in
`vitest.config.ts` pending a renderer-based follow-up.

View file

@ -0,0 +1,69 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { existsSync, mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
/**
* Wait for pino's async file destination to actually create the log file.
* `pino.destination({ mkdir: true })` opens the stream asynchronously, so the
* file (and any parent dir) appears a tick later. Awaiting this before cleanup
* also guarantees the background mkdir/open has finished, so `afterEach`'s
* rmSync can't race it into an ENOENT.
*/
async function waitForFile(filePath: string, timeoutMs = 2000): Promise<void> {
const start = Date.now();
while (!existsSync(filePath)) {
if (Date.now() - start > timeoutMs) {
throw new Error(`Timed out waiting for ${filePath}`);
}
await new Promise((resolve) => setTimeout(resolve, 20));
}
}
/**
* `getTuiLogger` lazily builds a pino singleton that writes to a file. Each
* test gets a fresh module (vi.resetModules) and a temp log dir so the two
* directory-resolution branches can both be exercised without touching the
* real ~/.mcp-inspector.
*/
describe("getTuiLogger", () => {
const envKeys = ["MCP_INSPECTOR_LOG_DIR", "HOME", "USERPROFILE", "LOG_LEVEL"];
let saved: Record<string, string | undefined>;
let tempDir: string;
beforeEach(() => {
saved = Object.fromEntries(envKeys.map((k) => [k, process.env[k]]));
tempDir = mkdtempSync(join(tmpdir(), "tui-logger-test-"));
vi.resetModules();
});
afterEach(() => {
for (const key of envKeys) {
if (saved[key] === undefined) delete process.env[key];
else process.env[key] = saved[key];
}
rmSync(tempDir, { recursive: true, force: true });
});
it("writes to MCP_INSPECTOR_LOG_DIR and returns a cached singleton", async () => {
process.env.MCP_INSPECTOR_LOG_DIR = tempDir;
const { getTuiLogger } = await import("../src/logger.js");
const first = getTuiLogger();
const second = getTuiLogger();
expect(first).toBe(second);
await waitForFile(join(tempDir, "auth.log"));
});
it("falls back to HOME/.mcp-inspector when MCP_INSPECTOR_LOG_DIR is unset", async () => {
delete process.env.MCP_INSPECTOR_LOG_DIR;
process.env.HOME = tempDir;
const { getTuiLogger } = await import("../src/logger.js");
const logger = getTuiLogger();
expect(logger).toBeDefined();
await waitForFile(join(tempDir, ".mcp-inspector", "auth.log"));
});
});

View file

@ -0,0 +1,27 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import open from "open";
import { openUrl } from "../src/utils/openUrl.js";
// `open` shells out to the OS browser opener — stub it so the test only checks
// that openUrl forwards the right string.
vi.mock("open", () => ({ default: vi.fn().mockResolvedValue(undefined) }));
const openMock = vi.mocked(open);
describe("openUrl", () => {
beforeEach(() => {
openMock.mockClear();
});
it("passes a string URL straight through", async () => {
await openUrl("https://example.com/auth");
expect(openMock).toHaveBeenCalledWith("https://example.com/auth");
});
it("serializes a URL object via .href", async () => {
await openUrl(new URL("https://example.com/callback?code=1"));
expect(openMock).toHaveBeenCalledWith(
"https://example.com/callback?code=1",
);
});
});

View file

@ -0,0 +1,31 @@
import { describe, it, expect } from "vitest";
import { promptArgsToForm } from "../src/utils/promptArgsToForm.js";
describe("promptArgsToForm", () => {
it("returns an empty Parameters section when there are no arguments", () => {
const form = promptArgsToForm([], "noArgs");
expect(form.title).toBe("Get Prompt: noArgs");
expect(form.sections).toEqual([{ title: "Parameters", fields: [] }]);
});
it("maps prompt arguments to required string fields by default", () => {
const form = promptArgsToForm(
[
{ name: "topic", description: "What to write about" },
{ name: "tone", required: false },
],
"writer",
);
expect(form.sections[0]!.title).toBe("Prompt Arguments");
const [topic, tone] = form.sections[0]!.fields;
expect(topic).toMatchObject({
name: "topic",
type: "string",
required: true,
description: "What to write about",
});
// Only an explicit `required: false` makes a field optional.
expect(tone).toMatchObject({ name: "tone", required: false });
});
});

View file

@ -0,0 +1,92 @@
import { describe, it, expect } from "vitest";
import { schemaToForm } from "../src/utils/schemaToForm.js";
describe("schemaToForm", () => {
it("returns an empty Parameters section when there is no schema", () => {
const form = schemaToForm(null, "noParams");
expect(form.title).toBe("Test Tool: noParams");
expect(form.sections).toEqual([{ title: "Parameters", fields: [] }]);
});
it("returns an empty Parameters section when properties are absent", () => {
const form = schemaToForm({}, "empty");
expect(form.sections[0]?.fields).toEqual([]);
});
it("maps each JSON Schema type to the matching ink-form field type", () => {
const form = schemaToForm(
{
properties: {
name: { type: "string", title: "Name" },
age: { type: "integer", minimum: 0, maximum: 120 },
ratio: { type: "number", minimum: 0, maximum: 1 },
active: { type: "boolean" },
mystery: { type: "object" },
},
required: ["name"],
},
"typed",
);
const fields = form.sections[0]!.fields;
const byName = Object.fromEntries(fields.map((f) => [f.name, f]));
expect(byName.name).toMatchObject({
type: "string",
label: "Name",
required: true,
});
expect(byName.age).toMatchObject({ type: "integer", min: 0, max: 120 });
expect(byName.ratio).toMatchObject({ type: "float", min: 0, max: 1 });
expect(byName.active).toMatchObject({ type: "boolean" });
// Unknown types fall back to string, and label falls back to the key.
expect(byName.mystery).toMatchObject({ type: "string", label: "mystery" });
});
it("builds a select field from an enum", () => {
const form = schemaToForm(
{ properties: { color: { type: "string", enum: ["red", "blue"] } } },
"enum",
);
expect(form.sections[0]!.fields[0]).toMatchObject({
type: "select",
options: [
{ label: "red", value: "red" },
{ label: "blue", value: "blue" },
],
});
});
it("builds a select field from an array-of-enum", () => {
const form = schemaToForm(
{
properties: {
// The array branch is nested under the outer `enum` guard, so a
// top-level `enum` must also be present for it to be reached; the
// options are taken from `items.enum`.
tags: {
type: "array",
enum: ["a", "b"],
items: { enum: ["a", "b"] },
},
},
},
"arrayEnum",
);
expect(form.sections[0]!.fields[0]).toMatchObject({
type: "select",
options: [
{ label: "a", value: "a" },
{ label: "b", value: "b" },
],
});
});
it("carries a JSON Schema default through as the field's initialValue", () => {
const form = schemaToForm(
{ properties: { greeting: { type: "string", default: "hi" } } },
"withDefault",
);
expect(form.sections[0]!.fields[0]).toMatchObject({ initialValue: "hi" });
});
});

View file

@ -0,0 +1,35 @@
import { describe, it, expect, vi, afterEach } from "vitest";
import { uriTemplateToForm } from "../src/utils/uriTemplateToForm.js";
describe("uriTemplateToForm", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("creates a field for each template variable", () => {
const form = uriTemplateToForm("file:///{path}/{name}", "file");
expect(form.title).toBe("Read Resource: file");
const fields = form.sections[0]!.fields;
expect(fields.map((f) => f.name)).toEqual(["path", "name"]);
expect(fields[0]).toMatchObject({ type: "string", required: false });
});
it("returns an empty Template Variables section for a static URI", () => {
const form = uriTemplateToForm("file:///static", "static");
expect(form.sections[0]).toEqual({
title: "Template Variables",
fields: [],
});
});
it("logs and returns an empty form when the template cannot be parsed", () => {
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
const form = uriTemplateToForm("file:///{unclosed", "broken");
expect(errorSpy).toHaveBeenCalledWith(
"Failed to parse URI template:",
expect.any(Error),
);
expect(form.sections[0]!.fields).toEqual([]);
});
});

View file

@ -28,6 +28,7 @@
"devDependencies": {
"@types/node": "^24.12.4",
"@types/react": "^19.2.14",
"@vitest/coverage-v8": "^4.1.0",
"tsup": "^8.5.0",
"tsx": "^4.21.0",
"typescript": "~5.9.3",
@ -49,6 +50,66 @@
"node": ">=18"
}
},
"node_modules/@babel/helper-string-parser": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
"integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
"integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
"integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.29.7"
},
"bin": {
"parser": "bin/babel-parser.js"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@babel/types": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
"integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.29.7",
"@babel/helper-validator-identifier": "^7.29.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@bcoe/v8-coverage": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz",
"integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@emnapi/core": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
@ -1328,17 +1389,48 @@
"csstype": "^3.2.2"
}
},
"node_modules/@vitest/coverage-v8": {
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.9.tgz",
"integrity": "sha512-G9/lgqibheLVBDRuya45EbsEXTYcWoSG+TLg7i2axuzx0Eq62eXn+aWXyaVdV5vKvFSWd6ywcX8hA7la9Pvu8g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@bcoe/v8-coverage": "^1.0.2",
"@vitest/utils": "4.1.9",
"ast-v8-to-istanbul": "^1.0.0",
"istanbul-lib-coverage": "^3.2.2",
"istanbul-lib-report": "^3.0.1",
"istanbul-reports": "^3.2.0",
"magicast": "^0.5.2",
"obug": "^2.1.1",
"std-env": "^4.0.0-rc.1",
"tinyrainbow": "^3.1.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"@vitest/browser": "4.1.9",
"vitest": "4.1.9"
},
"peerDependenciesMeta": {
"@vitest/browser": {
"optional": true
}
}
},
"node_modules/@vitest/expect": {
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.8.tgz",
"integrity": "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==",
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz",
"integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.1.0",
"@types/chai": "^5.2.2",
"@vitest/spy": "4.1.8",
"@vitest/utils": "4.1.8",
"@vitest/spy": "4.1.9",
"@vitest/utils": "4.1.9",
"chai": "^6.2.2",
"tinyrainbow": "^3.1.0"
},
@ -1347,13 +1439,13 @@
}
},
"node_modules/@vitest/mocker": {
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.8.tgz",
"integrity": "sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==",
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz",
"integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/spy": "4.1.8",
"@vitest/spy": "4.1.9",
"estree-walker": "^3.0.3",
"magic-string": "^0.30.21"
},
@ -1374,9 +1466,9 @@
}
},
"node_modules/@vitest/pretty-format": {
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.8.tgz",
"integrity": "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==",
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz",
"integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -1387,13 +1479,13 @@
}
},
"node_modules/@vitest/runner": {
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.8.tgz",
"integrity": "sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==",
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz",
"integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/utils": "4.1.8",
"@vitest/utils": "4.1.9",
"pathe": "^2.0.3"
},
"funding": {
@ -1401,14 +1493,14 @@
}
},
"node_modules/@vitest/snapshot": {
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.8.tgz",
"integrity": "sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==",
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz",
"integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "4.1.8",
"@vitest/utils": "4.1.8",
"@vitest/pretty-format": "4.1.9",
"@vitest/utils": "4.1.9",
"magic-string": "^0.30.21",
"pathe": "^2.0.3"
},
@ -1417,9 +1509,9 @@
}
},
"node_modules/@vitest/spy": {
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.8.tgz",
"integrity": "sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==",
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz",
"integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==",
"dev": true,
"license": "MIT",
"funding": {
@ -1427,13 +1519,13 @@
}
},
"node_modules/@vitest/utils": {
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.8.tgz",
"integrity": "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==",
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz",
"integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "4.1.8",
"@vitest/pretty-format": "4.1.9",
"convert-source-map": "^2.0.0",
"tinyrainbow": "^3.1.0"
},
@ -1556,6 +1648,18 @@
"node": ">=12"
}
},
"node_modules/ast-v8-to-istanbul": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.4.tgz",
"integrity": "sha512-0bC0/4bTSrnwdhU3IsZDwEdojvuPrSg59OYZfKsLRtJZ0u8VBx9DebfqqG8bRdCC0I7vjgxmPi41P0lpkhJHtA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.31",
"estree-walker": "^3.0.3",
"js-tokens": "^10.0.0"
}
},
"node_modules/atomic-sleep": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
@ -2432,6 +2536,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
@ -2465,6 +2579,13 @@
"node": ">=16.9.0"
}
},
"node_modules/html-escaper": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
"dev": true,
"license": "MIT"
},
"node_modules/http-errors": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
@ -2758,6 +2879,45 @@
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"license": "ISC"
},
"node_modules/istanbul-lib-coverage": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
"integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=8"
}
},
"node_modules/istanbul-lib-report": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
"integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"istanbul-lib-coverage": "^3.0.0",
"make-dir": "^4.0.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/istanbul-reports": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
"integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"html-escaper": "^2.0.0",
"istanbul-lib-report": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/jose": {
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz",
@ -2777,6 +2937,13 @@
"node": ">=10"
}
},
"node_modules/js-tokens": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz",
"integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==",
"dev": true,
"license": "MIT"
},
"node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
@ -3090,6 +3257,34 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"node_modules/magicast": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.3.tgz",
"integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.29.3",
"@babel/types": "^7.29.0",
"source-map-js": "^1.2.1"
}
},
"node_modules/make-dir": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
"integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
"dev": true,
"license": "MIT",
"dependencies": {
"semver": "^7.5.3"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@ -3799,6 +3994,19 @@
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
"license": "MIT"
},
"node_modules/semver": {
"version": "7.8.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/send": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
@ -4124,6 +4332,19 @@
"node": ">= 6"
}
},
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/tagged-tag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz",
@ -5027,19 +5248,19 @@
}
},
"node_modules/vitest": {
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.8.tgz",
"integrity": "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==",
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz",
"integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/expect": "4.1.8",
"@vitest/mocker": "4.1.8",
"@vitest/pretty-format": "4.1.8",
"@vitest/runner": "4.1.8",
"@vitest/snapshot": "4.1.8",
"@vitest/spy": "4.1.8",
"@vitest/utils": "4.1.8",
"@vitest/expect": "4.1.9",
"@vitest/mocker": "4.1.9",
"@vitest/pretty-format": "4.1.9",
"@vitest/runner": "4.1.9",
"@vitest/snapshot": "4.1.9",
"@vitest/spy": "4.1.9",
"@vitest/utils": "4.1.9",
"es-module-lexer": "^2.0.0",
"expect-type": "^1.3.0",
"magic-string": "^0.30.21",
@ -5067,12 +5288,12 @@
"@edge-runtime/vm": "*",
"@opentelemetry/api": "^1.9.0",
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
"@vitest/browser-playwright": "4.1.8",
"@vitest/browser-preview": "4.1.8",
"@vitest/browser-webdriverio": "4.1.8",
"@vitest/coverage-istanbul": "4.1.8",
"@vitest/coverage-v8": "4.1.8",
"@vitest/ui": "4.1.8",
"@vitest/browser-playwright": "4.1.9",
"@vitest/browser-preview": "4.1.9",
"@vitest/browser-webdriverio": "4.1.9",
"@vitest/coverage-istanbul": "4.1.9",
"@vitest/coverage-v8": "4.1.9",
"@vitest/ui": "4.1.9",
"happy-dom": "*",
"jsdom": "*",
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"

Some files were not shown because too many files have changed in this diff Show more