Files
ClaudeMailbox/node/tests/cli-hook.test.ts
Mika Kuns 48b6ba6452 feat(plugin): SessionStart hook discovers and announces active peers
session-announce now calls /v1/list with the session's X-Mailbox header,
which both registers the session with the daemon and returns all known
mailboxes in one round-trip. The output appends an "Active peers" block
listing mailboxes seen within the last hour (configurable via
--peer-window-minutes), capped at 10 entries by default. Self is
filtered out; the list is sorted most-recent-first.

So when the user says "I started a second session, coordinate with it",
Claude already has the peer's mailbox name in context — no manual
list_mailboxes call needed.

The peer-formatting logic is extracted into formatActivePeerList for
unit testing; CLI tests now pin --url to an unreachable port to keep
assertions stable on machines that have a real daemon running.
2026-05-19 11:59:31 +02:00

136 lines
4.6 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("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("");
});
});