| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "encoding/json" |
| 6 | "errors" |
| 7 | "strconv" |
| 8 | "strings" |
| 9 | "testing" |
| 10 | |
| 11 | "github.com/carlosframework/platform/internal/domains" |
| 12 | "github.com/carlosframework/platform/internal/provision" |
| 13 | "github.com/carlosframework/platform/internal/registry" |
| 14 | "github.com/carlosframework/platform/internal/release" |
| 15 | ) |
| 16 | |
| 17 | // ---- fakes ----------------------------------------------------------------- |
| 18 | |
| 19 | // failingListStore breaks the LISTING of one prefix — the control-bucket |
| 20 | // blip that must never reach the converger as "no edges" or "no records". |
| 21 | // Prefix-scoped because the pass now lists three of them (control/edges/, |
| 22 | // accounts/, apps/) and each is a separate fail-closed direction. |
| 23 | type failingListStore struct { |
| 24 | release.Store |
| 25 | prefix string |
| 26 | err error |
| 27 | } |
| 28 | |
| 29 | func (f failingListStore) List(ctx context.Context, prefix string) ([]string, error) { |
| 30 | if prefix == f.prefix { |
| 31 | return nil, f.err |
| 32 | } |
| 33 | return f.Store.List(ctx, prefix) |
| 34 | } |
| 35 | |
| 36 | // ListDirs fails the same prefix: the record readers walk apps/ by its |
| 37 | // directories now (provision.listAppSubtree), so "the apps listing fails" |
| 38 | // is a ListDirs of apps/ failing as much as a List of it. |
| 39 | func (f failingListStore) ListDirs(ctx context.Context, prefix string) ([]string, error) { |
| 40 | if prefix == f.prefix { |
| 41 | return nil, f.err |
| 42 | } |
| 43 | return f.Store.ListDirs(ctx, prefix) |
| 44 | } |
| 45 | |
| 46 | // failingGetStore lists fine but refuses ONE named key — the half-read that |
| 47 | // would otherwise fold to a short desired set and delete the missing box's |
| 48 | // records. |
| 49 | type failingGetStore struct { |
| 50 | release.Store |
| 51 | bad string |
| 52 | err error |
| 53 | } |
| 54 | |
| 55 | func (f failingGetStore) Get(ctx context.Context, key string) ([]byte, string, error) { |
| 56 | if key == f.bad { |
| 57 | return nil, "", f.err |
| 58 | } |
| 59 | return f.Store.Get(ctx, key) |
| 60 | } |
| 61 | |
| 62 | // convergeRecorder captures what the pass handed the converger, and whether |
| 63 | // it was called at all — the assertion every fail-closed test makes. |
| 64 | type convergeRecorder struct { |
| 65 | calls int |
| 66 | routes []registry.Route |
| 67 | edges [][]byte |
| 68 | err error |
| 69 | } |
| 70 | |
| 71 | func (c *convergeRecorder) converge(_ context.Context, routes []registry.Route, edges [][]byte) error { |
| 72 | c.calls++ |
| 73 | c.routes = routes |
| 74 | c.edges = edges |
| 75 | return c.err |
| 76 | } |
| 77 | |
| 78 | func steeringTestStore(t *testing.T, files map[string][]byte) release.Store { |
| 79 | t.Helper() |
| 80 | s := release.NewDirStore(t.TempDir()) |
| 81 | for key, body := range files { |
| 82 | if _, err := s.Put(context.Background(), key, body, ""); err != nil { |
| 83 | t.Fatal(err) |
| 84 | } |
| 85 | } |
| 86 | return s |
| 87 | } |
| 88 | |
| 89 | // seedAccount writes the account manifest ListAllRecords' known-account |
| 90 | // guard (provision.ParseRecordKey) requires, and returns its sqid — the |
| 91 | // frozen encoding of the id, via domains.Sqid, never a literal. |
| 92 | func seedAccount(t *testing.T, s release.Store, id int64, name string) string { |
| 93 | t.Helper() |
| 94 | sqid := domains.Sqid(id) |
| 95 | body := []byte(`{"id":` + strconv.FormatInt(id, 10) + `,"sqid":"` + sqid + |
| 96 | `","name":"` + name + `","created_at":"2026-08-02T09:00:00Z"}`) |
| 97 | if _, err := s.Put(context.Background(), "accounts/"+sqid+"/account.json", body, ""); err != nil { |
| 98 | t.Fatal(err) |
| 99 | } |
| 100 | return sqid |
| 101 | } |
| 102 | |
| 103 | // seedRecord writes one instance record at its canonical key. |
| 104 | func seedRecord(t *testing.T, s release.Store, sqid, app, host, steering, pool string) { |
| 105 | t.Helper() |
| 106 | rec := provision.Record{ |
| 107 | Schema: 1, Host: host, Slug: app, App: app, Account: sqid, |
| 108 | Pool: pool, Channel: "canary/main", Steering: steering, |
| 109 | } |
| 110 | body, err := json.Marshal(rec) |
| 111 | if err != nil { |
| 112 | t.Fatal(err) |
| 113 | } |
| 114 | if _, err := s.Put(context.Background(), provision.RecordKey(sqid, app, host), body, ""); err != nil { |
| 115 | t.Fatal(err) |
| 116 | } |
| 117 | } |
| 118 | |
| 119 | // ---- the pass: what it reads, and failing closed (H2) ---------------------- |
| 120 | |
| 121 | // steeringWorld seeds a whole small deployment: two boxes' edges files, one |
| 122 | // account, and three records — one steered, one unsteered, one steered under |
| 123 | // a pool. |
| 124 | func steeringWorld(t *testing.T) (release.Store, string) { |
| 125 | t.Helper() |
| 126 | s := steeringTestStore(t, map[string][]byte{ |
| 127 | "control/edges/dublin.json": []byte(`{"v":1,"box":"dublin"}`), |
| 128 | "control/edges/oregon.json": []byte(`{"v":1,"box":"oregon"}`), |
| 129 | }) |
| 130 | sqid := seedAccount(t, s, 12, "Tito") |
| 131 | seedRecord(t, s, sqid, "app", "app.example.com", "latency", "") |
| 132 | seedRecord(t, s, sqid, "app", "plain.example.com", "", "") |
| 133 | return s, sqid |
| 134 | } |
| 135 | |
| 136 | // The healthy pass: edges bodies in sorted listing order, and routes derived |
| 137 | // from the BUCKET RECORDS — not from any registry. |
| 138 | func TestSteeringPassReadsEdgesFilesAndRecords(t *testing.T) { |
| 139 | quietLog(t) |
| 140 | s, _ := steeringWorld(t) |
| 141 | rec := &convergeRecorder{} |
| 142 | |
| 143 | if err := steeringPass(context.Background(), s, rec.converge); err != nil { |
| 144 | t.Fatalf("steeringPass: %v", err) |
| 145 | } |
| 146 | if rec.calls != 1 { |
| 147 | t.Fatalf("converge calls = %d, want 1", rec.calls) |
| 148 | } |
| 149 | if len(rec.edges) != 2 { |
| 150 | t.Fatalf("edges = %d bodies, want 2", len(rec.edges)) |
| 151 | } |
| 152 | // Listing order is sorted, so the fold is deterministic tick to tick. |
| 153 | if !strings.Contains(string(rec.edges[0]), "dublin") || !strings.Contains(string(rec.edges[1]), "oregon") { |
| 154 | t.Fatalf("edges = %q, want dublin then oregon (sorted)", rec.edges) |
| 155 | } |
| 156 | // Only the steered record becomes a route; the unsteered one is dropped |
| 157 | // here because the fold would skip it silently anyway. |
| 158 | if len(rec.routes) != 1 { |
| 159 | t.Fatalf("routes = %+v, want only the steered record", rec.routes) |
| 160 | } |
| 161 | got := rec.routes[0] |
| 162 | if got.Host != "app.example.com" || got.Steering != "latency" || got.Pool != "" { |
| 163 | t.Fatalf("route = %+v, want the three fields the fold reads", got) |
| 164 | } |
| 165 | } |
| 166 | |
| 167 | // FINDING 1's pin (review, 2026-08-23). The steered host here is the |
| 168 | // HOME-BOXED class — an instance record with no Addr, which everywhereClass |
| 169 | // materializes on ONE box only, so it is absent from every other box's |
| 170 | // registry. It must still produce a record from whichever box holds the |
| 171 | // housekeeper lease, because the lease moves and DNS must not. |
| 172 | // |
| 173 | // The proof is structural as much as behavioural: steeringPass takes no |
| 174 | // registry at all, so there is no local view for the answer to depend on. |
| 175 | // Before the fix this test could not even have been written — the pass would |
| 176 | // have needed a registry, and the host would have been missing from it. |
| 177 | func TestSteeringPassSteersHomeBoxedRecordWithNoLocalRegistry(t *testing.T) { |
| 178 | quietLog(t) |
| 179 | s := steeringTestStore(t, map[string][]byte{ |
| 180 | "control/edges/dublin.json": []byte(`{"v":1,"box":"dublin"}`), |
| 181 | }) |
| 182 | sqid := seedAccount(t, s, 12, "Tito") |
| 183 | // No Addr, no Unit, default hibernation: home-boxed, exactly the class |
| 184 | // the rollout's stage-5 checklist steers. |
| 185 | seedRecord(t, s, sqid, "exec-app", "home.example.com", "latency", "") |
| 186 | rec := &convergeRecorder{} |
| 187 | |
| 188 | if err := steeringPass(context.Background(), s, rec.converge); err != nil { |
| 189 | t.Fatalf("steeringPass: %v", err) |
| 190 | } |
| 191 | if len(rec.routes) != 1 || rec.routes[0].Host != "home.example.com" { |
| 192 | t.Fatalf("routes = %+v, want the home-boxed steered host — a lease migration must not withdraw it", rec.routes) |
| 193 | } |
| 194 | } |
| 195 | |
| 196 | // The loader drops Steering=="" (the fold skips those silently anyway) but |
| 197 | // must NOT become a second, quieter policy gate: an invalid value, a |
| 198 | // wildcard host and a pool-scoped row all go DOWN to the fold, whose loud |
| 199 | // documented refusals are the operator's only signal that they exist. |
| 200 | func TestSteeringPassPassesNonLatencySteeringValuesDown(t *testing.T) { |
| 201 | quietLog(t) |
| 202 | s := steeringTestStore(t, map[string][]byte{ |
| 203 | "control/edges/dublin.json": []byte(`{"v":1,"box":"dublin"}`), |
| 204 | }) |
| 205 | sqid := seedAccount(t, s, 12, "Tito") |
| 206 | seedRecord(t, s, sqid, "app", "weird.example.com", "weighted", "") |
| 207 | seedRecord(t, s, sqid, "app", "pooled.example.com", "latency", "eu") |
| 208 | rec := &convergeRecorder{} |
| 209 | |
| 210 | if err := steeringPass(context.Background(), s, rec.converge); err != nil { |
| 211 | t.Fatalf("steeringPass: %v", err) |
| 212 | } |
| 213 | if len(rec.routes) != 2 { |
| 214 | t.Fatalf("routes = %+v, want both rows delivered for the fold to refuse loudly", rec.routes) |
| 215 | } |
| 216 | } |
| 217 | |
| 218 | // H2, direction one: a broken edges LISTING must not converge. |
| 219 | func TestSteeringPassFailsClosedOnEdgesListError(t *testing.T) { |
| 220 | quietLog(t) |
| 221 | boom := errors.New("bucket down") |
| 222 | base, _ := steeringWorld(t) |
| 223 | s := failingListStore{Store: base, prefix: "control/edges/", err: boom} |
| 224 | rec := &convergeRecorder{} |
| 225 | |
| 226 | err := steeringPass(context.Background(), s, rec.converge) |
| 227 | if err == nil { |
| 228 | t.Fatal("steeringPass returned nil on a failed edges listing") |
| 229 | } |
| 230 | if !errors.Is(err, boom) { |
| 231 | t.Fatalf("err = %v, want it to wrap the store error", err) |
| 232 | } |
| 233 | if rec.calls != 0 { |
| 234 | t.Fatalf("converge ran %d times after a failed listing, want 0 — never converge on a partial fold", rec.calls) |
| 235 | } |
| 236 | } |
| 237 | |
| 238 | // H2, direction two, and the one the shrink guard was written for: the |
| 239 | // listing succeeded, so the pass KNOWS how many boxes there are — and one of |
| 240 | // their files would not come back. |
| 241 | func TestSteeringPassFailsClosedOnEdgesGetError(t *testing.T) { |
| 242 | quietLog(t) |
| 243 | boom := errors.New("object unreadable") |
| 244 | base, _ := steeringWorld(t) |
| 245 | s := failingGetStore{Store: base, bad: "control/edges/oregon.json", err: boom} |
| 246 | rec := &convergeRecorder{} |
| 247 | |
| 248 | err := steeringPass(context.Background(), s, rec.converge) |
| 249 | if err == nil { |
| 250 | t.Fatal("steeringPass returned nil with one edges file unreadable") |
| 251 | } |
| 252 | if !errors.Is(err, boom) { |
| 253 | t.Fatalf("err = %v, want it to wrap the store error", err) |
| 254 | } |
| 255 | if !strings.Contains(err.Error(), "control/edges/oregon.json") { |
| 256 | t.Fatalf("err = %v, want the unreadable key named", err) |
| 257 | } |
| 258 | if rec.calls != 0 { |
| 259 | t.Fatalf("converge ran %d times after a failed get, want 0", rec.calls) |
| 260 | } |
| 261 | } |
| 262 | |
| 263 | // H2, direction three, re-pointed at the RECORDS read (FINDING 1): the |
| 264 | // records are now the routes half of the fold, so every way that read can |
| 265 | // come back short is a way a live steered host gets deleted. Four of them, |
| 266 | // each fatal. |
| 267 | func TestSteeringPassFailsClosedOnRecordsRead(t *testing.T) { |
| 268 | boom := errors.New("bucket down") |
| 269 | cases := []struct { |
| 270 | name string |
| 271 | wrap func(release.Store) release.Store |
| 272 | named string |
| 273 | }{ |
| 274 | { |
| 275 | // Without accounts/, ParseRecordKey's known-account guard rejects |
| 276 | // EVERY record key — the fold would see no steered hosts at all. |
| 277 | name: "accounts listing fails", |
| 278 | wrap: func(s release.Store) release.Store { |
| 279 | return failingListStore{Store: s, prefix: "accounts/", err: boom} |
| 280 | }, |
| 281 | }, |
| 282 | { |
| 283 | // The subtle one: ONE account's manifest is unreadable, so only |
| 284 | // THAT account's records vanish. Silent, partial, and exactly the |
| 285 | // shape that deletes one tenant's DNS. |
| 286 | name: "an account manifest is unreadable", |
| 287 | wrap: func(s release.Store) release.Store { |
| 288 | return failingGetStore{Store: s, bad: "accounts/" + domains.Sqid(12) + "/account.json", err: boom} |
| 289 | }, |
| 290 | }, |
| 291 | { |
| 292 | name: "apps listing fails", |
| 293 | wrap: func(s release.Store) release.Store { |
| 294 | return failingListStore{Store: s, prefix: "apps/", err: boom} |
| 295 | }, |
| 296 | }, |
| 297 | { |
| 298 | name: "a record object will not Get", |
| 299 | wrap: func(s release.Store) release.Store { |
| 300 | return failingGetStore{Store: s, bad: provision.RecordKey(domains.Sqid(12), "app", "app.example.com"), err: boom} |
| 301 | }, |
| 302 | named: "app.example.com", |
| 303 | }, |
| 304 | } |
| 305 | for _, c := range cases { |
| 306 | t.Run(c.name, func(t *testing.T) { |
| 307 | quietLog(t) |
| 308 | base, _ := steeringWorld(t) |
| 309 | rec := &convergeRecorder{} |
| 310 | |
| 311 | err := steeringPass(context.Background(), c.wrap(base), rec.converge) |
| 312 | if err == nil { |
| 313 | t.Fatalf("steeringPass returned nil when %s", c.name) |
| 314 | } |
| 315 | if rec.calls != 0 { |
| 316 | t.Fatalf("converge ran %d times, want 0 — a short records read must never converge", rec.calls) |
| 317 | } |
| 318 | if c.named != "" && !strings.Contains(err.Error(), c.named) { |
| 319 | t.Fatalf("err = %v, want the unreadable record named", err) |
| 320 | } |
| 321 | }) |
| 322 | } |
| 323 | } |
| 324 | |
| 325 | // The content-vs-visibility split, records half: a record that READS but does |
| 326 | // not decode is skipped and logged, and the pass converges on the rest. One |
| 327 | // malformed body must not blind the deployment to every other steered host. |
| 328 | func TestSteeringPassSkipsUnparseableRecord(t *testing.T) { |
| 329 | logs := captureLog(t) |
| 330 | s, sqid := steeringWorld(t) |
| 331 | if _, err := s.Put(context.Background(), provision.RecordKey(sqid, "app", "broken.example.com"), []byte("{not json"), ""); err != nil { |
| 332 | t.Fatal(err) |
| 333 | } |
| 334 | rec := &convergeRecorder{} |
| 335 | |
| 336 | if err := steeringPass(context.Background(), s, rec.converge); err != nil { |
| 337 | t.Fatalf("steeringPass: %v — an unparseable record is not a read failure", err) |
| 338 | } |
| 339 | if rec.calls != 1 { |
| 340 | t.Fatalf("converge calls = %d, want 1", rec.calls) |
| 341 | } |
| 342 | if len(rec.routes) != 1 || rec.routes[0].Host != "app.example.com" { |
| 343 | t.Fatalf("routes = %+v, want the healthy steered record still folded", rec.routes) |
| 344 | } |
| 345 | if !strings.Contains(logs(), "broken.example.com") { |
| 346 | t.Fatalf("log = %q, want the malformed record named", logs()) |
| 347 | } |
| 348 | } |
| 349 | |
| 350 | // The same split, edges half: the host loader must NOT pre-judge content — |
| 351 | // a body that will not parse is delivered verbatim and the fold decides. |
| 352 | func TestSteeringPassPassesUnparseableBodiesDown(t *testing.T) { |
| 353 | quietLog(t) |
| 354 | s := steeringTestStore(t, map[string][]byte{ |
| 355 | "control/edges/dublin.json": []byte("{not json"), |
| 356 | }) |
| 357 | seedAccount(t, s, 12, "Tito") |
| 358 | rec := &convergeRecorder{} |
| 359 | |
| 360 | if err := steeringPass(context.Background(), s, rec.converge); err != nil { |
| 361 | t.Fatalf("steeringPass: %v — an unparseable body is not a read failure", err) |
| 362 | } |
| 363 | if rec.calls != 1 { |
| 364 | t.Fatalf("converge calls = %d, want 1 — parse failures are the fold's business", rec.calls) |
| 365 | } |
| 366 | if len(rec.edges) != 1 { |
| 367 | t.Fatalf("edges = %d bodies, want the unparseable one delivered verbatim", len(rec.edges)) |
| 368 | } |
| 369 | } |
| 370 | |
| 371 | // Objects under the edges prefix that this deployment's edges loop did not |
| 372 | // write are not read at all — the same key-shape filter internal/edge's |
| 373 | // homeMap applies. A stray object must not be able to fail the pass forever. |
| 374 | func TestSteeringPassIgnoresForeignKeysUnderThePrefix(t *testing.T) { |
| 375 | quietLog(t) |
| 376 | s := steeringTestStore(t, map[string][]byte{ |
| 377 | "control/edges/dublin.json": []byte(`{"v":1,"box":"dublin"}`), |
| 378 | "control/edges/README.txt": []byte("hello"), |
| 379 | "control/edges/nested/inner.json": []byte(`{"v":1,"box":"nested"}`), |
| 380 | }) |
| 381 | seedAccount(t, s, 12, "Tito") |
| 382 | rec := &convergeRecorder{} |
| 383 | |
| 384 | if err := steeringPass(context.Background(), s, rec.converge); err != nil { |
| 385 | t.Fatalf("steeringPass: %v", err) |
| 386 | } |
| 387 | if len(rec.edges) != 1 { |
| 388 | t.Fatalf("edges = %d bodies, want only control/edges/<box>.json", len(rec.edges)) |
| 389 | } |
| 390 | } |
| 391 | |
| 392 | // ---- the duty: dormancy and the housekeeper gate --------------------------- |
| 393 | |
| 394 | // The dormancy proof: with no zone id the duty returns nil having called |
| 395 | // NOTHING — the pass closure holds every read and every AWS call, so "the |
| 396 | // pass never ran" is "zero reads, zero API calls" structurally. |
| 397 | func TestSteeringDutyDormantDoesNoWork(t *testing.T) { |
| 398 | quietLog(t) |
| 399 | ran := 0 |
| 400 | duty := steeringDuty("", func(context.Context) error { ran++; return nil }) |
| 401 | |
| 402 | if err := duty.run(context.Background()); err != nil { |
| 403 | t.Fatalf("dormant duty returned %v, want nil", err) |
| 404 | } |
| 405 | if ran != 0 { |
| 406 | t.Fatalf("dormant duty ran the pass %d times, want 0", ran) |
| 407 | } |
| 408 | } |
| 409 | |
| 410 | // FINDING 3 (review, 2026-08-23): totality on a production DNS write path. |
| 411 | // A zone id with no pass is unreachable today — steeringLoop leaves pass nil |
| 412 | // in exactly the branch that leaves zoneID empty — but if that ever stopped |
| 413 | // being true, the duty must decline rather than panic the agent's goroutine |
| 414 | // on a nil call. |
| 415 | func TestSteeringDutyWithNoPassDeclines(t *testing.T) { |
| 416 | quietLog(t) |
| 417 | duty := steeringDuty("Z0ABCDEF1234", nil) |
| 418 | if err := duty.run(context.Background()); err != nil { |
| 419 | t.Fatalf("duty with a nil pass returned %v, want nil (and no panic)", err) |
| 420 | } |
| 421 | } |
| 422 | |
| 423 | func TestSteeringDutyRunsThePassWhenZoneSet(t *testing.T) { |
| 424 | quietLog(t) |
| 425 | ran := 0 |
| 426 | boom := errors.New("converge failed") |
| 427 | duty := steeringDuty("Z0ABCDEF1234", func(context.Context) error { ran++; return boom }) |
| 428 | |
| 429 | if err := duty.run(context.Background()); !errors.Is(err, boom) { |
| 430 | t.Fatalf("duty error = %v, want the pass's error propagated to the housekeeper log", err) |
| 431 | } |
| 432 | if ran != 1 { |
| 433 | t.Fatalf("pass ran %d times, want 1", ran) |
| 434 | } |
| 435 | } |
| 436 | |
| 437 | // The housekeeper contract: one box converges the deployment's zone, not |
| 438 | // every box. A non-holder does nothing and says nothing. |
| 439 | func TestSteeringDutyRunsOnlyOnTheHousekeeper(t *testing.T) { |
| 440 | quietLog(t) |
| 441 | ran := 0 |
| 442 | duty := steeringDuty("Z0ABCDEF1234", func(context.Context) error { ran++; return nil }) |
| 443 | |
| 444 | runHousekeeperDuties(noGate, duty) |
| 445 | if ran != 0 { |
| 446 | t.Fatalf("pass ran %d times on a non-holder, want 0", ran) |
| 447 | } |
| 448 | runHousekeeperDuties(heldGate, duty) |
| 449 | if ran != 1 { |
| 450 | t.Fatalf("pass ran %d times on the holder, want 1", ran) |
| 451 | } |
| 452 | } |
| 453 | |
| 454 | // ---- the loop: arming ------------------------------------------------------ |
| 455 | |
| 456 | // Mirrors TestMintMeshKeyLoopIsAPlatformBoxJob: a fleet box must skip before |
| 457 | // ever opening a store. |
| 458 | func TestSteeringLoopIsAPlatformBoxJob(t *testing.T) { |
| 459 | const skipLine = "agent: steering loop is a platform-box job — skipped on a fleet box" |
| 460 | |
| 461 | logs := captureLog(t) |
| 462 | door := newDoorFake(t) |
| 463 | for k, v := range fleetDoorEnv(door.url) { |
| 464 | t.Setenv(k, v) |
| 465 | } |
| 466 | t.Setenv("CARLOS_DEPLOYMENT_BUCKET", "") |
| 467 | t.Setenv("CARLOS_DEPLOYMENT_DIR", "") |
| 468 | |
| 469 | steeringLoop() // must return; a fleet box never reaches the loop |
| 470 | |
| 471 | if got := logs(); !strings.Contains(got, skipLine) { |
| 472 | t.Fatalf("log = %q, want the skip line verbatim", got) |
| 473 | } |
| 474 | if hits := door.seen(); len(hits) != 0 { |
| 475 | t.Fatalf("door saw %+v, want zero — the skip must land BEFORE the store opens", hits) |
| 476 | } |
| 477 | } |
| 478 | |
| 479 | // Arming independence, the mesh-key loop's own regression guard applied here: |
| 480 | // a box missing CARLOS_RELEASE_PUBKEY (retentionLoop's gate, unrelated to |
| 481 | // steering) still WINS the housekeeper election, so a steering duty gated on |
| 482 | // that var would silently never converge fleet-wide. |
| 483 | func TestSteeringLoopArmsWithoutReleasePubkey(t *testing.T) { |
| 484 | t.Setenv("CARLOS_FLEET", "") |
| 485 | t.Setenv("CARLOS_DEPLOYMENT_BUCKET", "") |
| 486 | t.Setenv("CARLOS_DEPLOYMENT_DIR", t.TempDir()) |
| 487 | t.Setenv("CARLOS_RELEASE_PUBKEY", "") // deliberately unset — the whole point |
| 488 | t.Setenv("CARLOS_STEERING_ZONE_ID", "") |
| 489 | |
| 490 | buf := captureRacyLog(t) |
| 491 | go steeringLoop() |
| 492 | |
| 493 | waitFor(t, "the steering loop to arm despite no CARLOS_RELEASE_PUBKEY", func() bool { |
| 494 | return strings.Contains(buf.String(), "agent: steering loop armed") |
| 495 | }) |
| 496 | } |
| 497 | |
| 498 | // Today's boxes: the env is unset, so the loop arms and says so ONCE, and its |
| 499 | // duty is the dormant one — no store reads, no registry reads, no AWS client |
| 500 | // ever constructed. |
| 501 | func TestSteeringLoopDormantWhenZoneUnset(t *testing.T) { |
| 502 | t.Setenv("CARLOS_FLEET", "") |
| 503 | t.Setenv("CARLOS_DEPLOYMENT_BUCKET", "") |
| 504 | t.Setenv("CARLOS_DEPLOYMENT_DIR", t.TempDir()) |
| 505 | t.Setenv("CARLOS_STEERING_ZONE_ID", "") |
| 506 | |
| 507 | buf := captureRacyLog(t) |
| 508 | go steeringLoop() |
| 509 | |
| 510 | waitFor(t, "the dormant line", func() bool { |
| 511 | return strings.Contains(buf.String(), "steering: zone unset — dormant") |
| 512 | }) |
| 513 | if n := strings.Count(buf.String(), "steering: zone unset — dormant"); n != 1 { |
| 514 | t.Fatalf("dormant line logged %d times, want exactly 1 (at arm)", n) |
| 515 | } |
| 516 | } |
| 517 | |
| 518 | // A malformed zone id is dormancy too, not a crash loop: SteeringZoneID logs |
| 519 | // it loudly and returns "", and the loop arms dormant on that. |
| 520 | func TestSteeringLoopDormantWhenZoneMalformed(t *testing.T) { |
| 521 | t.Setenv("CARLOS_FLEET", "") |
| 522 | t.Setenv("CARLOS_DEPLOYMENT_BUCKET", "") |
| 523 | t.Setenv("CARLOS_DEPLOYMENT_DIR", t.TempDir()) |
| 524 | t.Setenv("CARLOS_STEERING_ZONE_ID", "/hostedzone/Z0ABCDEF1234") |
| 525 | |
| 526 | buf := captureRacyLog(t) |
| 527 | go steeringLoop() |
| 528 | |
| 529 | waitFor(t, "the dormant line for a malformed zone id", func() bool { |
| 530 | return strings.Contains(buf.String(), "steering: zone unset — dormant") |
| 531 | }) |
| 532 | if !strings.Contains(buf.String(), "is not a bare hosted-zone id") { |
| 533 | t.Fatalf("log = %q, want the malformed id named loudly", buf.String()) |
| 534 | } |
| 535 | } |
| 536 | |