import { spawn } from "node:child_process"; import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { ENV_AGENT_DIR } from "../src/config.js"; const cliPath = resolve(__dirname, "../src/cli.ts"); const tsxPath = resolve(__dirname, "../../../node_modules/tsx/dist/cli.mjs"); const tempDirs: string[] = []; afterEach(() => { for (const dir of tempDirs.splice(0)) { rmSync(dir, { recursive: true, force: true }); } }); function createTempDir(): string { const dir = mkdtempSync(join(tmpdir(), "pi-stdout-clean-")); tempDirs.push(dir); return dir; } async function runCli(args: string[]): Promise<{ stdout: string; stderr: string; code: number | null }> { const tempRoot = createTempDir(); const agentDir = join(tempRoot, "agent"); const projectDir = join(tempRoot, "project"); const projectConfigDir = join(projectDir, ".pi"); mkdirSync(agentDir, { recursive: true }); mkdirSync(projectConfigDir, { recursive: true }); const fakeNpmPath = join(tempRoot, "fake-npm.mjs"); writeFileSync( fakeNpmPath, [ 'console.log("changed 1 package in 471ms");', 'console.log("found 0 vulnerabilities");', "process.exit(0);", ].join("\n"), "utf-8", ); writeFileSync( join(projectConfigDir, "settings.json"), JSON.stringify( { packages: ["npm:fake-package"], npmCommand: [process.execPath, fakeNpmPath], }, null, 2, ), "utf-8", ); return await new Promise((resolvePromise, reject) => { const child = spawn(process.execPath, [tsxPath, cliPath, ...args], { cwd: projectDir, env: { ...process.env, [ENV_AGENT_DIR]: agentDir, }, stdio: ["ignore", "pipe", "pipe"], }); let stdout = ""; let stderr = ""; child.stdout.on("data", (chunk) => { stdout += chunk.toString(); }); child.stderr.on("data", (chunk) => { stderr += chunk.toString(); }); child.on("error", reject); child.on("close", (code) => { resolvePromise({ stdout, stderr, code }); }); }); } describe("stdout cleanliness in non-interactive modes", () => { it("keeps stdout empty for --mode json --help while routing startup chatter to stderr", async () => { const result = await runCli(["--mode", "json", "--help"]); expect(result.code).toBe(0); expect(result.stdout).toBe(""); expect(result.stderr).toContain("changed 1 package in 471ms"); expect(result.stderr).toContain("found 0 vulnerabilities"); expect(result.stderr).toContain("Usage:"); }); it("keeps stdout empty for -p --help while routing startup chatter to stderr", async () => { const result = await runCli(["-p", "--help"]); expect(result.code).toBe(0); expect(result.stdout).toBe(""); expect(result.stderr).toContain("changed 1 package in 471ms"); expect(result.stderr).toContain("found 0 vulnerabilities"); expect(result.stderr).toContain("Usage:"); }); });