* 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>
69 lines
2.4 KiB
TypeScript
69 lines
2.4 KiB
TypeScript
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"));
|
|
});
|
|
});
|