import { describe, it, expect, beforeAll } from "vitest"; import { spawnSync } from "node:child_process"; import { existsSync } from "node:fs"; import { resolve } from "node:path"; const cliPath = resolve(__dirname, "..", "dist", "cli.js"); function runCli( args: string[], opts: { env?: Record; stdin?: string } = {}, ): { status: number; stdout: string; stderr: string } { const r = spawnSync(process.execPath, [cliPath, ...args], { encoding: "utf8", env: { ...process.env, ...(opts.env ?? {}) }, input: opts.stdin, }); return { status: r.status ?? -1, stdout: typeof r.stdout === "string" ? r.stdout : "", stderr: typeof r.stderr === "string" ? r.stderr : "", }; } const HOOK_STDIN = JSON.stringify({ session_id: "abc12345-de67-89f0-1234-567890abcdef", hook_event_name: "UserPromptSubmit", cwd: "/tmp", prompt: "test", }); describe("`check --hook` CLI behavior", () => { beforeAll(() => { if (!existsSync(cliPath)) { throw new Error(`CLI not built. Run \`npm run build\` first. Missing: ${cliPath}`); } }); it("exits 0 silently when no stdin, no --name, no env", () => { const r = runCli(["check", "--hook"], { env: { CLAUDE_MAILBOX_NAME: undefined } }); expect(r.status).toBe(0); expect(r.stdout).toBe(""); expect(r.stderr).toBe(""); }); it("derives session-id-based name from stdin and emits daemon hint when down", () => { const r = runCli(["check", "--hook", "--url", "http://127.0.0.1:1"], { env: { CLAUDE_MAILBOX_NAME: undefined }, stdin: HOOK_STDIN, }); expect(r.status).toBe(0); expect(r.stdout).toContain("[Claude-Mailbox] Daemon not reachable"); }); it("uses base prefix from CLAUDE_MAILBOX_NAME when both env and stdin present", () => { // We can't directly assert the name from --hook output (it's only in the unreachable hint URL). // The hint always contains the URL we passed, so this just confirms the path runs without error. const r = runCli(["check", "--hook", "--url", "http://127.0.0.1:1"], { env: { CLAUDE_MAILBOX_NAME: "backend" }, stdin: HOOK_STDIN, }); expect(r.status).toBe(0); expect(r.stdout).toContain("[Claude-Mailbox] Daemon not reachable"); }); it("explicit --name overrides session-id derivation", () => { const r = runCli( ["check", "--hook", "--name", "explicit", "--url", "http://127.0.0.1:1"], { env: { CLAUDE_MAILBOX_NAME: "ignored" }, stdin: HOOK_STDIN }, ); expect(r.status).toBe(0); expect(r.stdout).toContain("[Claude-Mailbox] Daemon not reachable"); }); it("non-hook mode errors out when no name resolved", () => { const r = runCli(["check"], { env: { CLAUDE_MAILBOX_NAME: undefined } }); expect(r.status).not.toBe(0); expect(r.stderr).toContain("CLAUDE_MAILBOX_NAME"); }); }); describe("`session-announce` CLI behavior", () => { beforeAll(() => { if (!existsSync(cliPath)) { throw new Error(`CLI not built. Run \`npm run build\` first. Missing: ${cliPath}`); } }); it("prints the derived mailbox name from a SessionStart payload", () => { const r = runCli(["session-announce"], { env: { CLAUDE_MAILBOX_NAME: undefined }, stdin: HOOK_STDIN, }); expect(r.status).toBe(0); expect(r.stdout).toContain("`claude-abc12345`"); expect(r.stdout).toContain("mcp__mailbox__send"); expect(r.stdout).toContain(`from="claude-abc12345"`); }); it("uses base prefix when set", () => { const r = runCli(["session-announce"], { env: { CLAUDE_MAILBOX_NAME: "backend" }, stdin: HOOK_STDIN, }); expect(r.status).toBe(0); expect(r.stdout).toContain("`backend-abc12345`"); }); it("stays silent when no session_id in stdin", () => { const r = runCli(["session-announce"], { env: { CLAUDE_MAILBOX_NAME: undefined }, stdin: JSON.stringify({ hook_event_name: "SessionStart" }), }); expect(r.status).toBe(0); expect(r.stdout).toBe(""); }); it("stays silent when no stdin at all", () => { const r = runCli(["session-announce"], { env: { CLAUDE_MAILBOX_NAME: undefined } }); expect(r.status).toBe(0); expect(r.stdout).toBe(""); }); });