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 and no --name", () => { const r = runCli(["check", "--hook"]); 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"], { 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"], { stdin: HOOK_STDIN }, ); expect(r.status).toBe(0); expect(r.stdout).toContain("[Claude-Mailbox] Daemon not reachable"); }); it("uses CLAUDE_MAILBOX_URL env as default base URL when --url is not given", () => { const r = runCli(["check", "--hook"], { env: { CLAUDE_MAILBOX_URL: "http://127.0.0.1:1" }, stdin: HOOK_STDIN, }); expect(r.status).toBe(0); expect(r.stdout).toContain("[Claude-Mailbox] Daemon not reachable at http://127.0.0.1:1"); }); it("non-hook mode errors out when no name resolved", () => { const r = runCli(["check"]); expect(r.status).not.toBe(0); expect(r.stderr).toContain("Missing --name"); }); }); describe("`session-announce` CLI behavior", () => { const UNREACHABLE = "http://127.0.0.1:1"; 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 (project-prefixed)", () => { // cwd "/tmp" is not a git repo → basename "tmp" → project prefix "tmp". const r = runCli(["session-announce", "--url", UNREACHABLE], { stdin: HOOK_STDIN, }); expect(r.status).toBe(0); // The exact prefix depends on the runtime cwd if git resolves; the deterministic // assertion is the session-short suffix and the announcement structure. expect(r.stdout).toMatch(/`[a-z0-9-]+-abc12345`/); expect(r.stdout).toContain("mcp__mailbox__send"); expect(r.stdout).toMatch(/from="[a-z0-9-]+-abc12345"/); }); it("includes a hint about the rename tool", () => { const r = runCli(["session-announce", "--url", UNREACHABLE], { stdin: HOOK_STDIN }); expect(r.status).toBe(0); expect(r.stdout).toContain("mcp__mailbox__rename"); }); it("emits daemon-not-reachable hint when daemon is down", () => { const r = runCli(["session-announce", "--url", UNREACHABLE], { stdin: HOOK_STDIN }); expect(r.status).toBe(0); expect(r.stdout).toContain("[Claude-Mailbox] Daemon not reachable"); }); it("stays silent when no session_id in stdin", () => { const r = runCli(["session-announce", "--url", UNREACHABLE], { 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", "--url", UNREACHABLE]); expect(r.status).toBe(0); expect(r.stdout).toBe(""); }); });