console: region API messages, as approved
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 files changed,
+223
−0
internal/console/regionmsgs.go+96 −0internal/console/regionmsgs_test.go+127 −0
diff --git a/internal/console/regionmsgs.go b/internal/console/regionmsgs.go| new file mode 100644 |
| index 000000000..9b3504436 |
| --- /dev/null |
| +++ b/internal/console/regionmsgs.go |
| @@ -0,0 +1,96 @@ |
| +package console |
| + |
| +import ( |
| + "fmt" |
| + "strings" |
| +) |
| + |
| +// The nearest-region feature's API and operator messages (spec §19), as |
| +// approved in the nearest-region copy record. Change none of them without a |
| +// new copy review. |
| +// |
| +// Where a message takes a list ("Choose from: %s."), the caller passes the |
| +// region codes and the message joins them with ", ". An empty list never |
| +// renders as a list: the b variant drops the list sentence (codex plan-R4 2, |
| +// a list names only what the door would accept, so it can be empty). |
| + |
| +// reasonText turns a refusal reason (absent, not-hosting, no-secrets-key, |
| +// not-eligible) into the words C16 puts in its brackets (R-absent, |
| +// R-not-hosting, R-no-secrets-key, R-not-eligible). An unknown reason passes |
| +// through unchanged. |
| +func reasonText(reason string) string { |
| + switch reason { |
| + case "absent": |
| + return "no box runs there now" |
| + case "not-hosting": |
| + return "boxes there can't start instances" |
| + case "no-secrets-key": |
| + return "boxes there can't open this deployment's secrets" |
| + case "not-eligible": |
| + return "boxes there don't take ordinary instances" |
| + } |
| + return reason |
| +} |
| + |
| +// msgAppRegionUnavailable is C16, or C16b when can is empty. The verbs are |
| +// the app's region code, the reason text and the joined list. |
| +func msgAppRegionUnavailable(code, reason string, can []string) string { |
| + if len(can) == 0 { |
| + return fmt.Sprintf("this app's region %s can't take new instances right now (%s), and no other region this app may use can either. Try again later.", |
| + code, reasonText(reason)) |
| + } |
| + return fmt.Sprintf("this app's region %s can't take new instances right now (%s). Pass a region to place this instance elsewhere. Regions that can: %s.", |
| + code, reasonText(reason), strings.Join(can, ", ")) |
| +} |
| + |
| +// msgAppRegionNotAllowed is C17, or C17b when allowed is empty. |
| +func msgAppRegionNotAllowed(code string, allowed []string) string { |
| + if len(allowed) == 0 { |
| + return fmt.Sprintf("this app's region %s is not one its instances may use, and no region they may use can take new instances right now. Try again later.", code) |
| + } |
| + return fmt.Sprintf("this app's region %s is not one its instances may use. Allowed: %s. Pass a region to choose one.", |
| + code, strings.Join(allowed, ", ")) |
| +} |
| + |
| +// msgRegionUnknown is C18, or C18b when choices is empty. |
| +func msgRegionUnknown(code string, choices []string) string { |
| + if len(choices) == 0 { |
| + return fmt.Sprintf("region %s is not one this platform runs in. Run the command without --region.", code) |
| + } |
| + return fmt.Sprintf("region %s is not one this platform runs in. Choose from: %s.", code, strings.Join(choices, ", ")) |
| +} |
| + |
| +// msgRegionNotArmed is C19, or C19b when choices is empty. |
| +func msgRegionNotArmed(code string, choices []string) string { |
| + if len(choices) == 0 { |
| + return fmt.Sprintf("region %s can't take new apps right now. Run the command without --region.", code) |
| + } |
| + return fmt.Sprintf("region %s can't take new apps right now. Choose from: %s.", code, strings.Join(choices, ", ")) |
| +} |
| + |
| +// msgAppRegionUnreadable is C20. |
| +const msgAppRegionUnreadable = "could not read this app's region. Try again." |
| + |
| +// msgReplicaOverlap is C26. The region code fills both verbs. |
| +func msgReplicaOverlap(code string) string { |
| + return fmt.Sprintf("this app's region %s is also one of this instance's replica placements. Pass a region for the instance, or drop %s from replicas.", code, code) |
| +} |
| + |
| +// msgPolicyUnreadable is C27. |
| +const msgPolicyUnreadable = "could not read this app's instance policy, so there is no region suggestion. Try again." |
| + |
| +// msgAppRegionUnconfirmed is C28. |
| +func msgAppRegionUnconfirmed(code string) string { |
| + return fmt.Sprintf("could not confirm that this app's region %s can take new instances. Try again, or pass a region.", code) |
| +} |
| + |
| +// msgPlacementDiscrepancy is C29, an operator log line. The app name fills |
| +// the second and third verbs. |
| +func msgPlacementDiscrepancy(host, app, code string) string { |
| + return fmt.Sprintf("possible placement discrepancy: %s of %s has no region, but %s's region is %s. It may be a deliberate home override; review it.", host, app, app, code) |
| +} |
| + |
| +// msgNoSecretsKey is C31 (R9). |
| +func msgNoSecretsKey(code string) string { |
| + return fmt.Sprintf("region %s can't run instances right now: its boxes don't hold this deployment's secrets key, so an instance there would start without its config or secrets. Choose another region.", code) |
| +} |
diff --git a/internal/console/regionmsgs_test.go b/internal/console/regionmsgs_test.go| new file mode 100644 |
| index 000000000..0d0709866 |
| --- /dev/null |
| +++ b/internal/console/regionmsgs_test.go |
| @@ -0,0 +1,127 @@ |
| +package console |
| + |
| +import ( |
| + "encoding/json" |
| + "fmt" |
| + "os" |
| + "strconv" |
| + "strings" |
| + "testing" |
| +) |
| + |
| +func TestRegionMessagesRender(t *testing.T) { |
| + all := []string{ |
| + msgAppRegionUnavailable("uk", "absent", []string{"ie", "au"}), |
| + msgAppRegionNotAllowed("uk", []string{"ie"}), |
| + msgRegionUnknown("zz", []string{"ie", "uk"}), |
| + msgRegionNotArmed("uk", []string{"ie"}), |
| + msgAppRegionUnreadable, |
| + msgReplicaOverlap("uk"), |
| + msgPolicyUnreadable, |
| + msgAppRegionUnconfirmed("uk"), |
| + msgPlacementDiscrepancy("a.go.tito.io", "blog", "uk"), |
| + msgNoSecretsKey("uk"), |
| + // codex plan-R4 2: the empty-list variants, C16b to C19b. |
| + msgAppRegionUnavailable("uk", "absent", nil), |
| + msgAppRegionNotAllowed("uk", nil), |
| + msgRegionUnknown("zz", nil), |
| + msgRegionNotArmed("uk", nil), |
| + } |
| + for _, s := range all { |
| + if s == "" || strings.Contains(s, "—") || strings.Contains(s, "%!") { |
| + t.Errorf("bad message %q", s) |
| + } |
| + } |
| + if !strings.Contains(all[0], reasonText("absent")) || !strings.Contains(all[0], "ie, au") { |
| + t.Errorf("C16 = %q, want the readable reason and the list", all[0]) |
| + } |
| + for _, s := range all[len(all)-4:] { |
| + if strings.Contains(s, ": .") || strings.Contains(s, ": ,") { |
| + t.Errorf("an empty list rendered as a list: %q", s) |
| + } |
| + } |
| + seen := map[string]bool{} |
| + for _, r := range []string{"absent", "not-hosting", "no-secrets-key", "not-eligible"} { |
| + txt := reasonText(r) |
| + if txt == r || seen[txt] { |
| + t.Errorf("reasonText(%s) = %q: untranslated or duplicated", r, txt) |
| + } |
| + seen[txt] = true |
| + } |
| +} |
| + |
| +// TestRegionMessagesMatchTheApprovedRecord holds every record entry mapped to |
| +// regionmsgs.go to the source as a quoted literal (so a short text cannot |
| +// pass by matching part of an identifier), and holds each rendered message to |
| +// the record's text filled with the same verbs, in the record's order. |
| +func TestRegionMessagesMatchTheApprovedRecord(t *testing.T) { |
| + raw, err := os.ReadFile("../../docs/superpowers/plans/2026-10-02-nearest-region-copy.json") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + var rec struct { |
| + Entries []struct { |
| + ID string `json:"id"` |
| + File string `json:"file"` |
| + Text string `json:"text"` |
| + } `json:"entries"` |
| + } |
| + if err := json.Unmarshal(raw, &rec); err != nil { |
| + t.Fatal(err) |
| + } |
| + src, err := os.ReadFile("regionmsgs.go") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + byID := map[string]string{} |
| + mapped := 0 |
| + for _, e := range rec.Entries { |
| + byID[e.ID] = e.Text |
| + if e.File != "internal/console/regionmsgs.go" { |
| + continue |
| + } |
| + mapped++ |
| + if !strings.Contains(string(src), strconv.Quote(e.Text)) { |
| + t.Errorf("%s: regionmsgs.go has no literal %s", e.ID, strconv.Quote(e.Text)) |
| + } |
| + } |
| + // The record maps 4 reasons, C16 to C20, C16b to C19b, C26 to C29 and C31. |
| + if mapped != 18 { |
| + t.Errorf("the record maps %d entries to regionmsgs.go, want 18", mapped) |
| + } |
| + list := []string{"ie", "au"} |
| + cases := []struct { |
| + id, got, want string |
| + }{ |
| + {"R-absent", reasonText("absent"), byID["R-absent"]}, |
| + {"R-not-hosting", reasonText("not-hosting"), byID["R-not-hosting"]}, |
| + {"R-no-secrets-key", reasonText("no-secrets-key"), byID["R-no-secrets-key"]}, |
| + {"R-not-eligible", reasonText("not-eligible"), byID["R-not-eligible"]}, |
| + {"C16", msgAppRegionUnavailable("uk", "absent", list), fmt.Sprintf(byID["C16"], "uk", reasonText("absent"), "ie, au")}, |
| + {"C17", msgAppRegionNotAllowed("uk", list), fmt.Sprintf(byID["C17"], "uk", "ie, au")}, |
| + {"C18", msgRegionUnknown("zz", list), fmt.Sprintf(byID["C18"], "zz", "ie, au")}, |
| + {"C19", msgRegionNotArmed("uk", list), fmt.Sprintf(byID["C19"], "uk", "ie, au")}, |
| + {"C16b", msgAppRegionUnavailable("uk", "absent", nil), fmt.Sprintf(byID["C16b"], "uk", reasonText("absent"))}, |
| + {"C17b", msgAppRegionNotAllowed("uk", nil), fmt.Sprintf(byID["C17b"], "uk")}, |
| + {"C18b", msgRegionUnknown("zz", nil), fmt.Sprintf(byID["C18b"], "zz")}, |
| + {"C19b", msgRegionNotArmed("uk", nil), fmt.Sprintf(byID["C19b"], "uk")}, |
| + {"C20", msgAppRegionUnreadable, byID["C20"]}, |
| + {"C26", msgReplicaOverlap("uk"), fmt.Sprintf(byID["C26"], "uk", "uk")}, |
| + {"C27", msgPolicyUnreadable, byID["C27"]}, |
| + {"C28", msgAppRegionUnconfirmed("uk"), fmt.Sprintf(byID["C28"], "uk")}, |
| + {"C29", msgPlacementDiscrepancy("a.go.tito.io", "blog", "uk"), fmt.Sprintf(byID["C29"], "a.go.tito.io", "blog", "blog", "uk")}, |
| + {"C31", msgNoSecretsKey("uk"), fmt.Sprintf(byID["C31"], "uk")}, |
| + } |
| + for _, c := range cases { |
| + if byID[c.id] == "" { |
| + t.Errorf("%s: not in the record", c.id) |
| + continue |
| + } |
| + if c.got != c.want { |
| + t.Errorf("%s = %q, record gives %q", c.id, c.got, c.want) |
| + } |
| + } |
| + if reasonText("nonsense") != "nonsense" { |
| + t.Errorf("an unknown reason should pass through unchanged") |
| + } |
| +} |