feat(hook): close mailbox on SessionEnd
New SessionEnd plugin hook runs `claude-mailbox session-end`, which derives the session's auto-name from stdin and asks the daemon to delete the mailbox if it has no pending messages either direction. Renamed mailboxes are preserved (the auto-name no longer exists, so DELETE is a no-op). The daemon-side `SKIP_UPSERT_PATHS` prevents the request from auto-recreating the mailbox. Sweeper remains the safety net for sessions that exit ungracefully. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -210,4 +210,43 @@ describe("REST surface", () => {
|
||||
const peek = await call("GET", "/v1/peek?name=bob");
|
||||
expect(peek.status).toBe(200);
|
||||
});
|
||||
|
||||
it("POST /v1/session-end deletes an empty mailbox and does not auto-recreate it", async () => {
|
||||
await call("POST", "/v1/send", {
|
||||
headers: { "X-Mailbox": "alice" },
|
||||
body: { to: "bob", body: "x" },
|
||||
});
|
||||
await call("POST", "/v1/check-inbox?name=bob", { headers: { "X-Mailbox": "bob" } });
|
||||
|
||||
const r = await call("POST", "/v1/session-end", { headers: { "X-Mailbox": "bob" } });
|
||||
expect(r.status).toBe(200);
|
||||
expect(r.body).toMatchObject({ name: "bob", deleted: true, reason: "deleted" });
|
||||
|
||||
const list = await call("GET", "/v1/list");
|
||||
const names = (list.body as Array<{ name: string }>).map((m) => m.name);
|
||||
expect(names).not.toContain("bob");
|
||||
});
|
||||
|
||||
it("POST /v1/session-end refuses to delete a mailbox with pending messages", async () => {
|
||||
await call("POST", "/v1/send", {
|
||||
headers: { "X-Mailbox": "alice" },
|
||||
body: { to: "bob", body: "still pending" },
|
||||
});
|
||||
const r = await call("POST", "/v1/session-end", { headers: { "X-Mailbox": "bob" } });
|
||||
expect(r.status).toBe(200);
|
||||
expect(r.body).toMatchObject({ name: "bob", deleted: false, reason: "has-pending" });
|
||||
|
||||
const peek = await call("GET", "/v1/peek?name=bob");
|
||||
expect(peek.body).toMatchObject({ pending: 1 });
|
||||
});
|
||||
|
||||
it("POST /v1/session-end is a no-op for an unknown (e.g. renamed) name", async () => {
|
||||
const r = await call("POST", "/v1/session-end", { headers: { "X-Mailbox": "renamed-away" } });
|
||||
expect(r.status).toBe(200);
|
||||
expect(r.body).toMatchObject({ name: "renamed-away", deleted: false, reason: "not-found" });
|
||||
|
||||
const list = await call("GET", "/v1/list");
|
||||
const names = (list.body as Array<{ name: string }>).map((m) => m.name);
|
||||
expect(names).not.toContain("renamed-away");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user