From 11d76287d77f5e9ec86a545f1c60df2b80129cfa Mon Sep 17 00:00:00 2001 From: Dejan Strbac Date: Sat, 12 Sep 2026 13:12:59 +0200 Subject: [PATCH] cli: hint on the four pickup refusals run_cooldown quotes retryAfter, pickup_limit_reached quotes limit, and source_is_self and duplicate_source say what the code means. One test per hint, plus the number-less fallbacks for a body without the field. Co-Authored-By: Claude Fable 5.1 --- internal/cli/errorhint_test.go | 25 +++++++++++++++++++++++++ internal/cli/root.go | 24 ++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/internal/cli/errorhint_test.go b/internal/cli/errorhint_test.go index d5619d7..fa90b2e 100644 --- a/internal/cli/errorhint_test.go +++ b/internal/cli/errorhint_test.go @@ -97,6 +97,31 @@ func TestErrorHint(t *testing.T) { ae: &coreapi.APIError{Status: 400, Code: "destination_loops", Extra: map[string]any{"target": "a@x.test"}}, want: "routes back here", }, + { + name: "run_cooldown with retryAfter seconds", + ae: &coreapi.APIError{Status: 429, Code: "run_cooldown", Extra: map[string]any{"retryAfter": float64(45)}}, + want: "wait 45 seconds before fetching again", + }, + { + name: "run_cooldown without retryAfter still hints", + ae: &coreapi.APIError{Status: 429, Code: "run_cooldown"}, + want: "maximum once per minute", + }, + { + name: "pickup_limit_reached with limit", + ae: &coreapi.APIError{Status: 409, Code: "pickup_limit_reached", Extra: map[string]any{"limit": float64(10)}}, + want: "reached the limit of 10 pickup sources", + }, + { + name: "source_is_self explains self-fetch prohibition", + ae: &coreapi.APIError{Status: 400, Code: "source_is_self"}, + want: "cannot fetch from this mailbox itself", + }, + { + name: "duplicate_source names existing source", + ae: &coreapi.APIError{Status: 409, Code: "duplicate_source"}, + want: "already exists on this mailbox", + }, { name: "unrelated code gets no hint", ae: &coreapi.APIError{Status: 400, Code: "validation_failed"}, diff --git a/internal/cli/root.go b/internal/cli/root.go index ccc9e02..989cda1 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -280,6 +280,30 @@ func errorHint(ae *coreapi.APIError) string { return fmt.Sprintf("%s reaches more than one recipient here (a group, an alias, a webhook, or a route that forwards on) — nobody there can answer the code, and the disable link would stop your forwarding for whoever clicked it first; name one person's address", tgt) } return "that address reaches more than one recipient here (a group, an alias, a webhook, or a route that forwards on) — nobody there can answer the code, and the disable link would stop your forwarding for whoever clicked it first; name one person's address" + case "run_cooldown": + if ra, ok := ae.Extra["retryAfter"]; ok { + switch v := ra.(type) { + case float64: + return fmt.Sprintf("pickup is on cooldown (maximum once per minute) — please wait %d seconds before fetching again", int(v)) + case int: + return fmt.Sprintf("pickup is on cooldown (maximum once per minute) — please wait %d seconds before fetching again", v) + } + } + return "pickup is on cooldown (maximum once per minute) — please wait before fetching again" + case "pickup_limit_reached": + if lim, ok := ae.Extra["limit"]; ok { + switch v := lim.(type) { + case float64: + return fmt.Sprintf("this mailbox has reached the limit of %d pickup sources", int(v)) + case int: + return fmt.Sprintf("this mailbox has reached the limit of %d pickup sources", v) + } + } + return "this mailbox has reached the maximum number of pickup sources" + case "source_is_self": + return "cannot fetch from this mailbox itself — pickup is for consolidating mail from other mailboxes or external providers" + case "duplicate_source": + return "a pickup source for this server and username already exists on this mailbox" } return "" }