carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package console
2
3import (
4 "context"
5 "encoding/json"
6 "strings"
7 "testing"
8 "time"
9
10 "github.com/carlosframework/platform/internal/metrics"
11 "github.com/carlosframework/platform/internal/release"
12)
13
14// sentMail is one captured sendApp call. The token is captured so a test can
15// assert each operator is mailed with THEIR OWN grant, never a shared one.
16type sentMail struct {
17 token, to, subject, body string
18}
19
20// alertFixture is a console fixture with a captured sendApp. err makes
21// sends fail; failFor narrows the failure to one recipient, which is how
22// the per-recipient retry is pinned.
23type alertFixture struct {
24 *consoleFixture
25 sent []sentMail
26 err error
27 failFor string
28}
29
30// newCapturedFixture wires the capture and nothing else — no operators, so
31// a test can build its own recipient shape.
32func newCapturedFixture(t *testing.T) *alertFixture {
33 t.Helper()
34 fx := newConsoleFixture(t)
35 af := &alertFixture{consoleFixture: fx}
36 fx.srv.sendApp = func(_ context.Context, token, to, subject, body string) (string, error) {
37 af.sent = append(af.sent, sentMail{token, to, subject, body})
38 if af.err != nil && (af.failFor == "" || af.failFor == to) {
39 return "", af.err
40 }
41 return "msg-1", nil
42 }
43 return af
44}
45
46// newAlertFixture is the common shape: two operators, only one of whom ever
47// granted the console send access.
48func newAlertFixture(t *testing.T) *alertFixture {
49 t.Helper()
50 af := newCapturedFixture(t)
51 for _, addr := range []string{"ops-a@keymail.dev", "ops-b@keymail.dev"} {
52 if err := af.st.SetOperator(addr, true); err != nil {
53 t.Fatal(err)
54 }
55 }
56 if err := af.st.SetGrantToken("ops-a@keymail.dev", "grant-a", time.Now()); err != nil {
57 t.Fatal(err)
58 }
59 return af
60}
61
62func (af *alertFixture) writeAlert(t *testing.T, st metrics.AlertState) {
63 t.Helper()
64 b, err := json.Marshal(st)
65 if err != nil {
66 t.Fatal(err)
67 }
68 // Publishing is a CAS write (create-only on ""), so re-publishing a box's
69 // state — which is what a fire→clear transition is — needs the etag.
70 key := metrics.AlertKey(st.Box)
71 etag := ""
72 if _, cur, err := af.rel.Get(context.Background(), key); err == nil {
73 etag = cur
74 }
75 if _, err := af.rel.Put(context.Background(), key, b, etag); err != nil {
76 t.Fatal(err)
77 }
78}
79
80func (af *alertFixture) pass(t *testing.T) {
81 t.Helper()
82 af.sent = nil
83 if err := af.srv.alertPass(context.Background()); err != nil {
84 t.Fatal(err)
85 }
86}
87
88func firingDisk(firing bool) metrics.AlertState {
89 rs := metrics.RuleState{Firing: firing, Value: 0.95}
90 if firing {
91 rs.FiredAt = time.Date(2026, 8, 2, 10, 0, 0, 0, time.UTC)
92 rs.Since = rs.FiredAt
93 } else {
94 rs.Value = 0.80
95 }
96 return metrics.AlertState{
97 V: 1,
98 Box: "box-a",
99 UpdatedAt: time.Date(2026, 8, 2, 10, 5, 0, 0, time.UTC),
100 Rules: map[string]metrics.RuleState{"disk": rs},
101 }
102}
103
104// TestAlertPassFiresOnceThenGoesQuiet is the spec's core promise: a keymail
105// on the TRANSITION, to the operators who hold a grant, and silence while
106// the alert stays in the state the console already reported.
107func TestAlertPassFiresOnceThenGoesQuiet(t *testing.T) {
108 af := newAlertFixture(t)
109 af.writeAlert(t, firingDisk(true))
110
111 af.pass(t)
112 if len(af.sent) != 1 {
113 t.Fatalf("first pass sent %d keymails, want 1: %+v", len(af.sent), af.sent)
114 }
115 got := af.sent[0]
116 if got.to != "ops-a@keymail.dev" || got.token != "grant-a" {
117 t.Fatalf("sent to %q with token %q, want ops-a@keymail.dev/grant-a", got.to, got.token)
118 }
119 if got.subject != "[carlos] box-a: disk alert" {
120 t.Fatalf("subject = %q", got.subject)
121 }
122 // "95%", not "0.95": the mail carries the rule's own unit, the same as
123 // the operator dashboard's alerts table (see alertValueText).
124 for _, want := range []string{"disk", "box-a", "95%", "2026-08-02T10:00:00Z"} {
125 if !strings.Contains(got.body, want) {
126 t.Fatalf("body is missing %q:\n%s", want, got.body)
127 }
128 }
129 if strings.Contains(got.body, "grant-a") {
130 t.Fatal("body leaked the grant token")
131 }
132
133 // Recorded: the same state on the next pass says nothing.
134 firing, known, err := af.st.AlertSeen("box-a", "disk", "ops-a@keymail.dev")
135 if err != nil || !known || !firing {
136 t.Fatalf("AlertSeen = %v,%v,%v — want true,true,nil", firing, known, err)
137 }
138 af.pass(t)
139 if len(af.sent) != 0 {
140 t.Fatalf("second pass sent %d keymails, want 0: %+v", len(af.sent), af.sent)
141 }
142}
143
144// TestAlertPassClears covers the other half of the transition: the alert
145// going away is news too.
146func TestAlertPassClears(t *testing.T) {
147 af := newAlertFixture(t)
148 af.writeAlert(t, firingDisk(true))
149 af.pass(t)
150
151 af.writeAlert(t, firingDisk(false))
152 af.pass(t)
153 if len(af.sent) != 1 {
154 t.Fatalf("clear pass sent %d keymails, want 1: %+v", len(af.sent), af.sent)
155 }
156 if af.sent[0].subject != "[carlos] box-a: disk cleared" {
157 t.Fatalf("subject = %q", af.sent[0].subject)
158 }
159 af.pass(t)
160 if len(af.sent) != 0 {
161 t.Fatalf("pass after clear sent %d keymails, want 0", len(af.sent))
162 }
163}
164
165// TestAlertPassRetriesAfterSendFailure: a failed send leaves the transition
166// unrecorded, so the next tick delivers it. A missed page is worse than a
167// duplicate one.
168func TestAlertPassRetriesAfterSendFailure(t *testing.T) {
169 af := newAlertFixture(t)
170 af.writeAlert(t, firingDisk(true))
171 af.err = context.DeadlineExceeded
172
173 af.pass(t)
174 if len(af.sent) != 1 {
175 t.Fatalf("failing pass attempted %d sends, want 1", len(af.sent))
176 }
177 if _, known, err := af.st.AlertSeen("box-a", "disk", "ops-a@keymail.dev"); err != nil || known {
178 t.Fatalf("AlertSeen recorded despite a send failure (known=%v, err=%v)", known, err)
179 }
180
181 af.err = nil
182 af.pass(t)
183 if len(af.sent) != 1 {
184 t.Fatalf("retry pass sent %d keymails, want 1", len(af.sent))
185 }
186 if firing, known, err := af.st.AlertSeen("box-a", "disk", "ops-a@keymail.dev"); err != nil || !known || !firing {
187 t.Fatalf("AlertSeen = %v,%v,%v after a successful retry", firing, known, err)
188 }
189}
190
191// TestAlertPassRetriesOnlyTheFailedOperator is why alert_seen is keyed per
192// recipient: one operator's dead grant must not re-page every healthy
193// operator on the fleet, tick after tick.
194func TestAlertPassRetriesOnlyTheFailedOperator(t *testing.T) {
195 af := newAlertFixture(t)
196 if err := af.st.SetGrantToken("ops-b@keymail.dev", "grant-b", time.Now()); err != nil {
197 t.Fatal(err)
198 }
199 af.writeAlert(t, firingDisk(true))
200 af.err = context.DeadlineExceeded
201 af.failFor = "ops-b@keymail.dev"
202
203 af.pass(t)
204 if len(af.sent) != 2 {
205 t.Fatalf("first pass attempted %d sends, want 2: %+v", len(af.sent), af.sent)
206 }
207 if firing, known, err := af.st.AlertSeen("box-a", "disk", "ops-a@keymail.dev"); err != nil || !known || !firing {
208 t.Fatalf("ops-a AlertSeen = %v %v %v — a healthy delivery was not recorded", firing, known, err)
209 }
210 if _, known, err := af.st.AlertSeen("box-a", "disk", "ops-b@keymail.dev"); err != nil || known {
211 t.Fatalf("ops-b AlertSeen recorded despite a failed send (known=%v, err=%v)", known, err)
212 }
213
214 af.err = nil
215 af.pass(t)
216 if len(af.sent) != 1 || af.sent[0].to != "ops-b@keymail.dev" {
217 t.Fatalf("retry pass sent %+v, want exactly one mail to ops-b", af.sent)
218 }
219 if af.sent[0].token != "grant-b" {
220 t.Fatalf("retry used token %q, want ops-b's own grant", af.sent[0].token)
221 }
222 af.pass(t)
223 if len(af.sent) != 0 {
224 t.Fatalf("pass after the retry sent %d keymails, want 0", len(af.sent))
225 }
226}
227
228// TestAlertPassNoMailableOperator pins the one path where an alert is
229// genuinely dropped: nobody holds a grant, so there is nothing to send and
230// the transition is recorded against the synthetic recipient so it does not
231// replay every minute forever. The honest cost — documented, and asserted
232// here rather than left to be discovered — is that an operator who gains a
233// grant afterwards is never told about that transition.
234func TestAlertPassNoMailableOperator(t *testing.T) {
235 af := newCapturedFixture(t)
236 if err := af.st.SetOperator("ops-b@keymail.dev", true); err != nil {
237 t.Fatal(err)
238 }
239 af.writeAlert(t, firingDisk(true))
240
241 af.pass(t)
242 if len(af.sent) != 0 {
243 t.Fatalf("sent %d keymails with no grant anywhere: %+v", len(af.sent), af.sent)
244 }
245 if firing, known, err := af.st.AlertSeen("box-a", "disk", ""); err != nil || !known || !firing {
246 t.Fatalf("synthetic AlertSeen = %v %v %v, want true true nil — the drop was not recorded", firing, known, err)
247 }
248
249 // The grant arrives after the fact. The alert is still firing, but the
250 // console has already recorded the transition as handled: no catch-up.
251 if err := af.st.SetGrantToken("ops-b@keymail.dev", "grant-b", time.Now()); err != nil {
252 t.Fatal(err)
253 }
254 af.pass(t)
255 if len(af.sent) != 0 {
256 t.Fatalf("a newly-granted operator was retroactively paged: %+v", af.sent)
257 }
258 // But the NEXT transition reaches them normally.
259 af.writeAlert(t, firingDisk(false))
260 af.pass(t)
261 if len(af.sent) != 1 || af.sent[0].to != "ops-b@keymail.dev" {
262 t.Fatalf("clear after the grant arrived sent %+v, want one mail to ops-b", af.sent)
263 }
264}
265
266// TestAlertPassQuietFirstSighting: a rule the console has never seen that is
267// NOT firing is recorded silently. Nobody wants a keymail saying the disk is
268// fine.
269func TestAlertPassQuietFirstSighting(t *testing.T) {
270 af := newAlertFixture(t)
271 af.writeAlert(t, firingDisk(false))
272
273 af.pass(t)
274 if len(af.sent) != 0 {
275 t.Fatalf("quiet first sighting sent %d keymails, want 0: %+v", len(af.sent), af.sent)
276 }
277 if firing, known, err := af.st.AlertSeen("box-a", "disk", "ops-a@keymail.dev"); err != nil || !known || firing {
278 t.Fatalf("AlertSeen = %v,%v,%v — want false,true,nil", firing, known, err)
279 }
280}
281
282// TestAlertPassSurvivesJunk: an unparsable object under alerts/ is skipped,
283// and the boxes either side of it are still processed. A watcher that dies
284// on one bad file is a watcher that stops paging.
285func TestAlertPassSurvivesJunk(t *testing.T) {
286 af := newAlertFixture(t)
287 if _, err := af.rel.Put(context.Background(), "alerts/boxes/box-junk.json", []byte("{not json"), ""); err != nil {
288 t.Fatal(err)
289 }
290 af.writeAlert(t, firingDisk(true))
291
292 af.pass(t)
293 if len(af.sent) != 1 {
294 t.Fatalf("pass sent %d keymails, want 1: %+v", len(af.sent), af.sent)
295 }
296}
297
298// TestAlertPassEmptyBucket: no alerts/ prefix at all is a no-op, not an
299// error — a fleet that has never published an alert is the normal case.
300func TestAlertPassEmptyBucket(t *testing.T) {
301 fx := newConsoleFixture(t)
302 fx.srv.sendApp = func(context.Context, string, string, string, string) (string, error) {
303 t.Fatal("sent a keymail with no alerts published")
304 return "", nil
305 }
306 if err := fx.srv.alertPass(context.Background()); err != nil {
307 t.Fatal(err)
308 }
309}
310
311// TestNewServerDefaultsSendApp: production wiring goes through the console's
312// own keymail client; nothing else in NewServer starts a goroutine.
313func TestNewServerDefaultsSendApp(t *testing.T) {
314 fx := newConsoleFixture(t)
315 if fx.srv.sendApp == nil {
316 t.Fatal("NewServer left sendApp nil — the watcher would never deliver")
317 }
318 var _ release.Store = fx.rel
319}
320
321func geoipAlert(code float64, firing bool) metrics.AlertState {
322 rs := metrics.RuleState{Firing: firing, Value: code, FiredAt: time.Date(2026, 10, 6, 10, 0, 0, 0, time.UTC)}
323 return metrics.AlertState{V: 1, Box: "box-a", UpdatedAt: time.Date(2026, 10, 6, 10, 5, 0, 0, time.UTC),
324 Rules: map[string]metrics.RuleState{"geoip": rs}}
325}
326
327// Code review R4-3: a geoip reason that changes while firing is mailed.
328func TestAlertPassMailsAGeoIPReasonChange(t *testing.T) {
329 af := newAlertFixture(t)
330 step := func(st metrics.AlertState, wantMails int, wantBody string) {
331 t.Helper()
332 af.writeAlert(t, st)
333 af.pass(t) // resets af.sent
334 got := af.sent
335 if len(got) != wantMails {
336 t.Fatalf("mails = %d, want %d: %+v", len(got), wantMails, got)
337 }
338 if wantBody != "" && !strings.Contains(got[0].body, wantBody) {
339 t.Fatalf("body lacks %q: %s", wantBody, got[0].body)
340 }
341 }
342 step(geoipAlert(2, true), 1, "expires within 9 days")
343 step(geoipAlert(2, true), 0, "")
344 step(geoipAlert(3, true), 1, "expired and removed")
345 step(geoipAlert(0, false), 1, "")
346 // A later episode, and a return to an earlier reason within one: each
347 // is news (code review R5 on R4-3).
348 step(geoipAlert(2, true), 1, "expires within 9 days")
349 step(geoipAlert(3, true), 1, "expired and removed")
350 step(geoipAlert(2, true), 1, "expires within 9 days")
351 step(geoipAlert(2, true), 0, "")
352}
353