carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package status_test
2
3import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "errors"
8 "fmt"
9 "reflect"
10 "strings"
11 "testing"
12 "time"
13
14 "github.com/carlosframework/platform/internal/registry"
15 "github.com/carlosframework/platform/internal/release"
16 "github.com/carlosframework/platform/internal/status"
17)
18
19// flakyPutStore wraps a real release.Store and makes its first N Put calls
20// return release.ErrCASConflict before delegating — a stand-in for another
21// writer racing in between WriteBox's Get-etag and Put. Every other method
22// (Get, List) is the embedded Store's real behavior.
23type flakyPutStore struct {
24 release.Store
25 failCount int // Put calls left to fail before delegating
26 putCalls int
27}
28
29func (f *flakyPutStore) Put(ctx context.Context, key string, body []byte, ifMatch string) (string, error) {
30 f.putCalls++
31 if f.putCalls <= f.failCount {
32 return "", release.ErrCASConflict
33 }
34 return f.Store.Put(ctx, key, body, ifMatch)
35}
36
37func TestBandBoundaries(t *testing.T) {
38 f := func(v float64) *float64 { return &v }
39 cases := []struct {
40 frac *float64
41 want string
42 }{
43 {f(0.69), "plenty"}, {f(0.70), "getting-full"},
44 {f(0.90), "getting-full"}, {f(0.91), "at-capacity"},
45 }
46 for _, c := range cases {
47 got := status.CollectBox(nil, status.Probes{MachineID: "m", DiskFrac: c.frac}, time.Now()).Capacity
48 if got == nil || *got != c.want {
49 t.Errorf("frac %v: got %v want %s", *c.frac, got, c.want)
50 }
51 }
52 if b := status.CollectBox(nil, status.Probes{MachineID: "m"}, time.Now()); b.Capacity != nil || b.Load != nil {
53 t.Error("unknown fractions must band to null, not a word")
54 }
55}
56
57func TestAwakeAsleepNullWhenUnreadable(t *testing.T) {
58 routes := []registry.Route{{Host: "a", Kind: "instance", Hibernate: true, Socket: "/x.sock"}}
59 b := status.CollectBox(routes, status.Probes{MachineID: "m"}, time.Now()) // nil Listening
60 if b.Routes.Awake != nil || b.Routes.Asleep != nil {
61 t.Error("nil socket probe must publish null, never 0")
62 }
63}
64
65// TestCollectBoxSkipsPlatformMintedRoutes: a platform domain is a second
66// NAME for a route already counted, not a second instance. Counting both
67// would roughly double every fleet-wide instance number the moment
68// PLATFORM_APPS_DOMAIN is set — the public status page overstating the
69// fleet, which is exactly the kind of quiet dishonesty the page's whole
70// design exists to prevent.
71func TestCollectBoxSkipsPlatformMintedRoutes(t *testing.T) {
72 listening := func(string) *bool { v := true; return &v }
73 routes := []registry.Route{
74 {Host: "kass.training", Kind: "instance", Status: "ready"},
75 {Host: "kass.bab.oncarlos.test", Kind: "instance", Status: "ready", Platform: true},
76 {Host: "site.custom", Kind: "static"},
77 {Host: "site.bab.oncarlos.test", Kind: "static", Platform: true},
78 {Host: "sleepy.custom", Kind: "instance", Hibernate: true, Socket: "/s.sock"},
79 {Host: "sleepy.bab.oncarlos.test", Kind: "instance", Hibernate: true, Socket: "/s.sock", Platform: true},
80 }
81 b := status.CollectBox(routes, status.Probes{MachineID: "m", Listening: listening}, time.Now())
82 if b.Routes.Resident != 1 || b.Routes.Static != 1 || b.Routes.Hibernating != 1 {
83 t.Errorf("counts = %+v, want exactly one of each: aliases must not inflate the fleet", b.Routes)
84 }
85 if b.Routes.Awake == nil || *b.Routes.Awake != 1 {
86 t.Errorf("awake = %v, want 1 — the alias must not be counted awake twice", b.Routes.Awake)
87 }
88}
89
90// TestCollectBoxSidecarNotResident: a sidecar (Kind "sidecar") is not the
91// public fleet page's business — its existence is account-visible, not
92// public. Before an explicit branch, it fell into "default: Resident++",
93// silently inflating the public resident-app-instance count. It must land
94// in neither Resident, Static nor Hibernating (even when it happens to be
95// hibernating — a sidecar's Hibernate comes from the app manifest, so it may
96// legitimately be either), and Awake/Asleep must not count it — its Socket
97// is empty by construction, so probing it would be meaningless.
98func TestCollectBoxSidecarNotResident(t *testing.T) {
99 listening := func(string) *bool { v := true; return &v }
100 routes := []registry.Route{
101 {Host: "bot.eleven.bdm.sidecar.internal", Kind: "sidecar", Status: "ready"},
102 {Host: "bot2.eleven.bdm.sidecar.internal", Kind: "sidecar", Status: "ready", Hibernate: true},
103 {Host: "real.custom", Kind: "instance", Status: "ready"},
104 }
105 b := status.CollectBox(routes, status.Probes{MachineID: "m", Listening: listening}, time.Now())
106 if b.Routes.Resident != 1 {
107 t.Errorf("Resident = %d, want 1 — a sidecar must not be counted as a public resident app instance", b.Routes.Resident)
108 }
109 if b.Routes.Hibernating != 0 {
110 t.Errorf("Hibernating = %d, want 0 — a hibernating sidecar still belongs in its own bucket, not this one", b.Routes.Hibernating)
111 }
112 if b.Sidecars != 2 {
113 t.Errorf("Sidecars = %d, want 2 — both sidecars, hibernating or not", b.Sidecars)
114 }
115}
116
117// TestSidecarsNeverReachFleetJSON is the dryrun.go pattern applied to this
118// one field: BuildFleet's per-box copy names Box's public fields one at a
119// time (fleet.go), and Sidecars must never be one of them — a sidecar's
120// existence is account-visible, not public. Proven by round-tripping through
121// the real writer/merger, not just by the FleetBox struct's shape, so a
122// future field added to FleetBox that DOES copy it would be caught here too.
123func TestSidecarsNeverReachFleetJSON(t *testing.T) {
124 s := teststore(t)
125 now := time.Now()
126 routes := []registry.Route{{Host: "bot.x.sidecar.internal", Kind: "sidecar", Status: "ready"}}
127 b := status.CollectBox(routes, status.Probes{MachineID: "leaky"}, now)
128 if b.Sidecars != 1 {
129 t.Fatalf("Sidecars = %d, want 1 in the private snapshot itself", b.Sidecars)
130 }
131 if err := status.WriteBox(context.Background(), s, b); err != nil {
132 t.Fatal(err)
133 }
134 f, err := status.BuildFleet(context.Background(), s, nil, now, 5*time.Minute)
135 if err != nil {
136 t.Fatal(err)
137 }
138 j, err := json.Marshal(f)
139 if err != nil {
140 t.Fatal(err)
141 }
142 if strings.Contains(strings.ToLower(string(j)), "sidecar") {
143 t.Errorf("fleet.json mentions sidecars: %s", j)
144 }
145}
146
147func TestLabelStableAndOpaque(t *testing.T) {
148 a, b := status.Label("machine-1"), status.Label("machine-1")
149 if a != b || a == "" || strings.Contains(a, "machine") {
150 t.Errorf("label %q must be stable and never contain the machine id", a)
151 }
152}
153
154func TestSnapshotJSONCarriesNoHostnames(t *testing.T) {
155 routes := []registry.Route{{Host: "secret.example.com", Kind: "instance", Status: "ready"}}
156 b := status.CollectBox(routes, status.Probes{MachineID: "ip-10-0-0-7"}, time.Now())
157 j, _ := json.Marshal(b)
158 for _, leak := range []string{"secret", "example.com", "ip-10-0-0-7"} {
159 if strings.Contains(string(j), leak) {
160 t.Errorf("snapshot leaked %q: %s", leak, j)
161 }
162 }
163}
164
165// TestRouteCountsJSONKeysAreSnakeCase locks RouteCounts' wire shape to the
166// design spec's fleet.json example (docs/superpowers/specs/2026-08-01-fleet-
167// status-page-design.md §2: {"static":2,"resident":3,"hibernating":4,
168// "awake":1,"asleep":3}) — lowercase snake_case, not Go's default
169// capitalized field names.
170func TestRouteCountsJSONKeysAreSnakeCase(t *testing.T) {
171 routes := []registry.Route{
172 {Host: "a", Kind: "static"},
173 {Host: "b", Kind: "instance"},
174 {Host: "c", Kind: "instance", Hibernate: true, Socket: "/c.sock"},
175 }
176 listening := func(string) *bool { v := true; return &v }
177 b := status.CollectBox(routes, status.Probes{MachineID: "m", Listening: listening}, time.Now())
178 j, err := json.Marshal(b.Routes)
179 if err != nil {
180 t.Fatalf("marshal RouteCounts: %v", err)
181 }
182 for _, key := range []string{`"static"`, `"resident"`, `"hibernating"`, `"awake"`, `"asleep"`} {
183 if !strings.Contains(string(j), key) {
184 t.Errorf("RouteCounts JSON %s missing key %s", j, key)
185 }
186 }
187 for _, key := range []string{`"Static"`, `"Resident"`, `"Hibernating"`, `"Awake"`, `"Asleep"`} {
188 if strings.Contains(string(j), key) {
189 t.Errorf("RouteCounts JSON %s carries capitalized Go field name %s", j, key)
190 }
191 }
192}
193
194// TestWriteBoxPutsUnderLabeledKey isn't in the plan's Step-1 listing, but
195// WriteBox is part of the exported contract Task 3 relies on — worth locking
196// down that it actually lands the box at the key CollectBox's label implies.
197func TestWriteBoxPutsUnderLabeledKey(t *testing.T) {
198 dir := t.TempDir()
199 store := release.NewDirStore(dir)
200 b := status.CollectBox(nil, status.Probes{MachineID: "m"}, time.Now())
201
202 if err := status.WriteBox(context.Background(), store, b); err != nil {
203 t.Fatalf("WriteBox: %v", err)
204 }
205
206 key := "status/boxes/" + b.Label + ".json"
207 got, _, err := store.Get(context.Background(), key)
208 if err != nil {
209 t.Fatalf("Get %s: %v", key, err)
210 }
211 var round status.Box
212 if err := json.Unmarshal(got, &round); err != nil {
213 t.Fatalf("unmarshal written box: %v", err)
214 }
215 if round.Label != b.Label {
216 t.Errorf("round-tripped label %q, want %q", round.Label, b.Label)
217 }
218}
219
220// TestWriteBoxOverwritesOnSecondTick proves WriteBox is last-writer-wins for
221// a given label: boxes publish every 5 minutes to the same key, so a
222// create-only Put (which fails once the object exists) would break on the
223// second tick. Both calls must succeed, and the second call's content must
224// be what a reader sees afterward.
225func TestWriteBoxOverwritesOnSecondTick(t *testing.T) {
226 dir := t.TempDir()
227 store := release.NewDirStore(dir)
228 ctx := context.Background()
229
230 first := status.CollectBox(nil, status.Probes{MachineID: "m"}, time.Unix(1000, 0))
231 if err := status.WriteBox(ctx, store, first); err != nil {
232 t.Fatalf("first WriteBox: %v", err)
233 }
234
235 second := status.CollectBox(nil, status.Probes{MachineID: "m"}, time.Unix(2000, 0))
236 if err := status.WriteBox(ctx, store, second); err != nil {
237 t.Fatalf("second WriteBox (must overwrite, not conflict): %v", err)
238 }
239
240 key := "status/boxes/" + second.Label + ".json"
241 got, _, err := store.Get(ctx, key)
242 if err != nil {
243 t.Fatalf("Get %s: %v", key, err)
244 }
245 var round status.Box
246 if err := json.Unmarshal(got, &round); err != nil {
247 t.Fatalf("unmarshal written box: %v", err)
248 }
249 if !round.GeneratedAt.Equal(second.GeneratedAt) {
250 t.Errorf("stored box has GeneratedAt %v, want the second write's %v (second write must win)", round.GeneratedAt, second.GeneratedAt)
251 }
252}
253
254// TestWriteBoxRetriesOnceOnCASConflict exercises the retry branch directly:
255// the first Put loses the CAS race (something else wrote between WriteBox's
256// Get and its Put), so WriteBox must re-read the etag and retry exactly
257// once — and that retry must succeed.
258func TestWriteBoxRetriesOnceOnCASConflict(t *testing.T) {
259 dir := t.TempDir()
260 real := release.NewDirStore(dir)
261 flaky := &flakyPutStore{Store: real, failCount: 1}
262 b := status.CollectBox(nil, status.Probes{MachineID: "m"}, time.Now())
263
264 if err := status.WriteBox(context.Background(), flaky, b); err != nil {
265 t.Fatalf("WriteBox with one transient conflict: %v", err)
266 }
267 if flaky.putCalls != 2 {
268 t.Errorf("Put called %d times, want exactly 2 (fail once, retry once)", flaky.putCalls)
269 }
270
271 key := "status/boxes/" + b.Label + ".json"
272 if _, _, err := real.Get(context.Background(), key); err != nil {
273 t.Fatalf("Get %s after retry: %v", key, err)
274 }
275}
276
277// ---- Task 5: raw Metrics block (private to the bucket) --------------------
278
279// TestCollectBoxCarriesRawMetrics proves CollectBox attaches an unbanded
280// Metrics block straight through from Probes when the six fields are known —
281// the one deliberate exception to this package's band-everything rule (see
282// snapshot.go's package doc).
283func TestCollectBoxCarriesRawMetrics(t *testing.T) {
284 mt, ma, df := int64(4056547328), int64(2028273664), int64(9999999)
285 l1 := 0.42
286 b := status.CollectBox(nil, status.Probes{
287 MachineID: "m", MemTotalBytes: &mt, MemAvailBytes: &ma, Load1: &l1, DiskFreeBytes: &df,
288 Release: "abc1234", Tunnel: "connected",
289 }, time.Now())
290 m := b.Metrics
291 if m == nil || *m.MemTotalBytes != mt || *m.Load1 != l1 || m.Release != "abc1234" || m.Tunnel != "connected" {
292 t.Fatalf("metrics block wrong: %+v", m)
293 }
294}
295
296// TestCollectBoxOmitsMetricsWhenAllUnknown proves the all-unknown case
297// really omits the block rather than attaching an empty-but-present
298// BoxMetrics{} — the same "unknown stays absent" rule every other Box field
299// follows, and what makes an old parser's absent-key shape and a
300// can't-answer box's shape identical on the wire.
301func TestCollectBoxOmitsMetricsWhenAllUnknown(t *testing.T) {
302 b := status.CollectBox(nil, status.Probes{MachineID: "m"}, time.Now())
303 if b.Metrics != nil {
304 t.Fatalf("metrics block should be nil when nothing is known, got %+v", b.Metrics)
305 }
306}
307
308// TestBoxBackCompatParse: yesterday's Box shape (no metrics key) must parse
309// today, and a FUTURE box's unknown keys (inside or alongside metrics) must
310// not break today's parse either — additive wire shape, per AGENTS.md's
311// "consumer = everything that PARSES the shape" rule.
312func TestBoxBackCompatParse(t *testing.T) {
313 yesterday := []byte(`{"generated_at":"2026-08-03T10:00:00Z","label":"ada-stargazing",
314 "routes":{"static":1,"resident":2,"hibernating":3,"awake":null,"asleep":null},
315 "load":"comfortable","capacity":"plenty",
316 "replication":{"healthy":true,"lag_seconds":5},
317 "adoption_running":true,"expired_canaries":0}`)
318 var b status.Box
319 if err := json.Unmarshal(yesterday, &b); err != nil {
320 t.Fatalf("yesterday's shape failed to parse: %v", err)
321 }
322 if b.Metrics != nil {
323 t.Fatal("absent metrics must decode as nil, not zero-value")
324 }
325 tomorrow := []byte(`{"generated_at":"2026-08-05T10:00:00Z","label":"ada-stargazing",
326 "routes":{"static":0,"resident":0,"hibernating":0,"awake":null,"asleep":null},
327 "load":null,"capacity":null,"replication":{"healthy":null,"lag_seconds":null},
328 "adoption_running":null,"expired_canaries":null,
329 "metrics":{"mem_total_bytes":1,"some_future_field":true}}`)
330 if err := json.Unmarshal(tomorrow, &b); err != nil {
331 t.Fatalf("tomorrow's shape failed to parse: %v", err)
332 }
333}
334
335// ---- Task 8: staged_version (route versions, private) ----------------------
336
337// TestCollectBoxCarriesRouteVersions proves CollectBox attaches a host ->
338// Version map onto the raw Metrics block for every non-platform route that
339// carries a Version (registry.Route.Version, stamped by the adopt pass), and
340// that WriteBox lands it in status/boxes/<label>.json — while BuildFleet's
341// public fleet.json, which never copies Metrics by construction (see
342// snapshot.go's package doc), carries no trace of the hostname. A platform-
343// minted alias route and a route with no Version yet must not appear in the
344// map at all.
345func TestCollectBoxCarriesRouteVersions(t *testing.T) {
346 routes := []registry.Route{
347 {Host: "h.test", Kind: "instance", Status: "ready", Version: "abc1234"},
348 {Host: "unversioned.test", Kind: "instance", Status: "ready"},
349 {Host: "platform.test", Kind: "instance", Status: "ready", Version: "zzz9999", Platform: true},
350 }
351 b := status.CollectBox(routes, status.Probes{MachineID: "m"}, time.Now())
352 if b.Metrics == nil {
353 t.Fatal("metrics block missing — RouteVersions should have attached it")
354 }
355 if len(b.Metrics.RouteVersions) != 1 || b.Metrics.RouteVersions["h.test"] != "abc1234" {
356 t.Fatalf("route_versions = %+v, want exactly {h.test: abc1234}", b.Metrics.RouteVersions)
357 }
358
359 s := teststore(t)
360 ctx := context.Background()
361 if err := status.WriteBox(ctx, s, b); err != nil {
362 t.Fatal(err)
363 }
364 raw, _, err := s.Get(ctx, "status/boxes/"+b.Label+".json")
365 if err != nil {
366 t.Fatalf("Get written box: %v", err)
367 }
368 var round status.Box
369 if err := json.Unmarshal(raw, &round); err != nil {
370 t.Fatalf("unmarshal written box: %v", err)
371 }
372 if round.Metrics == nil || round.Metrics.RouteVersions["h.test"] != "abc1234" {
373 t.Errorf("written box route_versions = %+v, want {h.test: abc1234}: %s", round.Metrics, raw)
374 }
375
376 f, err := status.BuildFleet(ctx, s, routes, time.Now(), 5*time.Minute)
377 if err != nil {
378 t.Fatal(err)
379 }
380 fj, err := json.Marshal(f)
381 if err != nil {
382 t.Fatal(err)
383 }
384 if strings.Contains(string(fj), "h.test") {
385 t.Errorf("fleet.json leaked a route_versions hostname: %s", fj)
386 }
387}
388
389// TestCollectBoxRouteVersionsEmptyOmitsField proves a box with no versioned
390// route contributes no route_versions key at all — omitted, not an empty
391// object — matching the same "unknown/absent stays absent" convention every
392// other Metrics field follows.
393func TestCollectBoxRouteVersionsEmptyOmitsField(t *testing.T) {
394 routes := []registry.Route{{Host: "h.test", Kind: "instance", Status: "ready"}}
395 b := status.CollectBox(routes, status.Probes{MachineID: "m"}, time.Now())
396 if b.Metrics != nil {
397 t.Fatalf("metrics block should stay nil with nothing else known, got %+v", b.Metrics)
398 }
399}
400
401// ---- Task 6 (multi-upstream plan): per-upstream probe state ----------------
402
403// TestCollectBoxCarriesUpstreams is route_versions' sibling for the probe
404// table: CollectBox carries Probes.Upstreams onto the private Metrics block
405// verbatim, WriteBox lands it in status/boxes/<label>.json, and BuildFleet's
406// public fleet.json shows no trace of either the route host or the upstream
407// ADDRESS — the most identifying pair this block has ever carried, and safe
408// for exactly the same structural reason (BuildFleet never names Metrics).
409func TestCollectBoxCarriesUpstreams(t *testing.T) {
410 routes := []registry.Route{{Host: "ha.test", Kind: "service", Status: "ready"}}
411 since := time.Now().Add(-time.Minute).UTC().Truncate(time.Second)
412 p := status.Probes{MachineID: "m", Upstreams: map[string][]status.UpstreamState{
413 "ha.test": {
414 {Addr: "10.9.9.11:8080", Up: true, Since: since},
415 {Addr: "10.9.9.12:8080", Up: false, Since: since, Fails: 4, LastError: "dial: connection refused"},
416 },
417 }}
418
419 b := status.CollectBox(routes, p, time.Now())
420 if b.Metrics == nil {
421 t.Fatal("metrics block missing — Upstreams should have attached it")
422 }
423 if len(b.Metrics.Upstreams) != 1 || len(b.Metrics.Upstreams["ha.test"]) != 2 {
424 t.Fatalf("upstreams = %+v, want one host with two upstreams", b.Metrics.Upstreams)
425 }
426
427 s := teststore(t)
428 ctx := context.Background()
429 if err := status.WriteBox(ctx, s, b); err != nil {
430 t.Fatal(err)
431 }
432 raw, _, err := s.Get(ctx, "status/boxes/"+b.Label+".json")
433 if err != nil {
434 t.Fatalf("Get written box: %v", err)
435 }
436 var round status.Box
437 if err := json.Unmarshal(raw, &round); err != nil {
438 t.Fatalf("unmarshal written box: %v", err)
439 }
440 got := round.Metrics.Upstreams["ha.test"]
441 if !reflect.DeepEqual(got, p.Upstreams["ha.test"]) {
442 t.Errorf("written upstreams = %+v, want %+v: %s", got, p.Upstreams["ha.test"], raw)
443 }
444
445 f, err := status.BuildFleet(ctx, s, routes, time.Now(), 5*time.Minute)
446 if err != nil {
447 t.Fatal(err)
448 }
449 fj, err := json.Marshal(f)
450 if err != nil {
451 t.Fatal(err)
452 }
453 for _, needle := range []string{"ha.test", "10.9.9.11:8080", "connection refused"} {
454 if strings.Contains(string(fj), needle) {
455 t.Errorf("fleet.json leaked %q from the private upstream table: %s", needle, fj)
456 }
457 }
458}
459
460// TestCollectBoxUpstreamsEmptyOmitsField proves a box that probes nothing
461// contributes no upstreams key at all — the same "absent, not empty" rule
462// route_versions follows, which is what keeps every pre-existing snapshot
463// consumer parsing a box written by this build unchanged.
464func TestCollectBoxUpstreamsEmptyOmitsField(t *testing.T) {
465 routes := []registry.Route{{Host: "h.test", Kind: "instance", Status: "ready"}}
466 b := status.CollectBox(routes, status.Probes{MachineID: "m", Upstreams: map[string][]status.UpstreamState{}}, time.Now())
467 if b.Metrics != nil {
468 t.Fatalf("an empty probe table must not attach a metrics block, got %+v", b.Metrics)
469 }
470 raw, err := json.Marshal(b)
471 if err != nil {
472 t.Fatal(err)
473 }
474 if strings.Contains(string(raw), "upstreams") {
475 t.Errorf("box JSON carries an upstreams key with nothing probed: %s", raw)
476 }
477}
478
479// ---- Task 6: alias readings (route_aliases DNS/cert stamps, private) -----
480
481// TestCollectBoxAliases proves CollectBox maps Probes.Aliases (registry rows
482// carried in by the agent's own reg.ListAliases() call, see agent.go's
483// statusTick) onto Box.Metrics.Aliases, keyed by the alias hostname — the
484// same private home as RouteVersions (status/boxes/<label>.json only, never
485// BuildFleet's public fleet.json) — and that an empty Probes.Aliases omits
486// the "aliases" key entirely rather than publishing a present-but-empty map.
487func TestCollectBoxAliases(t *testing.T) {
488 p := status.Probes{
489 MachineID: "m",
490 Aliases: []registry.RouteAlias{
491 {Alias: "extra.test", Host: "h.test", DNSStatus: "ok", CertExpiresAt: 1234567890},
492 },
493 }
494 b := status.CollectBox(nil, p, time.Now())
495 if b.Metrics == nil {
496 t.Fatal("metrics block missing — Aliases should have attached it")
497 }
498 got, ok := b.Metrics.Aliases["extra.test"]
499 if !ok || got.Route != "h.test" || got.DNSStatus != "ok" || got.CertExpiresAt != 1234567890 {
500 t.Fatalf("aliases[extra.test] = %+v (ok=%v), want {Route:h.test DNSStatus:ok CertExpiresAt:1234567890}", got, ok)
501 }
502
503 // Empty Probes.Aliases, but something else known so Metrics still
504 // attaches — the omission must be specific to the aliases key, not a
505 // side effect of the whole Metrics block being absent.
506 mt := int64(1)
507 b2 := status.CollectBox(nil, status.Probes{MachineID: "m", MemTotalBytes: &mt}, time.Now())
508 j, err := json.Marshal(b2)
509 if err != nil {
510 t.Fatal(err)
511 }
512 if strings.Contains(string(j), "aliases") {
513 t.Fatalf("empty Probes.Aliases must omit the aliases key entirely, got: %s", j)
514 }
515}
516
517// ---- Task 3: fleet merge + publisher --------------------------------------
518
519// teststore is a fresh DirStore over a scratch directory — every fleet test
520// gets an isolated bucket so writes in one test can never leak state into
521// another.
522func teststore(t *testing.T) release.Store {
523 t.Helper()
524 return release.NewDirStore(t.TempDir())
525}
526
527// boxFixture builds a minimal Box for tests that only care about label,
528// capacity band, and generation time — the three things BuildFleet's
529// worst-of/staleness logic actually looks at.
530func boxFixture(label, capacity string, generatedAt time.Time) status.Box {
531 c := capacity
532 return status.Box{
533 GeneratedAt: generatedAt,
534 Label: label,
535 Routes: status.RouteCounts{},
536 Capacity: &c,
537 }
538}
539
540func TestFleetAppsOptIn(t *testing.T) {
541 routes := []registry.Route{
542 {Host: "kass.training", Kind: "instance", App: "kass", Status: "ready",
543 Channel: "stable", Version: "aedcae3", StatusNamed: true},
544 {Host: "hidden.app", Kind: "instance", App: "hidden", Status: "ready"},
545 {Host: "svc.pdf.internal", Kind: "service", App: "svc/pdf", Status: "ready"},
546 }
547 s := teststore(t) // DirStore over t.TempDir(); write one Box via WriteBox first
548 if err := status.WriteBox(context.Background(), s, boxFixture("solo", "plenty", time.Now())); err != nil {
549 t.Fatal(err)
550 }
551 f, err := status.BuildFleet(context.Background(), s, routes, time.Now(), 5*time.Minute)
552 if err != nil {
553 t.Fatal(err)
554 }
555 if len(f.Apps) != 1 || f.Apps[0].Name != "kass" || f.Apps[0].Version != "aedcae3" {
556 t.Errorf("apps = %+v, want exactly the opted-in kass", f.Apps)
557 }
558 if f.AppsUnlisted != 1 { // hidden; svc/* routes are services, not apps
559 t.Errorf("apps_unlisted = %d, want 1", f.AppsUnlisted)
560 }
561 if len(f.Services) != 1 || f.Services[0].Name != "svc/pdf" || !f.Services[0].Up {
562 t.Errorf("services = %+v", f.Services)
563 }
564 j, _ := json.Marshal(f)
565 if strings.Contains(string(j), "hidden") {
566 t.Errorf("un-opted app leaked: %s", j)
567 }
568}
569
570// App names are unique per ACCOUNT, never globally, so two tenants can both
571// run a "blog". Grouping on the name alone would merge them — publishing one
572// tenant's outage under the other's name, and letting either tenant's opt-in
573// decide whether the other appears at all.
574func TestFleetAppsAreGroupedPerAccount(t *testing.T) {
575 routes := []registry.Route{
576 {Host: "blog.one", Kind: "instance", App: "blog", Account: "bab", Status: "ready",
577 Channel: "stable", Version: "v1", StatusNamed: true},
578 {Host: "blog.two", Kind: "instance", App: "blog", Account: "kqmv", Status: "failed",
579 Channel: "stable", Version: "v9", StatusNamed: true},
580 }
581 s := teststore(t)
582 if err := status.WriteBox(context.Background(), s, boxFixture("solo", "plenty", time.Now())); err != nil {
583 t.Fatal(err)
584 }
585 f, err := status.BuildFleet(context.Background(), s, routes, time.Now(), 5*time.Minute)
586 if err != nil {
587 t.Fatal(err)
588 }
589 if len(f.Apps) != 2 {
590 t.Fatalf("apps = %+v, want two separate blogs (one per account)", f.Apps)
591 }
592 byVersion := map[string]bool{}
593 for _, a := range f.Apps {
594 if a.Name != "blog" {
595 t.Errorf("app name = %q, want the app name (never the sqid)", a.Name)
596 }
597 byVersion[a.Version] = a.Healthy
598 }
599 if !byVersion["v1"] {
600 t.Error("the healthy account's blog was dragged down by the other's")
601 }
602 if byVersion["v9"] {
603 t.Error("the failing account's blog was reported healthy")
604 }
605}
606
607// A tenant that did NOT opt in must stay dark even when another account's
608// same-named app did — the opt-in is per app, and merging would publish an
609// app that never asked to be named.
610func TestFleetOptInIsPerAccount(t *testing.T) {
611 routes := []registry.Route{
612 {Host: "blog.one", Kind: "instance", App: "blog", Account: "bab", Status: "ready",
613 Channel: "stable", Version: "v1", StatusNamed: true},
614 {Host: "blog.two", Kind: "instance", App: "blog", Account: "kqmv", Status: "ready",
615 Channel: "stable", Version: "secret", StatusNamed: false},
616 }
617 s := teststore(t)
618 if err := status.WriteBox(context.Background(), s, boxFixture("solo", "plenty", time.Now())); err != nil {
619 t.Fatal(err)
620 }
621 f, err := status.BuildFleet(context.Background(), s, routes, time.Now(), 5*time.Minute)
622 if err != nil {
623 t.Fatal(err)
624 }
625 if len(f.Apps) != 1 || f.Apps[0].Version != "v1" {
626 t.Fatalf("apps = %+v, want only the opted-in account's blog", f.Apps)
627 }
628 if f.AppsUnlisted != 1 {
629 t.Errorf("apps_unlisted = %d, want 1", f.AppsUnlisted)
630 }
631 j, _ := json.Marshal(f)
632 if strings.Contains(string(j), "secret") {
633 t.Errorf("un-opted app leaked: %s", j)
634 }
635}
636
637func TestFleetWorstOfAndStale(t *testing.T) {
638 s := teststore(t)
639 now := time.Now()
640 old, fresh := boxFixture("old", "at-capacity", now.Add(-16*time.Minute)),
641 boxFixture("fresh", "plenty", now) // helper: Box with that capacity word
642 for _, b := range []status.Box{old, fresh} {
643 if err := status.WriteBox(context.Background(), s, b); err != nil {
644 t.Fatal(err)
645 }
646 }
647 f, _ := status.BuildFleet(context.Background(), s, nil, now, 5*time.Minute)
648 if f.Capacity.Level == nil || *f.Capacity.Level != "at-capacity" {
649 t.Errorf("capacity must be the worst box's band, got %v", f.Capacity.Level)
650 }
651 var st, fr *status.FleetBox
652 for i := range f.Boxes {
653 if f.Boxes[i].Label == "old" {
654 st = &f.Boxes[i]
655 } else {
656 fr = &f.Boxes[i]
657 }
658 }
659 if !st.Stale || fr.Stale {
660 t.Error("boxes older than 3 periods must mark stale — loudly, never dropped")
661 }
662}
663
664// TestFleetInstanceSumsNullPropagate proves the "unknowns are null, never 0"
665// rule holds through aggregation too: one box with an unreadable socket
666// table (Awake/Asleep null) must null the FLEET-WIDE awake/asleep sum, not
667// silently drop that box's contribution and report a partial — and
668// misleadingly confident — total.
669func TestFleetInstanceSumsNullPropagate(t *testing.T) {
670 s := teststore(t)
671 now := time.Now()
672 known := status.CollectBox(
673 []registry.Route{{Host: "a", Kind: "instance", Hibernate: true, Socket: "/a.sock"}},
674 status.Probes{MachineID: "known", Listening: func(string) *bool { v := true; return &v }},
675 now,
676 )
677 unknown := status.CollectBox(
678 []registry.Route{{Host: "b", Kind: "instance", Hibernate: true, Socket: "/b.sock"}},
679 status.Probes{MachineID: "unknown"}, // nil Listening -> Awake/Asleep null
680 now,
681 )
682 for _, b := range []status.Box{known, unknown} {
683 if err := status.WriteBox(context.Background(), s, b); err != nil {
684 t.Fatal(err)
685 }
686 }
687 f, err := status.BuildFleet(context.Background(), s, nil, now, 5*time.Minute)
688 if err != nil {
689 t.Fatal(err)
690 }
691 if f.Instances["awake"] != nil {
692 t.Errorf("fleet awake = %v, want nil (one box's socket table was unreadable)", *f.Instances["awake"])
693 }
694 if f.Instances["static"] == nil || *f.Instances["static"] != 0 {
695 t.Errorf("fleet static = %v, want 0 (a known, non-null component)", f.Instances["static"])
696 }
697 if f.Instances["hibernating"] == nil || *f.Instances["hibernating"] != 2 {
698 t.Errorf("fleet hibernating = %v, want 2", f.Instances["hibernating"])
699 }
700}
701
702// TestFleetEmptyIsValid locks down the zero-snapshot state: BuildFleet must
703// not error when status/boxes/ is empty, and must report zero counts (not
704// null — there's nothing unknown about an empty fleet) with a null capacity
705// band (there's no box to have voted on one). This is the console's
706// "no snapshots yet" rendering.
707func TestFleetEmptyIsValid(t *testing.T) {
708 s := teststore(t)
709 f, err := status.BuildFleet(context.Background(), s, nil, time.Now(), 5*time.Minute)
710 if err != nil {
711 t.Fatal(err)
712 }
713 if len(f.Boxes) != 0 {
714 t.Errorf("boxes = %+v, want none", f.Boxes)
715 }
716 if f.Capacity.Level != nil {
717 t.Errorf("capacity.level = %v, want nil with no boxes to vote", *f.Capacity.Level)
718 }
719 if f.Instances["total"] == nil || *f.Instances["total"] != 0 {
720 t.Errorf("instances.total = %v, want 0", f.Instances["total"])
721 }
722 if len(f.Apps) != 0 || f.AppsUnlisted != 0 || len(f.Services) != 0 {
723 t.Errorf("apps/services must be empty with no routes, got apps=%+v unlisted=%d services=%+v", f.Apps, f.AppsUnlisted, f.Services)
724 }
725}
726
727// TestFleetSkipsCorruptSnapshot proves a corrupt status/boxes/*.json is a
728// logged skip, never a fatal error — one box's bad write must not take the
729// whole fleet page down.
730func TestFleetSkipsCorruptSnapshot(t *testing.T) {
731 s := teststore(t)
732 now := time.Now()
733 if err := status.WriteBox(context.Background(), s, boxFixture("good", "plenty", now)); err != nil {
734 t.Fatal(err)
735 }
736 if _, err := s.Put(context.Background(), "status/boxes/broken.json", []byte("{not json"), ""); err != nil {
737 t.Fatal(err)
738 }
739 f, err := status.BuildFleet(context.Background(), s, nil, now, 5*time.Minute)
740 if err != nil {
741 t.Fatalf("BuildFleet must not fail on a corrupt snapshot: %v", err)
742 }
743 if len(f.Boxes) != 1 || f.Boxes[0].Label != "good" {
744 t.Errorf("boxes = %+v, want only the good one (corrupt skipped)", f.Boxes)
745 }
746}
747
748// TestFleetLoadBoxesCapsKeyCount is I3 (final customer-fleets review):
749// status/boxes/ is customer-writable in the fleet case (via
750// CARLOS_STATUS_PREFIX), so loadBoxes must bound how many keys it will ever
751// process from one listing rather than trusting an untrusted prefix's size.
752// Seed one more box than the cap and confirm only the cap's worth land in
753// the merged Fleet.
754func TestFleetLoadBoxesCapsKeyCount(t *testing.T) {
755 s := teststore(t)
756 now := time.Now()
757 const keyCap = 200
758 for i := 0; i < keyCap+1; i++ {
759 label := fmt.Sprintf("box-%04d", i)
760 if err := status.WriteBox(context.Background(), s, boxFixture(label, "plenty", now)); err != nil {
761 t.Fatal(err)
762 }
763 }
764 f, err := status.BuildFleet(context.Background(), s, nil, now, 5*time.Minute)
765 if err != nil {
766 t.Fatalf("BuildFleet must not fail on an oversized key listing: %v", err)
767 }
768 if len(f.Boxes) != keyCap {
769 t.Fatalf("boxes = %d, want exactly the %d-key cap (seeded %d)", len(f.Boxes), keyCap, keyCap+1)
770 }
771}
772
773// TestFleetLoadBoxesSkipsOversizedBody is I3's other half: a single huge
774// object under status/boxes/ (a hostile or misbehaving box parking a large
775// body on its own customer-writable prefix) must be skipped, logged, and
776// must not prevent a sibling box's snapshot from loading.
777func TestFleetLoadBoxesSkipsOversizedBody(t *testing.T) {
778 s := teststore(t)
779 now := time.Now()
780 if err := status.WriteBox(context.Background(), s, boxFixture("good", "plenty", now)); err != nil {
781 t.Fatal(err)
782 }
783 huge := make([]byte, (1<<20)+1) // one byte over the 1MB cap
784 for i := range huge {
785 huge[i] = ' '
786 }
787 if _, err := s.Put(context.Background(), "status/boxes/huge.json", huge, ""); err != nil {
788 t.Fatal(err)
789 }
790 f, err := status.BuildFleet(context.Background(), s, nil, now, 5*time.Minute)
791 if err != nil {
792 t.Fatalf("BuildFleet must not fail on an oversized body: %v", err)
793 }
794 if len(f.Boxes) != 1 || f.Boxes[0].Label != "good" {
795 t.Errorf("boxes = %+v, want only the good one (oversized body skipped)", f.Boxes)
796 }
797}
798
799// TestPublishFleetWritesAndOverwrites mirrors WriteBox's own overwrite test:
800// PublishFleet lands status/fleet.json on every publish pass (every 5
801// minutes, per the plan), so a second call must overwrite cleanly rather
802// than failing with ErrCASConflict against release.Store's create-only Put.
803func TestPublishFleetWritesAndOverwrites(t *testing.T) {
804 s := teststore(t)
805 ctx := context.Background()
806
807 first := &status.Fleet{GeneratedAt: time.Unix(1000, 0)}
808 if err := status.PublishFleet(ctx, s, first); err != nil {
809 t.Fatalf("first PublishFleet: %v", err)
810 }
811 second := &status.Fleet{GeneratedAt: time.Unix(2000, 0)}
812 if err := status.PublishFleet(ctx, s, second); err != nil {
813 t.Fatalf("second PublishFleet (must overwrite, not conflict): %v", err)
814 }
815
816 got, _, err := s.Get(ctx, "status/fleet.json")
817 if err != nil {
818 t.Fatalf("Get status/fleet.json: %v", err)
819 }
820 var round status.Fleet
821 if err := json.Unmarshal(got, &round); err != nil {
822 t.Fatalf("unmarshal published fleet: %v", err)
823 }
824 if !round.GeneratedAt.Equal(second.GeneratedAt) {
825 t.Errorf("stored fleet has GeneratedAt %v, want second publish's %v", round.GeneratedAt, second.GeneratedAt)
826 }
827}
828
829// ---- Task 4: embedded viewer page -----------------------------------------
830
831// TestPublishWritesPageAssets proves PublishFleet ships the whole viewer —
832// fleet.json alongside the embedded index.html and status.css — and that the
833// page is same-origin only: no absolute http(s):// URL and no <script src>
834// anywhere in the shipped markup. (Simplified from the plan's step-1 text,
835// per the operator's ambiguity resolution: the footer's incident-fallback
836// URL is injected as text by inline JS from `location` at view time, never
837// baked into the HTML as a hardcoded absolute href, so a flat "no
838// http(s):// substring, no <script src" check is the honest same-origin
839// assertion here — no carve-out needed for a rel="me" link that doesn't
840// exist.)
841func TestPublishWritesPageAssets(t *testing.T) {
842 s := teststore(t)
843 f, err := status.BuildFleet(context.Background(), s, nil, time.Now(), 5*time.Minute)
844 if err != nil {
845 t.Fatal(err)
846 }
847 if err := status.PublishFleet(context.Background(), s, f); err != nil {
848 t.Fatal(err)
849 }
850 for _, key := range []string{"status/fleet.json", "status/index.html", "status/status.css"} {
851 if _, _, err := s.Get(context.Background(), key); err != nil {
852 t.Errorf("%s not published: %v", key, err)
853 }
854 }
855 html, _, err := s.Get(context.Background(), "status/index.html")
856 if err != nil {
857 t.Fatalf("Get status/index.html: %v", err)
858 }
859 // The last three entries catch protocol-relative references
860 // (src="//cdn...", href="//cdn...", url(//cdn...)) — another way to
861 // reach off-origin without ever spelling "http". A bare "//" substring
862 // isn't checked: this file's inline script is full of legitimate "//
863 // comment" lines, so the assertion targets the actual reference
864 // positions (an attribute/url() opening onto "//") instead.
865 for _, banned := range []string{"http://", "https://", "<script src", `src="//`, `href="//`, `url(//`} {
866 if strings.Contains(string(html), banned) {
867 t.Errorf("page must be same-origin only; found %q in index.html", banned)
868 }
869 }
870}
871
872// TestWriteBoxReturnsErrorOnDoubleCASConflict proves WriteBox does NOT retry
873// a second time: if the retried Put also loses the race, that error must
874// reach the caller rather than being swallowed or retried forever.
875func TestWriteBoxReturnsErrorOnDoubleCASConflict(t *testing.T) {
876 dir := t.TempDir()
877 real := release.NewDirStore(dir)
878 flaky := &flakyPutStore{Store: real, failCount: 2}
879 b := status.CollectBox(nil, status.Probes{MachineID: "m"}, time.Now())
880
881 err := status.WriteBox(context.Background(), flaky, b)
882 if err == nil {
883 t.Fatal("WriteBox: want an error after two consecutive CAS conflicts, got nil")
884 }
885 if !errors.Is(err, release.ErrCASConflict) {
886 t.Errorf("WriteBox error = %v, want it to wrap release.ErrCASConflict", err)
887 }
888 if flaky.putCalls != 2 {
889 t.Errorf("Put called %d times, want exactly 2 (no third attempt)", flaky.putCalls)
890 }
891}
892
893// ---- Task 6: dry-run self-check --------------------------------------------
894
895// TestDryRunSelfChecks is the plan's Step-1 test verbatim: DryRun must run
896// the real CollectBox -> WriteBox -> BuildFleet -> PublishFleet path over a
897// fixture registry stuffed with needles (hostnames, a machine id, mid-band
898// fractions), then read every published object back and prove none of those
899// needles survived — the self-hoster's "what would I publish?" check, run
900// before ever pointing this binary at a real bucket.
901func TestDryRunSelfChecks(t *testing.T) {
902 var buf bytes.Buffer
903 if err := status.DryRun(t.TempDir(), &buf); err != nil {
904 t.Fatalf("dry run: %v\n%s", err, buf.String())
905 }
906 if !strings.Contains(buf.String(), "no leaks") {
907 t.Errorf("dry run must attest the self-check ran: %s", buf.String())
908 }
909}
910
911// ---- Pipelines Task 5: adoption health -------------------------------------
912
913// TestAdoptionUnknownIsANull pins the honest-unknown half of the pipelines
914// spec's §5 ("adoption must actually run"): a box whose probe never managed to
915// ask systemd about the adoption timer publishes null, exactly like every
916// other unmeasured quantity in this package. The failure this guards against
917// is not a wrong reading, it is a REASSURING one — a default true would make
918// every box that can't answer look like a box that is adopting fine, which is
919// precisely the silence that let carlos-adopt.timer sit enabled-but-never-
920// started on the flagship for weeks with nothing on any page to show for it.
921func TestAdoptionUnknownIsANull(t *testing.T) {
922 b := status.CollectBox(nil, status.Probes{MachineID: "m"}, time.Now()) // nil AdoptRunning
923 if b.AdoptionRunning != nil {
924 t.Errorf("adoption_running = %v, want null — an unasked question is not a healthy answer", *b.AdoptionRunning)
925 }
926 for _, want := range []bool{true, false} {
927 got := status.CollectBox(nil, status.Probes{MachineID: "m", AdoptRunning: &want}, time.Now()).AdoptionRunning
928 if got == nil || *got != want {
929 t.Errorf("AdoptRunning=%v: adoption_running = %v, want %v", want, got, want)
930 }
931 }
932}
933
934// TestConfigPathRunningTravelsPerBox pins the issue #44 probe's whole chain:
935// whether this box's config-sync trigger unit (carlos-adopt.path) is ACTIVE
936// travels from Probes onto the box snapshot and through BuildFleet onto the
937// box's public row — the same journey adoption_running makes, and for the
938// same reason. An inactive path unit is a box that silently fell back to the
939// hourly config floor, which is exactly the class of quiet degradation this
940// package exists to make visible. Unknown stays null: an unasked question is
941// not a reassuring answer here either.
942func TestConfigPathRunningTravelsPerBox(t *testing.T) {
943 if b := status.CollectBox(nil, status.Probes{MachineID: "m"}, time.Now()); b.ConfigPathRunning != nil {
944 t.Errorf("config_path_running = %v, want null when the probe never answered", *b.ConfigPathRunning)
945 }
946 for _, want := range []bool{true, false} {
947 got := status.CollectBox(nil, status.Probes{MachineID: "m", ConfigPathRunning: &want}, time.Now()).ConfigPathRunning
948 if got == nil || *got != want {
949 t.Fatalf("ConfigPathRunning=%v: box field = %v, want %v", want, got, want)
950 }
951 }
952
953 s := teststore(t)
954 now := time.Now()
955 no := false
956 b := status.CollectBox(nil, status.Probes{MachineID: "m", ConfigPathRunning: &no}, now)
957 if err := status.WriteBox(context.Background(), s, b); err != nil {
958 t.Fatal(err)
959 }
960 f, err := status.BuildFleet(context.Background(), s, nil, now, 5*time.Minute)
961 if err != nil {
962 t.Fatal(err)
963 }
964 if len(f.Boxes) != 1 {
965 t.Fatalf("fleet boxes = %d, want 1", len(f.Boxes))
966 }
967 if got := f.Boxes[0].ConfigPathRunning; got == nil || *got {
968 t.Fatalf("fleet box config_path_running = %v, want false — knowing WHICH box lost its trigger is the whole value", got)
969 }
970}
971
972// TestConfigPathRunningIsWireAdditive pins the other half: a snapshot written
973// before this field existed (no "config_path_running" key at all) must still
974// parse, and must read as unknown rather than as a healthy false. Live
975// buckets hold exactly those objects today.
976func TestConfigPathRunningIsWireAdditive(t *testing.T) {
977 var b status.Box
978 old := `{"generated_at":"2026-08-04T00:00:00Z","label":"box-old","routes":{"static":0,"resident":0,"hibernating":0,"awake":null,"asleep":null},"load":null,"capacity":null,"replication":{"healthy":null,"lag_seconds":null},"adoption_running":true,"expired_canaries":null}`
979 if err := json.Unmarshal([]byte(old), &b); err != nil {
980 t.Fatalf("a pre-#44 snapshot must still parse: %v", err)
981 }
982 if b.ConfigPathRunning != nil {
983 t.Fatalf("config_path_running = %v, want null for a snapshot written before the field existed", *b.ConfigPathRunning)
984 }
985}
986
987// TestRestartPathRunningTravelsPerBox is TestConfigPathRunningTravelsPerBox's
988// issue #113 twin, and it exists for a sharper reason than symmetry: with
989// carlos-restart.path unarmed, a member runs `carlos restart` at a wedged
990// instance, is told "requested", and waits for a cycle that will not come until
991// the hourly pass. Nothing inside the agent can see that — it writes its marker
992// either way — so this reading is the only thing that can. Unknown stays null.
993func TestRestartPathRunningTravelsPerBox(t *testing.T) {
994 if b := status.CollectBox(nil, status.Probes{MachineID: "m"}, time.Now()); b.RestartPathRunning != nil {
995 t.Errorf("restart_path_running = %v, want null when the probe never answered", *b.RestartPathRunning)
996 }
997 for _, want := range []bool{true, false} {
998 got := status.CollectBox(nil, status.Probes{MachineID: "m", RestartPathRunning: &want}, time.Now()).RestartPathRunning
999 if got == nil || *got != want {
1000 t.Fatalf("RestartPathRunning=%v: box field = %v, want %v", want, got, want)
1001 }
1002 }
1003
1004 s := teststore(t)
1005 now := time.Now()
1006 no := false
1007 b := status.CollectBox(nil, status.Probes{MachineID: "m", RestartPathRunning: &no}, now)
1008 if err := status.WriteBox(context.Background(), s, b); err != nil {
1009 t.Fatal(err)
1010 }
1011 f, err := status.BuildFleet(context.Background(), s, nil, now, 5*time.Minute)
1012 if err != nil {
1013 t.Fatal(err)
1014 }
1015 if len(f.Boxes) != 1 {
1016 t.Fatalf("fleet boxes = %d, want 1", len(f.Boxes))
1017 }
1018 if got := f.Boxes[0].RestartPathRunning; got == nil || *got {
1019 t.Fatalf("fleet box restart_path_running = %v, want false — knowing WHICH box cannot cycle a wedged instance is the whole value", got)
1020 }
1021
1022 // Wire-additive, like every field before it: a snapshot written before
1023 // issue #113 has no such key and must read as unknown, not a healthy false.
1024 var old status.Box
1025 raw := `{"generated_at":"2026-08-04T00:00:00Z","label":"box-old","routes":{"static":0,"resident":0,"hibernating":0,"awake":null,"asleep":null},"load":null,"capacity":null,"replication":{"healthy":null,"lag_seconds":null},"adoption_running":true,"config_path_running":true,"expired_canaries":null}`
1026 if err := json.Unmarshal([]byte(raw), &old); err != nil {
1027 t.Fatalf("a pre-#113 snapshot must still parse: %v", err)
1028 }
1029 if old.RestartPathRunning != nil {
1030 t.Fatalf("restart_path_running = %v, want null for a snapshot written before the field existed", *old.RestartPathRunning)
1031 }
1032}
1033
1034// TestFleetAdoptionWorstOf covers the aggregate: a box that is KNOWN not to be
1035// adopting is a defect the fleet number must never hide, so one false outranks
1036// any number of trues; an unknown box can't be summed into a confident true,
1037// so it nulls the aggregate instead; and a stale box doesn't vote at all
1038// (staleness means "we no longer know", the same rule worstReplication uses).
1039func TestFleetAdoptionWorstOf(t *testing.T) {
1040 yes, no := true, false
1041 cases := []struct {
1042 name string
1043 boxes map[string]*bool // machine id -> AdoptRunning
1044 stale map[string]bool // machine id -> written 16 minutes ago
1045 want *bool
1046 }{
1047 {"all running", map[string]*bool{"a": &yes, "b": &yes}, nil, &yes},
1048 {"one dead timer wins", map[string]*bool{"a": &yes, "b": &no}, nil, &no},
1049 {"one unknown blocks a cheerful true", map[string]*bool{"a": &yes, "b": nil}, nil, nil},
1050 {"a known defect beats an unknown", map[string]*bool{"a": nil, "b": &no}, nil, &no},
1051 {"stale boxes do not vote", map[string]*bool{"a": &yes, "b": &no}, map[string]bool{"b": true}, &yes},
1052 {"no boxes at all", nil, nil, nil},
1053 }
1054 for _, c := range cases {
1055 t.Run(c.name, func(t *testing.T) {
1056 s := teststore(t)
1057 now := time.Now()
1058 for id, adopt := range c.boxes {
1059 at := now
1060 if c.stale[id] {
1061 at = now.Add(-16 * time.Minute) // > 3 x the 5-minute period
1062 }
1063 b := status.CollectBox(nil, status.Probes{MachineID: id, AdoptRunning: adopt}, at)
1064 if err := status.WriteBox(context.Background(), s, b); err != nil {
1065 t.Fatal(err)
1066 }
1067 }
1068 f, err := status.BuildFleet(context.Background(), s, nil, now, 5*time.Minute)
1069 if err != nil {
1070 t.Fatal(err)
1071 }
1072 switch {
1073 case c.want == nil && f.AdoptionRunning != nil:
1074 t.Errorf("fleet adoption_running = %v, want null", *f.AdoptionRunning)
1075 case c.want != nil && f.AdoptionRunning == nil:
1076 t.Errorf("fleet adoption_running = null, want %v", *c.want)
1077 case c.want != nil && *f.AdoptionRunning != *c.want:
1078 t.Errorf("fleet adoption_running = %v, want %v", *f.AdoptionRunning, *c.want)
1079 }
1080 })
1081 }
1082}
1083
1084// TestFleetBoxCarriesAdoption keeps the per-box row honest too: the fleet-wide
1085// number says SOMETHING is not adopting, the boxes table says which label —
1086// without it, an operator reading "not running" on a two-box fleet has to go
1087// SSH into both to find out where the dead timer is.
1088// ---- Pipelines Task: canary TTL (spec §5 "Canaries expire") ---------------
1089
1090// TestExpiredCanariesUnknownIsANull mirrors TestAdoptionUnknownIsANull: a box
1091// whose probe never resolved its canary channel pointers (e.g. no deployment
1092// store configured) publishes null, exactly like every other unmeasured
1093// quantity here. A default 0 would read as "no expired canaries on this
1094// box" when the honest answer is "this box couldn't check" — precisely the
1095// reassuring-wrong-answer failure the rest of this package refuses to make.
1096func TestExpiredCanariesUnknownIsANull(t *testing.T) {
1097 b := status.CollectBox(nil, status.Probes{MachineID: "m"}, time.Now()) // nil ExpiredCanaries
1098 if b.ExpiredCanaries != nil {
1099 t.Errorf("expired_canaries = %v, want null — an unasked question is not a healthy answer", *b.ExpiredCanaries)
1100 }
1101 for _, want := range []int{0, 3} {
1102 got := status.CollectBox(nil, status.Probes{MachineID: "m", ExpiredCanaries: &want}, time.Now()).ExpiredCanaries
1103 if got == nil || *got != want {
1104 t.Errorf("ExpiredCanaries=%d: expired_canaries = %v, want %d", want, got, want)
1105 }
1106 }
1107}
1108
1109// TestFleetExpiredCanariesSums proves the fleet-wide count is the sum across
1110// boxes when every box's own count is known — the aggregate an operator
1111// actually wants ("how many expired canaries are running anywhere"), not a
1112// per-box worst-of.
1113func TestFleetExpiredCanariesSums(t *testing.T) {
1114 s := teststore(t)
1115 now := time.Now()
1116 two, one := 2, 1
1117 for _, b := range []status.Box{
1118 status.CollectBox(nil, status.Probes{MachineID: "a", ExpiredCanaries: &two}, now),
1119 status.CollectBox(nil, status.Probes{MachineID: "b", ExpiredCanaries: &one}, now),
1120 } {
1121 if err := status.WriteBox(context.Background(), s, b); err != nil {
1122 t.Fatal(err)
1123 }
1124 }
1125 f, err := status.BuildFleet(context.Background(), s, nil, now, 5*time.Minute)
1126 if err != nil {
1127 t.Fatal(err)
1128 }
1129 if f.ExpiredCanaries == nil || *f.ExpiredCanaries != 3 {
1130 t.Errorf("fleet expired_canaries = %v, want 3", f.ExpiredCanaries)
1131 }
1132}
1133
1134// TestFleetExpiredCanariesNullPropagates: one box that couldn't check nulls
1135// the fleet-wide sum, the same null-propagation rule sumInstances applies to
1136// Awake/Asleep — a partial sum would read as more certain than it is.
1137func TestFleetExpiredCanariesNullPropagates(t *testing.T) {
1138 s := teststore(t)
1139 now := time.Now()
1140 two := 2
1141 known := status.CollectBox(nil, status.Probes{MachineID: "known", ExpiredCanaries: &two}, now)
1142 unknown := status.CollectBox(nil, status.Probes{MachineID: "unknown"}, now) // nil probe
1143 for _, b := range []status.Box{known, unknown} {
1144 if err := status.WriteBox(context.Background(), s, b); err != nil {
1145 t.Fatal(err)
1146 }
1147 }
1148 f, err := status.BuildFleet(context.Background(), s, nil, now, 5*time.Minute)
1149 if err != nil {
1150 t.Fatal(err)
1151 }
1152 if f.ExpiredCanaries != nil {
1153 t.Errorf("fleet expired_canaries = %v, want nil (one box could not check)", *f.ExpiredCanaries)
1154 }
1155}
1156
1157// TestFleetEmptyExpiredCanariesIsZero: no boxes at all is not "unknown", the
1158// same rule TestFleetEmptyIsValid pins for instances — there is nothing
1159// unknown about an empty fleet.
1160func TestFleetEmptyExpiredCanariesIsZero(t *testing.T) {
1161 s := teststore(t)
1162 f, err := status.BuildFleet(context.Background(), s, nil, time.Now(), 5*time.Minute)
1163 if err != nil {
1164 t.Fatal(err)
1165 }
1166 if f.ExpiredCanaries == nil || *f.ExpiredCanaries != 0 {
1167 t.Errorf("expired_canaries = %v, want 0 with no boxes", f.ExpiredCanaries)
1168 }
1169}
1170
1171// TestFleetBoxCarriesExpiredCanaries keeps the per-box row honest too — the
1172// fleet-wide number says something is expired somewhere, the boxes table
1173// says which box, the same reasoning TestFleetBoxCarriesAdoption pins for
1174// the adoption-timer reading.
1175func TestFleetBoxCarriesExpiredCanaries(t *testing.T) {
1176 s := teststore(t)
1177 now := time.Now()
1178 three := 3
1179 if err := status.WriteBox(context.Background(), s,
1180 status.CollectBox(nil, status.Probes{MachineID: "leaky", ExpiredCanaries: &three}, now)); err != nil {
1181 t.Fatal(err)
1182 }
1183 f, err := status.BuildFleet(context.Background(), s, nil, now, 5*time.Minute)
1184 if err != nil {
1185 t.Fatal(err)
1186 }
1187 if len(f.Boxes) != 1 {
1188 t.Fatalf("boxes = %d, want 1", len(f.Boxes))
1189 }
1190 if f.Boxes[0].ExpiredCanaries == nil || *f.Boxes[0].ExpiredCanaries != 3 {
1191 t.Errorf("boxes[0].expired_canaries = %v, want 3", f.Boxes[0].ExpiredCanaries)
1192 }
1193}
1194
1195func TestFleetBoxCarriesAdoption(t *testing.T) {
1196 s := teststore(t)
1197 now := time.Now()
1198 no := false
1199 if err := status.WriteBox(context.Background(), s,
1200 status.CollectBox(nil, status.Probes{MachineID: "dead", AdoptRunning: &no}, now)); err != nil {
1201 t.Fatal(err)
1202 }
1203 f, err := status.BuildFleet(context.Background(), s, nil, now, 5*time.Minute)
1204 if err != nil {
1205 t.Fatal(err)
1206 }
1207 if len(f.Boxes) != 1 {
1208 t.Fatalf("boxes = %d, want 1", len(f.Boxes))
1209 }
1210 if f.Boxes[0].AdoptionRunning == nil || *f.Boxes[0].AdoptionRunning {
1211 t.Errorf("boxes[0].adoption_running = %v, want false — the row must name the box with the dead timer",
1212 f.Boxes[0].AdoptionRunning)
1213 }
1214}
1215
1216// ---- Task 7: the exported box loader and the one staleness rule -------------
1217
1218// TestLoadBoxesAndStaleness pins the two things the console's private host
1219// detail and `carlos status -live` both read the bucket through: LoadBoxes
1220// returns every snapshot WITH its private Metrics block intact (the merged
1221// public feed drops it — this loader must not), and BoxStale answers the same
1222// "older than 3 periods" question BuildFleet's own boxes table answers, so the
1223// two renderings can never disagree about which host is dark.
1224// oneFailingGet fails the read of one key and serves every other.
1225type oneFailingGet struct {
1226 release.Store
1227 key string
1228}
1229
1230func (f oneFailingGet) Get(ctx context.Context, key string) ([]byte, string, error) {
1231 if key == f.key {
1232 return nil, "", errors.New("read timeout")
1233 }
1234 return f.Store.Get(ctx, key)
1235}
1236
1237// LoadBoxesComplete returns what LoadBoxes returns and, separately, whether
1238// every listed snapshot was read: a failed GET or a corrupt body is a
1239// logged skip for LoadBoxes and an INCOMPLETE read here. The console's
1240// automatic region placement fails closed on it (nearest-region re-review
1241// r1): a box whose snapshot could not be read has unknown liveness, which
1242// is not stale.
1243func TestLoadBoxesCompleteReportsSkippedReads(t *testing.T) {
1244 ctx := context.Background()
1245 s := release.NewDirStore(t.TempDir())
1246 for _, label := range []string{"alpha", "beta"} {
1247 if err := status.WriteBox(ctx, s, status.Box{Label: label, GeneratedAt: time.Now()}); err != nil {
1248 t.Fatal(err)
1249 }
1250 }
1251 boxes, complete, err := status.LoadBoxesComplete(ctx, s)
1252 if err != nil || !complete || len(boxes) != 2 {
1253 t.Fatalf("clean store: %d boxes, complete %v, err %v; want 2, true, nil", len(boxes), complete, err)
1254 }
1255 boxes, complete, err = status.LoadBoxesComplete(ctx, oneFailingGet{Store: s, key: "status/boxes/beta.json"})
1256 if err != nil || complete || len(boxes) != 1 {
1257 t.Fatalf("one failed GET: %d boxes, complete %v, err %v; want 1 (alpha), false, nil", len(boxes), complete, err)
1258 }
1259 plain, err := status.LoadBoxes(ctx, oneFailingGet{Store: s, key: "status/boxes/beta.json"})
1260 if err != nil || len(plain) != 1 {
1261 t.Fatalf("LoadBoxes under the same failure: %d boxes, err %v; want the unchanged logged-skip behaviour", len(plain), err)
1262 }
1263 _, etag, err := s.Get(ctx, "status/boxes/beta.json")
1264 if err != nil {
1265 t.Fatal(err)
1266 }
1267 if _, err := s.Put(ctx, "status/boxes/beta.json", []byte("{not json"), etag); err != nil {
1268 t.Fatal(err)
1269 }
1270 if boxes, complete, err := status.LoadBoxesComplete(ctx, s); err != nil || complete || len(boxes) != 1 {
1271 t.Fatalf("corrupt body: %d boxes, complete %v, err %v; want 1, false, nil", len(boxes), complete, err)
1272 }
1273 if _, complete, err := status.LoadBoxesComplete(ctx, release.NewDirStore(t.TempDir())); err != nil || !complete {
1274 t.Fatalf("empty prefix: complete %v, err %v; want true, nil", complete, err)
1275 }
1276}
1277
1278func TestLoadBoxesAndStaleness(t *testing.T) {
1279 s := teststore(t)
1280 ctx := context.Background()
1281 now := time.Now()
1282 fresh := status.Box{GeneratedAt: now, Label: "ada-stargazing",
1283 Metrics: &status.BoxMetrics{Release: "abc1234", Tunnel: "connected"}}
1284 dark := status.Box{GeneratedAt: now.Add(-time.Hour), Label: "grace-kayaking"}
1285 for _, b := range []status.Box{fresh, dark} {
1286 if err := status.WriteBox(ctx, s, b); err != nil {
1287 t.Fatal(err)
1288 }
1289 }
1290 boxes, err := status.LoadBoxes(ctx, s)
1291 if err != nil || len(boxes) != 2 {
1292 t.Fatalf("LoadBoxes: %v, %d boxes", err, len(boxes))
1293 }
1294 for _, b := range boxes {
1295 wantStale := b.Label == "grace-kayaking"
1296 if got := status.BoxStale(b, now, 5*time.Minute); got != wantStale {
1297 t.Fatalf("%s: stale = %v, want %v", b.Label, got, wantStale)
1298 }
1299 if b.Label == "ada-stargazing" {
1300 if b.Metrics == nil || b.Metrics.Release != "abc1234" || b.Metrics.Tunnel != "connected" {
1301 t.Fatalf("LoadBoxes dropped the private metrics block: %+v", b.Metrics)
1302 }
1303 }
1304 }
1305}
1306
1307// TestBoxStaleMatchesFleetBoxStale is the anti-drift assertion: whatever
1308// BoxStale says about a box must be exactly what BuildFleet publishes in that
1309// box's FleetBox.Stale — one rule, one constant, two renderings.
1310func TestBoxStaleMatchesFleetBoxStale(t *testing.T) {
1311 s := teststore(t)
1312 ctx := context.Background()
1313 now := time.Now()
1314 const period = 5 * time.Minute
1315 for _, b := range []status.Box{
1316 boxFixture("just-now", "plenty", now),
1317 boxFixture("two-periods", "plenty", now.Add(-2*period)),
1318 boxFixture("exactly-three", "plenty", now.Add(-3*period)),
1319 boxFixture("long-dark", "plenty", now.Add(-time.Hour)),
1320 } {
1321 if err := status.WriteBox(ctx, s, b); err != nil {
1322 t.Fatal(err)
1323 }
1324 }
1325 f, err := status.BuildFleet(ctx, s, nil, now, period)
1326 if err != nil {
1327 t.Fatal(err)
1328 }
1329 boxes, err := status.LoadBoxes(ctx, s)
1330 if err != nil {
1331 t.Fatal(err)
1332 }
1333 byLabel := map[string]bool{}
1334 for _, fb := range f.Boxes {
1335 byLabel[fb.Label] = fb.Stale
1336 }
1337 for _, b := range boxes {
1338 if got, want := status.BoxStale(b, now, period), byLabel[b.Label]; got != want {
1339 t.Errorf("%s: BoxStale = %v, FleetBox.Stale = %v — the rule has drifted", b.Label, got, want)
1340 }
1341 }
1342}
1343
1344// A first-route claim materializes a ROUTE, not an alias. Without its stamps
1345// on the wire the console can never take it past "pending" — which is what
1346// `carlos deploy -kind static -host <site>` has always shown.
1347func TestCollectBoxCarriesRouteDNSStampsForNonPlatformRoutes(t *testing.T) {
1348 routes := []registry.Route{{
1349 Host: "story.customer.com", Status: "ready", App: "site",
1350 DNSStatus: "pointing", DNSCheckedAt: 1756300000, CertExpiresAt: 1764000000,
1351 }}
1352 box := status.CollectBox(routes, status.Probes{}, time.Unix(1756300060, 0))
1353
1354 got, ok := box.Metrics.Aliases["story.customer.com"]
1355 if !ok {
1356 t.Fatal("a claim-backed route must publish a reading")
1357 }
1358 if got.DNSStatus != "pointing" || got.CertExpiresAt != 1764000000 {
1359 t.Fatalf("reading = %+v", got)
1360 }
1361 if got.Route != "story.customer.com" {
1362 t.Fatalf("Route = %q, want the host as its own primary (an empty claim route means exactly that)", got.Route)
1363 }
1364 if got.DNSCheckedAt != 1756300000 {
1365 t.Fatalf("DNSCheckedAt = %d, want the stamp's own age carried", got.DNSCheckedAt)
1366 }
1367}
1368
1369// Platform routes are the deployment's own hosts, not custom domains, and
1370// they already have RouteVersions. Keep the map to what the domains surface
1371// actually joins against.
1372func TestCollectBoxOmitsPlatformRoutesFromReadings(t *testing.T) {
1373 routes := []registry.Route{
1374 {Host: "app.bdk.oncarlos.com", Status: "ready", Platform: true, DNSStatus: "pointing"},
1375 // A custom domain alongside it, so Metrics attaches at all: Box.Metrics
1376 // is a POINTER and stays nil when nothing is known (snapshot.go:485-510).
1377 // Without this row the assertion below would nil-panic rather than
1378 // testing anything.
1379 {Host: "shop.customer.com", Status: "ready", DNSStatus: "pointing", DNSCheckedAt: 1756300000},
1380 }
1381 box := status.CollectBox(routes, status.Probes{}, time.Unix(1756300060, 0))
1382 if box.Metrics == nil {
1383 t.Fatal("Metrics must attach once a custom domain has a reading")
1384 }
1385 if _, ok := box.Metrics.Aliases["app.bdk.oncarlos.com"]; ok {
1386 t.Fatal("a platform route must not appear in the hostname readings")
1387 }
1388 if _, ok := box.Metrics.Aliases["shop.customer.com"]; !ok {
1389 t.Fatal("the custom domain must appear")
1390 }
1391}
1392
1393// TestCollectBoxCarriesRouteDBBytes: every non-platform, non-static route
1394// with a DBPath is sized through Probes.DBBytes into Metrics.RouteDBBytes. A
1395// route the probe cannot size is absent rather than 0, and the hostname never
1396// reaches the public fleet.json.
1397func TestCollectBoxCarriesRouteDBBytes(t *testing.T) {
1398 routes := []registry.Route{
1399 {Host: "sized.test", Kind: "instance", DBPath: "/data/sized.test.db"},
1400 {Host: "missing.test", Kind: "instance", DBPath: "/data/missing.test.db"},
1401 {Host: "nodb.test", Kind: "instance"},
1402 {Host: "static.test", Kind: "static", DBPath: "/data/static.test.db"},
1403 {Host: "platform.test", Kind: "instance", DBPath: "/data/sized.test.db", Platform: true},
1404 }
1405 var asked []string
1406 probe := func(p string) *int64 {
1407 asked = append(asked, p)
1408 if p == "/data/sized.test.db" {
1409 n := int64(4096)
1410 return &n
1411 }
1412 return nil
1413 }
1414 b := status.CollectBox(routes, status.Probes{MachineID: "m", DBBytes: probe}, time.Now())
1415 if b.Metrics == nil {
1416 t.Fatal("metrics block missing — RouteDBBytes should have attached it")
1417 }
1418 if len(b.Metrics.RouteDBBytes) != 1 || b.Metrics.RouteDBBytes["sized.test"] != 4096 {
1419 t.Fatalf("route_db_bytes = %v, want exactly {sized.test: 4096}", b.Metrics.RouteDBBytes)
1420 }
1421 if strings.Join(asked, ",") != "/data/sized.test.db,/data/missing.test.db" {
1422 t.Errorf("probed %v, want only the two instance routes with a DBPath", asked)
1423 }
1424
1425 s := teststore(t)
1426 ctx := context.Background()
1427 if err := status.WriteBox(ctx, s, b); err != nil {
1428 t.Fatal(err)
1429 }
1430 f, err := status.BuildFleet(ctx, s, routes, time.Now(), 5*time.Minute)
1431 if err != nil {
1432 t.Fatal(err)
1433 }
1434 fj, err := json.Marshal(f)
1435 if err != nil {
1436 t.Fatal(err)
1437 }
1438 if strings.Contains(string(fj), "sized.test") {
1439 t.Errorf("fleet.json leaked a route_db_bytes hostname: %s", fj)
1440 }
1441
1442 // No probe at all: nothing sized, and no Metrics block from this alone.
1443 if b := status.CollectBox(routes[:1], status.Probes{MachineID: "m"}, time.Now()); b.Metrics != nil {
1444 t.Errorf("metrics = %+v with no DBBytes probe, want absent", b.Metrics)
1445 }
1446}
1447
1448// TestCollectBoxCarriesConfigRefusals (amadan #213): a box that refuses to
1449// take an instance because it cannot provide that instance's config says so
1450// in its private Metrics block, where an operator can see which hosts and
1451// why. The hostnames never reach the public fleet.json, and a box with no
1452// refusal carries no key at all.
1453func TestCollectBoxCarriesConfigRefusals(t *testing.T) {
1454 since := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC)
1455 refusals := []status.ConfigRefusal{
1456 {Host: "refused-secret.example.com", Reason: "no_key", Since: since, Last: since.Add(time.Minute), Count: 3},
1457 }
1458 routes := []registry.Route{{Host: "refused-secret.example.com", Kind: "instance", Hibernate: true}}
1459 b := status.CollectBox(routes, status.Probes{MachineID: "m", ConfigRefusals: refusals, ConfigRefusalsTotal: 5}, time.Now())
1460 if b.Metrics == nil {
1461 t.Fatal("metrics block missing: a refusal alone should attach it")
1462 }
1463 if got := b.Metrics.ConfigRefusals; len(got) != 1 || got[0] != refusals[0] {
1464 t.Fatalf("config_refusals = %+v, want %+v", got, refusals)
1465 }
1466 if b.Metrics.ConfigRefusalsTotal != 5 {
1467 t.Errorf("config_refusals_total = %d, want 5", b.Metrics.ConfigRefusalsTotal)
1468 }
1469
1470 s := teststore(t)
1471 ctx := context.Background()
1472 if err := status.WriteBox(ctx, s, b); err != nil {
1473 t.Fatal(err)
1474 }
1475 f, err := status.BuildFleet(ctx, s, routes, time.Now(), 5*time.Minute)
1476 if err != nil {
1477 t.Fatal(err)
1478 }
1479 fj, err := json.Marshal(f)
1480 if err != nil {
1481 t.Fatal(err)
1482 }
1483 for _, needle := range []string{"refused-secret.example.com", "no_key", "config_refusals"} {
1484 if strings.Contains(string(fj), needle) {
1485 t.Errorf("fleet.json leaked %q: %s", needle, fj)
1486 }
1487 }
1488
1489 // Nothing refused: no Metrics block from this alone, no key on the wire.
1490 quiet := status.CollectBox(routes, status.Probes{MachineID: "m"}, time.Now())
1491 if quiet.Metrics != nil {
1492 t.Errorf("metrics = %+v with no refusal, want absent", quiet.Metrics)
1493 }
1494 raw, _ := json.Marshal(status.CollectBox(routes, status.Probes{MachineID: "m", MemTotalBytes: new(int64)}, time.Now()))
1495 if strings.Contains(string(raw), "config_refusals") {
1496 t.Errorf("a box with no refusal publishes config_refusals: %s", raw)
1497 }
1498}
1499
1500// TestCollectBoxCarriesReleaseRefusals (D1): a box that rolled a build back
1501// says so in its private Metrics block, naming the host, version and reason.
1502// The hostname never reaches the public fleet.json, and a box with no
1503// refusal carries no key at all.
1504func TestCollectBoxCarriesReleaseRefusals(t *testing.T) {
1505 at := time.Date(2026, 10, 6, 9, 30, 40, 0, time.UTC)
1506 refusals := []status.ReleaseRefusal{
1507 {Host: "rolled-back.example.com", Version: "v2-secret-build", Reason: "exited: status=2", RefusedAt: at},
1508 }
1509 routes := []registry.Route{{Host: "rolled-back.example.com", Kind: "instance", Hibernate: true}}
1510 b := status.CollectBox(routes, status.Probes{MachineID: "m", ReleaseRefusals: refusals}, time.Now())
1511 if b.Metrics == nil {
1512 t.Fatal("metrics block missing: a release refusal alone should attach it")
1513 }
1514 if got := b.Metrics.ReleaseRefusals; len(got) != 1 || got[0] != refusals[0] {
1515 t.Fatalf("release_refusals = %+v, want %+v", got, refusals)
1516 }
1517
1518 s := teststore(t)
1519 ctx := context.Background()
1520 if err := status.WriteBox(ctx, s, b); err != nil {
1521 t.Fatal(err)
1522 }
1523 f, err := status.BuildFleet(ctx, s, routes, time.Now(), 5*time.Minute)
1524 if err != nil {
1525 t.Fatal(err)
1526 }
1527 fj, err := json.Marshal(f)
1528 if err != nil {
1529 t.Fatal(err)
1530 }
1531 for _, needle := range []string{"rolled-back.example.com", "v2-secret-build", "exited: status=2", "release_refusals"} {
1532 if strings.Contains(string(fj), needle) {
1533 t.Errorf("fleet.json leaked %q: %s", needle, fj)
1534 }
1535 }
1536
1537 quiet := status.CollectBox(routes, status.Probes{MachineID: "m"}, time.Now())
1538 if quiet.Metrics != nil {
1539 t.Errorf("metrics = %+v with no refusal, want absent", quiet.Metrics)
1540 }
1541 raw, _ := json.Marshal(status.CollectBox(routes, status.Probes{MachineID: "m", MemTotalBytes: new(int64)}, time.Now()))
1542 if strings.Contains(string(raw), "release_refusals") {
1543 t.Errorf("a box with no refusal publishes release_refusals: %s", raw)
1544 }
1545}
1546
1547// TestCollectBoxCarriesProofWaits (D3): a box holding a host on its current
1548// build for a dev proof says so in its private Metrics block. The hostname
1549// and build never reach the public fleet.json, and a box with no wait carries
1550// no key at all.
1551func TestCollectBoxCarriesProofWaits(t *testing.T) {
1552 at := time.Date(2026, 10, 6, 9, 30, 40, 0, time.UTC)
1553 waits := []status.ProofWait{
1554 {Host: "waiting.example.com", Version: "v3-secret-build", SHA256: "e47fcc1e0123456789abcdef", Prover: "dev",
1555 Reason: "no proof from dev", Since: at},
1556 }
1557 routes := []registry.Route{{Host: "waiting.example.com", Kind: "instance", Hibernate: true}}
1558 b := status.CollectBox(routes, status.Probes{MachineID: "m", ProofWaits: waits}, time.Now())
1559 if b.Metrics == nil {
1560 t.Fatal("metrics block missing: a proof wait alone should attach it")
1561 }
1562 if got := b.Metrics.ProofWaits; len(got) != 1 || got[0] != waits[0] {
1563 t.Fatalf("proof_waits = %+v, want %+v", got, waits)
1564 }
1565
1566 s := teststore(t)
1567 ctx := context.Background()
1568 if err := status.WriteBox(ctx, s, b); err != nil {
1569 t.Fatal(err)
1570 }
1571 f, err := status.BuildFleet(ctx, s, routes, time.Now(), 5*time.Minute)
1572 if err != nil {
1573 t.Fatal(err)
1574 }
1575 fj, err := json.Marshal(f)
1576 if err != nil {
1577 t.Fatal(err)
1578 }
1579 for _, needle := range []string{"waiting.example.com", "v3-secret-build", "e47fcc1e", "no proof from dev", "proof_waits"} {
1580 if strings.Contains(string(fj), needle) {
1581 t.Errorf("fleet.json leaked %q: %s", needle, fj)
1582 }
1583 }
1584
1585 quiet := status.CollectBox(routes, status.Probes{MachineID: "m"}, time.Now())
1586 if quiet.Metrics != nil {
1587 t.Errorf("metrics = %+v with no wait, want absent", quiet.Metrics)
1588 }
1589 raw, _ := json.Marshal(status.CollectBox(routes, status.Probes{MachineID: "m", MemTotalBytes: new(int64)}, time.Now()))
1590 if strings.Contains(string(raw), "proof_waits") {
1591 t.Errorf("a box with no wait publishes proof_waits: %s", raw)
1592 }
1593}
1594
1595// The config guard's bypass is visible in the private block, and only there.
1596func TestCollectBoxCarriesTheConfigGuardBypass(t *testing.T) {
1597 b := status.CollectBox(nil, status.Probes{MachineID: "m", ConfigGuard: "bypassed (CARLOS_NO_SEALED_CONFIG)"}, time.Now())
1598 if b.Metrics == nil || b.Metrics.ConfigGuard != "bypassed (CARLOS_NO_SEALED_CONFIG)" {
1599 t.Fatalf("metrics = %+v, want the bypass word", b.Metrics)
1600 }
1601 s := teststore(t)
1602 ctx := context.Background()
1603 if err := status.WriteBox(ctx, s, b); err != nil {
1604 t.Fatal(err)
1605 }
1606 f, err := status.BuildFleet(ctx, s, nil, time.Now(), 5*time.Minute)
1607 if err != nil {
1608 t.Fatal(err)
1609 }
1610 fj, _ := json.Marshal(f)
1611 if strings.Contains(string(fj), "bypass") || strings.Contains(string(fj), "NO_SEALED") {
1612 t.Errorf("fleet.json carries the bypass: %s", fj)
1613 }
1614 if quiet := status.CollectBox(nil, status.Probes{MachineID: "m"}, time.Now()); quiet.Metrics != nil {
1615 t.Errorf("a box in the normal mode attached metrics: %+v", quiet.Metrics)
1616 }
1617}
1618
1619// TestCollectBoxCarriesBuildStates (settings spec rev 11 §4.4): a box names
1620// the build-state builds of the instances it hosts in its private Metrics
1621// block, so the housekeeper can protect them. The hostnames and builds never
1622// reach the public fleet.json, and a box with none carries no key at all.
1623func TestCollectBoxCarriesBuildStates(t *testing.T) {
1624 pins := map[string]status.BuildStatePins{
1625 "held-secret.example.com": {Account: "bab", App: "km", Builds: []string{"v1-secret-build", "v2-secret-build"}, Complete: true},
1626 }
1627 routes := []registry.Route{{Host: "held-secret.example.com", Kind: "instance", Hibernate: true}}
1628 b := status.CollectBox(routes, status.Probes{MachineID: "m", BuildStates: pins}, time.Now())
1629 if b.Metrics == nil {
1630 t.Fatal("metrics block missing: build states alone should attach it")
1631 }
1632 if got := b.Metrics.BuildStates; !reflect.DeepEqual(got, pins) {
1633 t.Fatalf("build_states = %+v, want %+v", got, pins)
1634 }
1635
1636 s := teststore(t)
1637 ctx := context.Background()
1638 if err := status.WriteBox(ctx, s, b); err != nil {
1639 t.Fatal(err)
1640 }
1641 boxes, complete, err := status.LoadBoxesComplete(ctx, s)
1642 if err != nil || !complete || len(boxes) != 1 || boxes[0].Metrics == nil || !reflect.DeepEqual(boxes[0].Metrics.BuildStates, pins) {
1643 t.Fatalf("read back %+v, %v, %v; want the build states", boxes, complete, err)
1644 }
1645 f, err := status.BuildFleet(ctx, s, routes, time.Now(), 5*time.Minute)
1646 if err != nil {
1647 t.Fatal(err)
1648 }
1649 fj, err := json.Marshal(f)
1650 if err != nil {
1651 t.Fatal(err)
1652 }
1653 for _, needle := range []string{"held-secret.example.com", "v1-secret-build", "build_states"} {
1654 if strings.Contains(string(fj), needle) {
1655 t.Errorf("fleet.json leaked %q: %s", needle, fj)
1656 }
1657 }
1658
1659 raw, _ := json.Marshal(status.CollectBox(routes, status.Probes{MachineID: "m", MemTotalBytes: new(int64)}, time.Now()))
1660 if strings.Contains(string(raw), "build_states") {
1661 t.Errorf("a box with no build states publishes build_states: %s", raw)
1662 }
1663}
1664
1665// A box that could not collect its build states says so (Astra I2 #5): the
1666// error alone attaches the Metrics block, so the housekeeper reads "could not
1667// collect", never an absent map it might take for "nothing to protect".
1668func TestCollectBoxCarriesABuildStatesFailure(t *testing.T) {
1669 b := status.CollectBox(nil, status.Probes{MachineID: "m", BuildStatesErr: "listing routes for build states: database is locked"}, time.Now())
1670 if b.Metrics == nil || b.Metrics.BuildStatesErr != "listing routes for build states: database is locked" {
1671 t.Fatalf("metrics = %+v, want the collection failure", b.Metrics)
1672 }
1673 raw, _ := json.Marshal(b)
1674 if !strings.Contains(string(raw), `"build_states_err"`) {
1675 t.Fatalf("the failure is not published: %s", raw)
1676 }
1677}
1678
1679// TestCollectBoxCarriesTheUpdateSettingsProtocolAndDecisions (settings spec
1680// §4.3, §4.4): a box that enforces update settings says so, and names root's
1681// last decision per instance in the private Metrics block. The protocol alone
1682// attaches the block (the console reads it to lift its refusal); the hosts
1683// and builds never reach the public fleet.json.
1684func TestCollectBoxCarriesTheUpdateSettingsProtocolAndDecisions(t *testing.T) {
1685 routes := []registry.Route{{Host: "decided-secret.example.com", Kind: "instance", Hibernate: true}}
1686 only := status.CollectBox(routes, status.Probes{MachineID: "m", UpdateSettings: status.UpdateSettingsProtocol}, time.Now())
1687 if only.Metrics == nil || only.Metrics.UpdateSettings != status.UpdateSettingsProtocol {
1688 t.Fatalf("metrics = %+v, want the protocol alone to attach the block", only.Metrics)
1689 }
1690 raw, _ := json.Marshal(only)
1691 if strings.Contains(string(raw), `"updates"`) {
1692 t.Errorf("a box with no decisions publishes updates: %s", raw)
1693 }
1694
1695 two := 2
1696 ups := map[string]status.HostUpdates{
1697 "decided-secret.example.com": {Desired: "v-secret-desired", Reason: "major", Detail: "v1", Baking: "v-secret-baking", Major: &two, Pending: 3, Completed: 2, CompletedFailed: true,
1698 Account: "acct-secret-tenant", App: "app-secret-tenant"},
1699 }
1700 b := status.CollectBox(routes, status.Probes{MachineID: "m", Updates: ups}, time.Now())
1701 if b.Metrics == nil || !reflect.DeepEqual(b.Metrics.Updates, ups) {
1702 t.Fatalf("updates = %+v, want %+v", b.Metrics, ups)
1703 }
1704 s := teststore(t)
1705 ctx := context.Background()
1706 if err := status.WriteBox(ctx, s, b); err != nil {
1707 t.Fatal(err)
1708 }
1709 boxes, complete, err := status.LoadBoxesComplete(ctx, s)
1710 if err != nil || !complete || len(boxes) != 1 || boxes[0].Metrics == nil || !reflect.DeepEqual(boxes[0].Metrics.Updates, ups) {
1711 t.Fatalf("read back %+v, %v, %v; want the decisions", boxes, complete, err)
1712 }
1713 f, err := status.BuildFleet(ctx, s, routes, time.Now(), 5*time.Minute)
1714 if err != nil {
1715 t.Fatal(err)
1716 }
1717 fj, _ := json.Marshal(f)
1718 for _, needle := range []string{"decided-secret.example.com", "v-secret-desired", "v-secret-baking", "acct-secret-tenant", "app-secret-tenant", `"updates"`, "update_settings"} {
1719 if strings.Contains(string(fj), needle) {
1720 t.Errorf("fleet.json leaked %q: %s", needle, fj)
1721 }
1722 }
1723}
1724