All checks were successful
CI (Node) / build-test (push) Successful in 9s
The plugin's UserPromptSubmit and SessionStart hooks call `claude-mailbox` with no --url flag, so they previously always hit the hardcoded http://127.0.0.1:47822/mcp default. If port 47822 was held by another local service (e.g. ClaudeDo), the daemon couldn't bind there and every hook was talking to the wrong process. CLI default for --url now resolves to $CLAUDE_MAILBOX_URL when set, falling back to http://127.0.0.1:47822. Doctor gained a Step 2 that probes /health on 47822, identifies foreign occupants, picks a free port, writes both ~/.claude-mailbox/mailbox.json and the CLAUDE_MAILBOX_URL entry in .claude/settings.json env so the hooks follow along automatically. Also adds a fallback hint when Windows schtasks /Create fails with Access is denied (Group Policy restricts non-admin task creation): run install-autostart from an elevated shell, or accept an ephemeral serve for the current session.
145 lines
5.0 KiB
TypeScript
145 lines
5.0 KiB
TypeScript
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<string, string | undefined>; 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("uses CLAUDE_MAILBOX_URL env as default base URL when --url is not given", () => {
|
|
const r = runCli(["check", "--hook"], {
|
|
env: { CLAUDE_MAILBOX_NAME: undefined, 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"], { env: { CLAUDE_MAILBOX_NAME: undefined } });
|
|
expect(r.status).not.toBe(0);
|
|
expect(r.stderr).toContain("CLAUDE_MAILBOX_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", () => {
|
|
const r = runCli(["session-announce", "--url", UNREACHABLE], {
|
|
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", "--url", UNREACHABLE], {
|
|
env: { CLAUDE_MAILBOX_NAME: "backend" },
|
|
stdin: HOOK_STDIN,
|
|
});
|
|
expect(r.status).toBe(0);
|
|
expect(r.stdout).toContain("`backend-abc12345`");
|
|
});
|
|
|
|
it("emits daemon-not-reachable hint when daemon is down", () => {
|
|
const r = runCli(["session-announce", "--url", UNREACHABLE], {
|
|
env: { CLAUDE_MAILBOX_NAME: undefined },
|
|
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], {
|
|
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", "--url", UNREACHABLE], {
|
|
env: { CLAUDE_MAILBOX_NAME: undefined },
|
|
});
|
|
expect(r.status).toBe(0);
|
|
expect(r.stdout).toBe("");
|
|
});
|
|
});
|