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 { 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; 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")); }); it("falls back to USERPROFILE/.mcp-inspector when HOME is unset", async () => { // Exercises the second arm of `HOME || USERPROFILE || "."`: HOME empty so // the chain continues to USERPROFILE (the Windows home var). delete process.env.MCP_INSPECTOR_LOG_DIR; delete process.env.HOME; process.env.USERPROFILE = tempDir; const { getTuiLogger } = await import("../src/logger.js"); const logger = getTuiLogger(); expect(logger).toBeDefined(); await waitForFile(join(tempDir, ".mcp-inspector", "auth.log")); }); it('falls back to "." when neither HOME nor USERPROFILE is set', async () => { // Exercises the final arm of the chain: with both home vars unset, the dir // resolves to "./.mcp-inspector" relative to the process cwd. chdir into the // temp dir so the relative path pino opens lands there (and gets cleaned up). delete process.env.MCP_INSPECTOR_LOG_DIR; delete process.env.HOME; delete process.env.USERPROFILE; const originalCwd = process.cwd(); process.chdir(tempDir); // Re-read cwd after chdir: on macOS tmpdir is a symlink (/var -> /private/var), // so the resolved cwd differs from `tempDir` and the file lands under it. const resolvedCwd = process.cwd(); try { const { getTuiLogger } = await import("../src/logger.js"); const logger = getTuiLogger(); expect(logger).toBeDefined(); // pino resolves the relative "./.mcp-inspector/auth.log" against cwd. await waitForFile(join(resolvedCwd, ".mcp-inspector", "auth.log")); } finally { process.chdir(originalCwd); } }); });