carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package host
2
3import (
4 "bytes"
5 "context"
6 "crypto/ecdh"
7 "encoding/base64"
8 "encoding/json"
9 "log"
10 "os"
11 "path/filepath"
12 "strings"
13 "sync"
14 "testing"
15 "time"
16
17 "github.com/carlosframework/platform/internal/appconfig"
18 "github.com/carlosframework/platform/internal/edgesfile"
19 "github.com/carlosframework/platform/internal/moves"
20 "github.com/carlosframework/platform/internal/registry"
21 "github.com/carlosframework/platform/internal/release"
22 "github.com/carlosframework/platform/internal/status"
23)
24
25func edgesTestRegistry(t *testing.T) *registry.Registry {
26 t.Helper()
27 reg, err := registry.Open(filepath.Join(t.TempDir(), "registry.db"))
28 if err != nil {
29 t.Fatal(err)
30 }
31 t.Cleanup(func() { reg.Close() })
32 return reg
33}
34
35// seedRoute upserts a minimal ready route under the given host — any served
36// kind does for these tests, since edgesFilePass folds every kind in
37// (the serving-class split is the reader's business, Task 3).
38func seedRoute(t *testing.T, reg *registry.Registry, host string) {
39 t.Helper()
40 if err := reg.Upsert(registry.Route{Host: host, Kind: "service", Addr: "10.0.0.1:3000", Status: "ready"}); err != nil {
41 t.Fatal(err)
42 }
43}
44
45// putCountingStore wraps a release.Store and counts Put calls — the same
46// wrap-and-count idiom agent_test.go's countingStore uses for Get, applied
47// to Put so the change-gate test can pin "second identical pass writes
48// nothing" without inspecting store internals.
49type putCountingStore struct {
50 release.Store
51 puts int
52}
53
54func (p *putCountingStore) Put(ctx context.Context, key string, body []byte, ifMatch string) (string, error) {
55 p.puts++
56 return p.Store.Put(ctx, key, body, ifMatch)
57}
58
59func TestEdgesFilePublishesHomedHosts(t *testing.T) {
60 s, reg := release.NewDirStore(t.TempDir()), edgesTestRegistry(t)
61 seedRoute(t, reg, "app.example.com")
62 var lastBody []byte
63 if err := edgesFilePass(context.Background(), s, reg, edgesFile{Box: "annie", Region: "", Placement: "", DataBucket: "", DataBucketRegion: ""}, "1.2.3.4", &lastBody); err != nil {
64 t.Fatal(err)
65 }
66 got, _, err := s.Get(context.Background(), "control/edges/annie.json")
67 if err != nil {
68 t.Fatal(err)
69 }
70 var f edgesFile
71 if err := json.Unmarshal(got, &f); err != nil {
72 t.Fatal(err)
73 }
74 if f.V != 1 || f.Box != "annie" || f.EIP != "1.2.3.4" {
75 t.Fatalf("file = %+v", f)
76 }
77 if r, ok := f.Hosts["app.example.com"]; !ok || r.Role != "home" {
78 t.Fatalf("hosts = %+v", f.Hosts)
79 }
80 if f.Generation == "" {
81 t.Fatal("generation must be stamped")
82 }
83 if got, want := edgesFileKey("annie"), "control/edges/annie.json"; got != want {
84 t.Fatalf("edgesFileKey = %q, want %q", got, want)
85 }
86}
87
88// A box with no discoverable public IP still publishes the file, with
89// "eip":"" — the DR runbook's hosts list must not depend on IMDS working.
90func TestEdgesFilePublishesWithEmptyEIP(t *testing.T) {
91 s, reg := release.NewDirStore(t.TempDir()), edgesTestRegistry(t)
92 seedRoute(t, reg, "app.example.com")
93 var lastBody []byte
94 if err := edgesFilePass(context.Background(), s, reg, edgesFile{Box: "annie", Region: "", Placement: "", DataBucket: "", DataBucketRegion: ""}, "", &lastBody); err != nil {
95 t.Fatal(err)
96 }
97 got, _, err := s.Get(context.Background(), "control/edges/annie.json")
98 if err != nil {
99 t.Fatal(err)
100 }
101 var f edgesFile
102 if err := json.Unmarshal(got, &f); err != nil {
103 t.Fatal(err)
104 }
105 if f.EIP != "" {
106 t.Fatalf("eip = %q, want empty", f.EIP)
107 }
108 if _, ok := f.Hosts["app.example.com"]; !ok {
109 t.Fatalf("hosts = %+v, want app.example.com present despite empty eip", f.Hosts)
110 }
111}
112
113func TestEdgesFileSkipsWriteWhenUnchanged(t *testing.T) {
114 real := release.NewDirStore(t.TempDir())
115 s := &putCountingStore{Store: real}
116 reg := edgesTestRegistry(t)
117 seedRoute(t, reg, "app.example.com")
118
119 var lastBody []byte
120 if err := edgesFilePass(context.Background(), s, reg, edgesFile{Box: "annie", Region: "", Placement: "", DataBucket: "", DataBucketRegion: ""}, "1.2.3.4", &lastBody); err != nil {
121 t.Fatal(err)
122 }
123 if s.puts != 1 {
124 t.Fatalf("first pass puts = %d, want 1", s.puts)
125 }
126
127 // Second pass, identical registry state: a fresh generation timestamp
128 // would differ from the first pass's, but the change-gate excludes
129 // Generation, so this must not write.
130 if err := edgesFilePass(context.Background(), s, reg, edgesFile{Box: "annie", Region: "", Placement: "", DataBucket: "", DataBucketRegion: ""}, "1.2.3.4", &lastBody); err != nil {
131 t.Fatal(err)
132 }
133 if s.puts != 1 {
134 t.Fatalf("unchanged second pass puts = %d, want still 1 (no write)", s.puts)
135 }
136
137 // A registry change makes the next pass write again.
138 seedRoute(t, reg, "second.example.com")
139 if err := edgesFilePass(context.Background(), s, reg, edgesFile{Box: "annie", Region: "", Placement: "", DataBucket: "", DataBucketRegion: ""}, "1.2.3.4", &lastBody); err != nil {
140 t.Fatal(err)
141 }
142 if s.puts != 2 {
143 t.Fatalf("changed third pass puts = %d, want 2", s.puts)
144 }
145}
146
147// FINDING 1 (review, 2026-08-22): readPublicIP is a single 2s best-effort
148// IMDS attempt, re-run every tick — a transient throttle or blip returns ""
149// exactly like a box that never had a public IP at all. Without a hold, a
150// single hiccuped tick would blank a known-good EIP in the published file,
151// costing every remote edge its dial address for this box's home-boxed
152// hosts until the next successful tick (up to 5min + jitter). A pass
153// reporting "" after a pass that reported "1.2.3.4" must keep "1.2.3.4".
154func TestEdgesFileHoldsLastKnownGoodEIPOnTransientBlank(t *testing.T) {
155 s, reg := release.NewDirStore(t.TempDir()), edgesTestRegistry(t)
156 seedRoute(t, reg, "app.example.com")
157
158 var last []byte
159 if err := edgesFilePass(context.Background(), s, reg, edgesFile{Box: "annie", Region: "", Placement: "", DataBucket: "", DataBucketRegion: ""}, "1.2.3.4", &last); err != nil {
160 t.Fatal(err)
161 }
162
163 // Simulate a transient IMDS blip: this tick's probe returns "".
164 if err := edgesFilePass(context.Background(), s, reg, edgesFile{Box: "annie", Region: "", Placement: "", DataBucket: "", DataBucketRegion: ""}, "", &last); err != nil {
165 t.Fatal(err)
166 }
167
168 got, _, err := s.Get(context.Background(), "control/edges/annie.json")
169 if err != nil {
170 t.Fatal(err)
171 }
172 var f edgesFile
173 if err := json.Unmarshal(got, &f); err != nil {
174 t.Fatal(err)
175 }
176 if f.EIP != "1.2.3.4" {
177 t.Fatalf("eip after a blank probe = %q, want held value %q", f.EIP, "1.2.3.4")
178 }
179}
180
181// FINDING 2 (review, 2026-08-22, ruling): edge.Reload (internal/edge/edge.go)
182// skips any route whose Status != "ready" — that gate is the deployment's
183// own definition of "serves", so a pending/creating/failed row must not
184// claim role:"home" here either. Hibernating routes stay Status "ready",
185// so this filter excludes nothing a remote backhaul could actually reach.
186func TestEdgesFileExcludesNonReadyRows(t *testing.T) {
187 s, reg := release.NewDirStore(t.TempDir()), edgesTestRegistry(t)
188 seedRoute(t, reg, "ready.example.com")
189 if err := reg.Upsert(registry.Route{Host: "creating.example.com", Kind: "service", Addr: "10.0.0.2:3000", Status: "creating"}); err != nil {
190 t.Fatal(err)
191 }
192
193 var last []byte
194 if err := edgesFilePass(context.Background(), s, reg, edgesFile{Box: "annie", Region: "", Placement: "", DataBucket: "", DataBucketRegion: ""}, "1.2.3.4", &last); err != nil {
195 t.Fatal(err)
196 }
197 got, _, err := s.Get(context.Background(), "control/edges/annie.json")
198 if err != nil {
199 t.Fatal(err)
200 }
201 var f edgesFile
202 if err := json.Unmarshal(got, &f); err != nil {
203 t.Fatal(err)
204 }
205 if _, ok := f.Hosts["ready.example.com"]; !ok {
206 t.Fatalf("hosts = %+v, want ready.example.com present", f.Hosts)
207 }
208 if _, ok := f.Hosts["creating.example.com"]; ok {
209 t.Fatalf("hosts = %+v, want creating.example.com ABSENT (not ready, nothing serves it yet)", f.Hosts)
210 }
211}
212
213// A sidecar row is READY — that is what makes it runnable — so the readiness
214// filter above lets it straight through, and this file is published to every
215// box in the deployment. That matters more than it looks: a REMOTE edge has
216// no sidecar row and never sees a Kind, so edge.Reload's refusal cannot
217// protect it. It misses on its own upstream lookup, reads this file through
218// the home map, and backhauls — dialing the home box with the sidecar's own
219// name as SNI. The same entry also satisfies deploymentHost, which is what
220// admits a name to the cert machinery.
221//
222// So the guarantee has to be made by the WRITER, exactly as
223// publishResidentFleetBindings makes it: a sidecar is never advertised as
224// served, by this box or any other.
225func TestEdgesFileNeverPublishesASidecar(t *testing.T) {
226 s, reg := release.NewDirStore(t.TempDir()), edgesTestRegistry(t)
227 seedRoute(t, reg, "ready.example.com")
228 // Minted the way the sidecar reconcile pass mints one: ready, and
229 // carrying neither socket nor addr, because nothing dials it.
230 if err := reg.Upsert(registry.Route{
231 Host: "bot.eleven.bdm.sidecar.internal", Kind: "sidecar",
232 App: "eleven", Account: "bdm", Status: "ready", Provisioned: true,
233 }); err != nil {
234 t.Fatal(err)
235 }
236
237 var last []byte
238 if err := edgesFilePass(context.Background(), s, reg, edgesFile{Box: "annie"}, "1.2.3.4", &last); err != nil {
239 t.Fatal(err)
240 }
241 got, _, err := s.Get(context.Background(), "control/edges/annie.json")
242 if err != nil {
243 t.Fatal(err)
244 }
245 var f edgesFile
246 if err := json.Unmarshal(got, &f); err != nil {
247 t.Fatal(err)
248 }
249 if _, ok := f.Hosts["ready.example.com"]; !ok {
250 t.Fatalf("hosts = %+v, want the ordinary route still published", f.Hosts)
251 }
252 if _, ok := f.Hosts["bot.eleven.bdm.sidecar.internal"]; ok {
253 t.Fatalf("hosts = %+v, want the sidecar ABSENT — publishing it makes every remote edge a backhaul door onto a member nothing may dial", f.Hosts)
254 }
255}
256
257// FINDING 3 (review, 2026-08-22): a catch-all claim's registry row is a real
258// "*.parent" host (Task 3's reconcile pass derives it the same way
259// edge.go's upstream map does). This writer performs no wildcard expansion
260// — the row's Host lands in Hosts verbatim, exactly like every other row —
261// so an exact-match reader must apply edge.go's lookupUpstream one-label
262// rule itself; that contract is documented at the call site and in the
263// spec, not enforced here.
264func TestEdgesFileWildcardRowPublishesVerbatim(t *testing.T) {
265 s, reg := release.NewDirStore(t.TempDir()), edgesTestRegistry(t)
266 if err := reg.Upsert(registry.Route{Host: "*.example.com", Kind: "service", Addr: "10.0.0.3:3000", Status: "ready"}); err != nil {
267 t.Fatal(err)
268 }
269
270 var last []byte
271 if err := edgesFilePass(context.Background(), s, reg, edgesFile{Box: "annie", Region: "", Placement: "", DataBucket: "", DataBucketRegion: ""}, "1.2.3.4", &last); err != nil {
272 t.Fatal(err)
273 }
274 got, _, err := s.Get(context.Background(), "control/edges/annie.json")
275 if err != nil {
276 t.Fatal(err)
277 }
278 var f edgesFile
279 if err := json.Unmarshal(got, &f); err != nil {
280 t.Fatal(err)
281 }
282 if r, ok := f.Hosts["*.example.com"]; !ok || r.Role != "home" {
283 t.Fatalf("hosts = %+v, want \"*.example.com\" present verbatim with role home", f.Hosts)
284 }
285}
286
287// FINDING 4 (review, 2026-08-22): every other test in this file round-trips
288// through the edgesFile struct on both the write and the assert side, so a
289// json-tag rename (e.g. "eip" -> "publicIP") would stay green everywhere
290// while breaking Task 3's parser, which reads the wire bytes independently
291// of this package's Go types. This test pins the RAW body against a
292// hand-written literal matching the brief's exact schema string — every
293// byte is fixed except the generation timestamp, which this fixed
294// signature has no clock seam to inject (edgesFilePass takes no `now`
295// parameter, per the brief's pinned interface); that one field is read
296// back and re-inserted into the literal rather than relaxed by a regex.
297func TestEdgesFileRawWireBodyMatchesSchema(t *testing.T) {
298 s, reg := release.NewDirStore(t.TempDir()), edgesTestRegistry(t)
299 seedRoute(t, reg, "app.example.com")
300
301 var last []byte
302 if err := edgesFilePass(context.Background(), s, reg, edgesFile{Box: "quiet-harbour", Region: "", Placement: "", DataBucket: "", DataBucketRegion: ""}, "203.0.113.10", &last); err != nil {
303 t.Fatal(err)
304 }
305 got, _, err := s.Get(context.Background(), "control/edges/quiet-harbour.json")
306 if err != nil {
307 t.Fatal(err)
308 }
309
310 var probe struct {
311 Generation string `json:"generation"`
312 }
313 if err := json.Unmarshal(got, &probe); err != nil {
314 t.Fatal(err)
315 }
316 if _, err := time.Parse(time.RFC3339, probe.Generation); err != nil {
317 t.Fatalf("generation %q is not RFC3339: %v", probe.Generation, err)
318 }
319
320 want := `{"v":1,"box":"quiet-harbour","eip":"203.0.113.10","hibernation_configured":false,"serves_shared_pool":false,"accepts_inbound":false,"moves_protocol":0,"seeds_protocol":0,"generation":"` +
321 probe.Generation + `","hosts":{"app.example.com":{"role":"home","platform":false}}}`
322 if string(got) != want {
323 t.Fatalf("raw wire body =\n%s\nwant\n%s", got, want)
324 }
325}
326
327// FINDING 5 (task 1, 2026-08-22): region is additive to the pinned schema —
328// this extends FINDING 4's raw-body pin rather than replacing it. Two
329// bodies, byte for byte: an unresolved region (edgesFileRegion's "" case —
330// off EC2, no CARLOS_AWS_REGION override) must produce the IDENTICAL bytes
331// FINDING 4 already pins (no "region" key at all, omitempty), and a
332// resolved region must insert exactly `"region":"eu-west-1"` between "eip"
333// and "generation" — the struct field order — with nothing else disturbed.
334// This test drives edgesFilePass directly with the resolved value; the
335// resolution contract itself (override wins, IMDS otherwise, trimmed) is
336// pinned separately in TestEdgesFileRegionResolution.
337func TestEdgesFileRawWireBodyRegionField(t *testing.T) {
338 t.Parallel()
339 s, reg := release.NewDirStore(t.TempDir()), edgesTestRegistry(t)
340 seedRoute(t, reg, "app.example.com")
341
342 genOf := func(t *testing.T, body []byte) string {
343 t.Helper()
344 var probe struct {
345 Generation string `json:"generation"`
346 }
347 if err := json.Unmarshal(body, &probe); err != nil {
348 t.Fatal(err)
349 }
350 return probe.Generation
351 }
352
353 t.Run("unset region is omitted entirely", func(t *testing.T) {
354 var last []byte
355 if err := edgesFilePass(context.Background(), s, reg, edgesFile{Box: "quiet-harbour", Region: "", Placement: "", DataBucket: "", DataBucketRegion: ""}, "203.0.113.10", &last); err != nil {
356 t.Fatal(err)
357 }
358 got, _, err := s.Get(context.Background(), "control/edges/quiet-harbour.json")
359 if err != nil {
360 t.Fatal(err)
361 }
362 gen := genOf(t, got)
363 want := `{"v":1,"box":"quiet-harbour","eip":"203.0.113.10","hibernation_configured":false,"serves_shared_pool":false,"accepts_inbound":false,"moves_protocol":0,"seeds_protocol":0,"generation":"` +
364 gen + `","hosts":{"app.example.com":{"role":"home","platform":false}}}`
365 if string(got) != want {
366 t.Fatalf("raw wire body (region unset) =\n%s\nwant\n%s", got, want)
367 }
368 })
369
370 t.Run("set region is inserted between eip and generation", func(t *testing.T) {
371 s2, reg2 := release.NewDirStore(t.TempDir()), edgesTestRegistry(t)
372 seedRoute(t, reg2, "app.example.com")
373 var last []byte
374 if err := edgesFilePass(context.Background(), s2, reg2, edgesFile{Box: "quiet-harbour", Region: "eu-west-1", Placement: "", DataBucket: "", DataBucketRegion: ""}, "203.0.113.10", &last); err != nil {
375 t.Fatal(err)
376 }
377 got, _, err := s2.Get(context.Background(), "control/edges/quiet-harbour.json")
378 if err != nil {
379 t.Fatal(err)
380 }
381 gen := genOf(t, got)
382 want := `{"v":1,"box":"quiet-harbour","eip":"203.0.113.10","region":"eu-west-1","hibernation_configured":false,"serves_shared_pool":false,"accepts_inbound":false,"moves_protocol":0,"seeds_protocol":0,"generation":"` +
383 gen + `","hosts":{"app.example.com":{"role":"home","platform":false}}}`
384 if string(got) != want {
385 t.Fatalf("raw wire body (region set) =\n%s\nwant\n%s", got, want)
386 }
387 })
388}
389
390// Plan re-ruling (2026-08-23, review catch): CARLOS_REGION is the CARLOS
391// placement code ("ie"), NOT an AWS region — Task 5's
392// ChangeResourceRecordSets would reject it batch-wide, so the field's
393// value must come from CARLOS_AWS_REGION / IMDS instead, never
394// CARLOS_REGION. edgesFileRegion is the pure half of that contract
395// (getenv/probe-injectable, domainsTickConfig's own shape) — this pins
396// override-wins, unset-and-no-probe-answer → "", and trimming on both
397// inputs, all without a live IMDS server.
398func TestEdgesFileRegionResolution(t *testing.T) {
399 env := func(m map[string]string) func(string) string {
400 return func(k string) string { return m[k] }
401 }
402
403 cases := []struct {
404 name string
405 env map[string]string
406 probed string
407 want string
408 }{
409 {
410 name: "override set wins over the probed value",
411 env: map[string]string{"CARLOS_AWS_REGION": "eu-west-1"},
412 probed: "us-east-1",
413 want: "eu-west-1",
414 },
415 {
416 name: "override set, untrimmed, wins and is trimmed",
417 env: map[string]string{"CARLOS_AWS_REGION": " eu-west-1 "},
418 probed: "us-east-1",
419 want: "eu-west-1",
420 },
421 {
422 name: "override unset falls back to the probed value",
423 env: map[string]string{},
424 probed: "eu-west-1",
425 want: "eu-west-1",
426 },
427 {
428 name: "override unset, probed value untrimmed, is trimmed",
429 env: map[string]string{},
430 probed: " eu-west-1 ",
431 want: "eu-west-1",
432 },
433 {
434 name: "CARLOS_REGION (the placement code) is never consulted",
435 env: map[string]string{"CARLOS_REGION": "ie"},
436 probed: "",
437 want: "",
438 },
439 {
440 name: "unset override and no probe answer (off EC2) is empty, not an error",
441 env: map[string]string{},
442 probed: "",
443 want: "",
444 },
445 {
446 name: "blank override (set but empty) does not shadow the probed value",
447 env: map[string]string{"CARLOS_AWS_REGION": ""},
448 probed: "eu-west-1",
449 want: "eu-west-1",
450 },
451 }
452 for _, c := range cases {
453 t.Run(c.name, func(t *testing.T) {
454 if got := edgesFileRegion(env(c.env), c.probed); got != c.want {
455 t.Fatalf("edgesFileRegion() = %q, want %q", got, c.want)
456 }
457 })
458 }
459}
460
461// FINDING 6 (task 1, 2026-08-22, the plan-2 final review's Important 1): a
462// freshly ARMED loop must seed its change-gate from whatever this box last
463// published, or two bugs follow — see seedEdgesFileGate's doc. This test
464// pins the seed-then-pass contract directly: a process that primes its
465// *last from seedEdgesFileGate, then runs a pass against UNCHANGED registry
466// state, must write ZERO times. Before this task, edgesFileLoop always
467// started with a bare `var last []byte`, so the very first tick after any
468// restart always wrote — this is the regression that closes.
469func TestEdgesFileSeedGateSkipsWriteOnUnchangedBucketState(t *testing.T) {
470 real := release.NewDirStore(t.TempDir())
471 reg := edgesTestRegistry(t)
472 seedRoute(t, reg, "app.example.com")
473
474 // A prior process (or the prior pass of this one) already published.
475 var priming []byte
476 if err := edgesFilePass(context.Background(), real, reg, edgesFile{Box: "annie", Region: "", Placement: "", DataBucket: "", DataBucketRegion: ""}, "1.2.3.4", &priming); err != nil {
477 t.Fatal(err)
478 }
479
480 // A fresh process arms: seed the gate from the bucket, as edgesFileLoop
481 // now does at loop arm.
482 last := seedEdgesFileGate(context.Background(), real, "annie")
483 if last == nil {
484 t.Fatal("seedEdgesFileGate returned nil, want the prior file's gate body")
485 }
486
487 counting := &putCountingStore{Store: real}
488 if err := edgesFilePass(context.Background(), counting, reg, edgesFile{Box: "annie", Region: "", Placement: "", DataBucket: "", DataBucketRegion: ""}, "1.2.3.4", &last); err != nil {
489 t.Fatal(err)
490 }
491 if counting.puts != 0 {
492 t.Fatalf("puts = %d, want 0 — a seeded gate against unchanged state must not write", counting.puts)
493 }
494}
495
496// The restart-blip case named in the final review: a process restarts,
497// arms, and its very FIRST tick hits both a transient IMDS blank AND a real
498// registry change (so the pass must write). The write must still carry the
499// bucket's held EIP, not the blank probe — seedEdgesFileGate's dual
500// contract (gate AND EIP hold) has to survive an ACTUAL write, not just a
501// no-op pass, or the hold half of the fix is unverified.
502func TestEdgesFileSeedHoldsEIPThroughRestartBlip(t *testing.T) {
503 s, reg := release.NewDirStore(t.TempDir()), edgesTestRegistry(t)
504 seedRoute(t, reg, "app.example.com")
505
506 var priming []byte
507 if err := edgesFilePass(context.Background(), s, reg, edgesFile{Box: "annie", Region: "", Placement: "", DataBucket: "", DataBucketRegion: ""}, "1.2.3.4", &priming); err != nil {
508 t.Fatal(err)
509 }
510
511 last := seedEdgesFileGate(context.Background(), s, "annie")
512 if last == nil {
513 t.Fatal("seedEdgesFileGate returned nil, want the prior file's gate body")
514 }
515
516 // This tick both hits a transient IMDS blip AND sees a real registry
517 // change, so the pass must write.
518 seedRoute(t, reg, "second.example.com")
519 if err := edgesFilePass(context.Background(), s, reg, edgesFile{Box: "annie", Region: "", Placement: "", DataBucket: "", DataBucketRegion: ""}, "", &last); err != nil {
520 t.Fatal(err)
521 }
522
523 got, _, err := s.Get(context.Background(), "control/edges/annie.json")
524 if err != nil {
525 t.Fatal(err)
526 }
527 var f edgesFile
528 if err := json.Unmarshal(got, &f); err != nil {
529 t.Fatal(err)
530 }
531 if f.EIP != "1.2.3.4" {
532 t.Fatalf("eip after restart + blank probe = %q, want held %q", f.EIP, "1.2.3.4")
533 }
534 if _, ok := f.Hosts["second.example.com"]; !ok {
535 t.Fatalf("hosts = %+v, want second.example.com present — the write must carry the real registry change too", f.Hosts)
536 }
537}
538
539// Without the seed, a fresh *last is nil and edgesFilePass's own hold guard
540// (`*last != nil`) cannot fire — this pins that this really is the bug the
541// two tests above close, not a tautology of the hold logic that would pass
542// regardless of seeding.
543func TestEdgesFileUnseededLastPublishesBlankEIPOnFirstTickBlip(t *testing.T) {
544 s, reg := release.NewDirStore(t.TempDir()), edgesTestRegistry(t)
545 seedRoute(t, reg, "app.example.com")
546
547 var priming []byte
548 if err := edgesFilePass(context.Background(), s, reg, edgesFile{Box: "annie", Region: "", Placement: "", DataBucket: "", DataBucketRegion: ""}, "1.2.3.4", &priming); err != nil {
549 t.Fatal(err)
550 }
551
552 // The pre-fix shape: a fresh process's *last starts nil (no seed read).
553 var unseeded []byte
554 if err := edgesFilePass(context.Background(), s, reg, edgesFile{Box: "annie", Region: "", Placement: "", DataBucket: "", DataBucketRegion: ""}, "", &unseeded); err != nil {
555 t.Fatal(err)
556 }
557
558 got, _, err := s.Get(context.Background(), "control/edges/annie.json")
559 if err != nil {
560 t.Fatal(err)
561 }
562 var f edgesFile
563 if err := json.Unmarshal(got, &f); err != nil {
564 t.Fatal(err)
565 }
566 if f.EIP != "" {
567 t.Fatalf("eip = %q, want blank — demonstrating why the seed in the tests above is load-bearing, not incidental", f.EIP)
568 }
569}
570
571// Self-review: does the seed interact correctly with the change gate when
572// the bucket file PREDATES the region field? A box upgraded to this build
573// finds a bucket file an older build left with no "region" key at all.
574// Seeding *last from it must not treat this build's first pass — the one
575// that finally sets a real region — as unchanged: Region participates in
576// the change gate exactly like every other field (edgesFilePass's doc), so
577// a seeded pre-region gate and a fresh body WITH region differ in bytes and
578// must write exactly once, not zero and not on every tick after.
579func TestEdgesFileSeedPreRegionBucketWritesOnceWhenRegionArrives(t *testing.T) {
580 real := release.NewDirStore(t.TempDir())
581 reg := edgesTestRegistry(t)
582 seedRoute(t, reg, "app.example.com")
583
584 // Hand-written exactly as a pre-region build would have left it in the
585 // bucket: no "region" key present.
586 preRegion := []byte(`{"v":1,"box":"annie","eip":"1.2.3.4","generation":"2026-08-01T00:00:00Z","hosts":{"app.example.com":{"role":"home"}}}`)
587 if err := putReplacing(context.Background(), real, edgesFileKey("annie"), preRegion); err != nil {
588 t.Fatal(err)
589 }
590
591 last := seedEdgesFileGate(context.Background(), real, "annie")
592 if last == nil {
593 t.Fatal("seedEdgesFileGate returned nil")
594 }
595
596 // This build now has CARLOS_REGION set: the first pass must write, once.
597 counting := &putCountingStore{Store: real}
598 if err := edgesFilePass(context.Background(), counting, reg, edgesFile{Box: "annie", Region: "eu-west-1", Placement: "", DataBucket: "", DataBucketRegion: ""}, "1.2.3.4", &last); err != nil {
599 t.Fatal(err)
600 }
601 if counting.puts != 1 {
602 t.Fatalf("puts = %d, want 1 — a region arriving on a seeded pre-region body is a real change", counting.puts)
603 }
604 got, _, err := real.Get(context.Background(), edgesFileKey("annie"))
605 if err != nil {
606 t.Fatal(err)
607 }
608 var f edgesFile
609 if err := json.Unmarshal(got, &f); err != nil {
610 t.Fatal(err)
611 }
612 if f.Region != "eu-west-1" {
613 t.Fatalf("region = %q, want eu-west-1", f.Region)
614 }
615
616 // A second, identical-region pass must not write again — the gate now
617 // includes region on both sides.
618 if err := edgesFilePass(context.Background(), counting, reg, edgesFile{Box: "annie", Region: "eu-west-1", Placement: "", DataBucket: "", DataBucketRegion: ""}, "1.2.3.4", &last); err != nil {
619 t.Fatal(err)
620 }
621 if counting.puts != 1 {
622 t.Fatalf("puts after unchanged second pass = %d, want still 1", counting.puts)
623 }
624}
625
626// The class mark (#300): platform-minted rows carry platform:true; a stray
627// operator row does not, so no remote edge derives a wildcard parent from it.
628func TestEdgesFileMarksPlatformRows(t *testing.T) {
629 s, reg := release.NewDirStore(t.TempDir()), edgesTestRegistry(t)
630 for _, r := range []registry.Route{
631 {Host: "app.acct.apps.test", Kind: "service", Addr: "10.0.0.1:3000", Status: "ready", Platform: true},
632 {Host: "prov.acct.apps.test", Kind: "service", Addr: "10.0.0.1:3001", Status: "ready", Provisioned: true},
633 {Host: "stray.acct.apps.test", Kind: "service", Addr: "10.0.0.1:3002", Status: "ready"},
634 {Host: "pending.acct.apps.test", Kind: "service", Addr: "10.0.0.1:3003", Status: "pending", Platform: true},
635 } {
636 if err := reg.Upsert(r); err != nil {
637 t.Fatal(err)
638 }
639 }
640 var lastBody []byte
641 if err := edgesFilePass(context.Background(), s, reg, edgesFile{Box: "annie", Region: "", Placement: "", DataBucket: "", DataBucketRegion: ""}, "1.2.3.4", &lastBody); err != nil {
642 t.Fatal(err)
643 }
644 body, _, err := s.Get(context.Background(), "control/edges/annie.json")
645 if err != nil {
646 t.Fatal(err)
647 }
648 var f struct {
649 Hosts map[string]struct {
650 Platform bool `json:"platform"`
651 } `json:"hosts"`
652 }
653 if err := json.Unmarshal(body, &f); err != nil {
654 t.Fatal(err)
655 }
656 for host, want := range map[string]bool{"app.acct.apps.test": true, "prov.acct.apps.test": true, "stray.acct.apps.test": false} {
657 if got := f.Hosts[host].Platform; got != want {
658 t.Errorf("%s platform = %v; want %v", host, got, want)
659 }
660 }
661 if _, ok := f.Hosts["pending.acct.apps.test"]; ok {
662 t.Error("a non-ready row is published regardless of class")
663 }
664 // The mark is always written, so a keyless host object can only mean a
665 // pre-mark publisher.
666 if !strings.Contains(string(body), `"stray.acct.apps.test":{"role":"home","platform":false}`) {
667 t.Errorf("mark not emitted for an unclassed row: %s", body)
668 }
669}
670
671// #287: attached custom domains are published under their own names —
672// only when their primary is served AND their DNS points here, marked as
673// aliases, never platform-class, catch-alls never, and never over a real
674// route. Detaching one republishes without it.
675func TestEdgesFilePublishesPointingAliasesOfServedPrimaries(t *testing.T) {
676 withheldAliasSaid = sync.Map{} // once per PROCESS: isolate from -count>1
677 s, reg := release.NewDirStore(t.TempDir()), edgesTestRegistry(t)
678 if err := reg.Upsert(registry.Route{Host: "app.acct.apps.test", Kind: "service", Addr: "10.0.0.1:3000", Status: "ready", Platform: true}); err != nil {
679 t.Fatal(err)
680 }
681 if err := reg.Upsert(registry.Route{Host: "pending.acct.apps.test", Kind: "service", Addr: "10.0.0.1:3001", Status: "pending"}); err != nil {
682 t.Fatal(err)
683 }
684 if err := reg.Upsert(registry.Route{Host: "real.example.net", Kind: "service", Addr: "10.0.0.1:3002", Status: "ready", Platform: true}); err != nil {
685 t.Fatal(err)
686 }
687 for _, a := range []registry.RouteAlias{
688 {Alias: "www.customer.com", Host: "app.acct.apps.test", Account: "acct", App: "app", DNSStatus: "pointing"},
689 {Alias: "unpointed.customer.com", Host: "app.acct.apps.test", Account: "acct", App: "app", DNSStatus: "elsewhere"},
690 {Alias: "fresh.customer.com", Host: "app.acct.apps.test", Account: "acct", App: "app"},
691 {Alias: "*.customer.com", Host: "app.acct.apps.test", Account: "acct", App: "app", DNSStatus: "pointing"},
692 {Alias: "orphan.example.org", Host: "pending.acct.apps.test", Account: "acct", App: "pending", DNSStatus: "pointing"},
693 {Alias: "real.example.net", Host: "app.acct.apps.test", Account: "acct", App: "app", DNSStatus: "pointing"}, // shadowed
694 } {
695 if err := reg.UpsertAlias(a); err != nil {
696 t.Fatal(err)
697 }
698 }
699 type hostView struct {
700 Role string `json:"role"`
701 Platform bool `json:"platform"`
702 Alias bool `json:"alias"`
703 }
704 publish := func() map[string]hostView {
705 t.Helper()
706 var lastBody []byte
707 if err := edgesFilePass(context.Background(), s, reg, edgesFile{Box: "annie", Region: "", Placement: "", DataBucket: "", DataBucketRegion: ""}, "1.2.3.4", &lastBody); err != nil {
708 t.Fatal(err)
709 }
710 body, _, err := s.Get(context.Background(), "control/edges/annie.json")
711 if err != nil {
712 t.Fatal(err)
713 }
714 var f struct {
715 Hosts map[string]hostView `json:"hosts"`
716 }
717 if err := json.Unmarshal(body, &f); err != nil {
718 t.Fatal(err)
719 }
720 return f.Hosts
721 }
722 var buf strings.Builder
723 log.SetOutput(&buf)
724 t.Cleanup(func() { log.SetOutput(os.Stderr) })
725 hosts := publish()
726 h, ok := hosts["www.customer.com"]
727 if !ok || h.Role != "home" || !h.Alias || h.Platform {
728 t.Errorf("www.customer.com = %+v, %v; want home, alias, not platform", h, ok)
729 }
730 for _, absent := range []string{"unpointed.customer.com", "fresh.customer.com", "*.customer.com", "orphan.example.org"} {
731 if _, ok := hosts[absent]; ok {
732 t.Errorf("%s must not be published", absent)
733 }
734 }
735 if h := hosts["real.example.net"]; !h.Platform || h.Alias {
736 t.Errorf("an alias colliding with a real route displaced the route's entry: %+v", h)
737 }
738 // A withheld alias says so once per process, not once per pass.
739 publish()
740 publish()
741 if n := strings.Count(buf.String(), "unpointed.customer.com not published"); n != 1 {
742 t.Errorf("withheld line logged %d times over 3 passes; want 1:\n%s", n, buf.String())
743 }
744 // Detach: gone on the next pass.
745 if err := reg.DeleteAlias("www.customer.com"); err != nil {
746 t.Fatal(err)
747 }
748 if _, ok := publish()["www.customer.com"]; ok {
749 t.Error("a detached alias was still published")
750 }
751}
752
753// #305: delegated customer wildcard parents publish as "*.<parent>" in their
754// own class — never platform, never over an existing key.
755func TestEdgesFilePublishesDelegatedWildcardParents(t *testing.T) {
756 s, reg := release.NewDirStore(t.TempDir()), edgesTestRegistry(t)
757 seedRoute(t, reg, "app.acct.apps.test")
758 if err := reg.Upsert(registry.Route{Host: "*.taken.example", Kind: "service", Addr: "10.0.0.1:3001", Status: "ready", Platform: true}); err != nil {
759 t.Fatal(err)
760 }
761 // customer.example has a served catch-all (Catchall claim) → dial half
762 // too; certonly.example is a Wildcard claim without Catchall → cert
763 // half only; pending.example's catch-all points at an unserved primary.
764 if err := reg.Upsert(registry.Route{Host: "pend.acct.apps.test", Kind: "service", Addr: "10.0.0.1:3002", Status: "pending"}); err != nil {
765 t.Fatal(err)
766 }
767 for _, a := range []registry.RouteAlias{
768 {Alias: "*.customer.example", Host: "app.acct.apps.test", Account: "acct", App: "app"},
769 {Alias: "*.pending.example", Host: "pend.acct.apps.test", Account: "acct", App: "pend"},
770 } {
771 if err := reg.UpsertAlias(a); err != nil {
772 t.Fatal(err)
773 }
774 }
775 for _, w := range []registry.WildcardDomain{
776 {Host: "customer.example", Account: "acct", App: "app"},
777 {Host: "certonly.example", Account: "acct", App: "app"},
778 {Host: "pending.example", Account: "acct", App: "pend"},
779 {Host: "*.odd.example", Account: "acct", App: "app"}, // never written by the pass; refused here too
780 {Host: "taken.example", Account: "acct", App: "app"}, // a real route owns the key
781 } {
782 if err := reg.UpsertWildcardDomain(w); err != nil {
783 t.Fatal(err)
784 }
785 }
786 var lastBody []byte
787 if err := edgesFilePass(context.Background(), s, reg, edgesFile{Box: "annie", Region: "", Placement: "", DataBucket: "", DataBucketRegion: ""}, "1.2.3.4", &lastBody); err != nil {
788 t.Fatal(err)
789 }
790 body, _, err := s.Get(context.Background(), "control/edges/annie.json")
791 if err != nil {
792 t.Fatal(err)
793 }
794 var f struct {
795 Hosts map[string]struct {
796 Role string `json:"role"`
797 Platform bool `json:"platform"`
798 Wildcard bool `json:"wildcard"`
799 Alias bool `json:"alias"`
800 } `json:"hosts"`
801 }
802 if err := json.Unmarshal(body, &f); err != nil {
803 t.Fatal(err)
804 }
805 // Marked alias BECAUSE the dial half is on: a catch-all we serve is a
806 // materialized claim, and DeploymentRoutes keys the whole "is this
807 // somebody's real route" question on that mark. See the end-to-end
808 // tripwire below for what an unmarked row does to the claim.
809 h, ok := f.Hosts["*.customer.example"]
810 if !ok || h.Role != "home" || !h.Wildcard || h.Platform || !h.Alias {
811 t.Fatalf("*.customer.example = %+v, %v; want home, wildcard, alias, not platform", h, ok)
812 }
813 for _, name := range []string{"*.certonly.example", "*.pending.example"} {
814 h, ok := f.Hosts[name]
815 if !ok || h.Role != "wildcard" || !h.Wildcard || h.Platform || h.Alias {
816 t.Errorf("%s = %+v, %v; want the cert half only (role wildcard, unmarked)", name, h, ok)
817 }
818 }
819 if _, ok := f.Hosts["*.*.odd.example"]; ok {
820 t.Error("a malformed wildcard_domains row was published")
821 }
822 // A real route owns the key: it keeps role and class, and the parent's
823 // class is added — the reader must still load the PEM everywhere. It
824 // stays UNMARKED, which is the other half of the mark's meaning: this
825 // hostname really is served as a route, so it must go on outranking any
826 // claim another box makes on the same name.
827 if h := f.Hosts["*.taken.example"]; !h.Platform || !h.Wildcard || h.Role != "home" || h.Alias {
828 t.Errorf("route-owned key = %+v; want platform AND wildcard, role home, unmarked", h)
829 }
830}
831
832// The tripwire on a loop that ran in production: a served catch-all published
833// without the alias mark is read back by DeploymentRoutes as evidence that
834// some live box serves that hostname as a REAL route, so the next
835// domains.ReconcileAliases pass refuses the very claim that produced the row
836// and deletes the alias. Deleting it turns the dial half off, which drops the
837// row, which lets the pass after that recreate it — and the name flaps
838// on/off forever (*.woodstar.app, every 5-6 minutes, down about half the
839// time). Neither half is wrong alone, so only an end-to-end check catches it:
840// publish with the real publisher, then fold with the real reader.
841func TestEdgesFileServedCatchAllIsNotItsOwnDeploymentRoute(t *testing.T) {
842 s, reg := release.NewDirStore(t.TempDir()), edgesTestRegistry(t)
843 seedRoute(t, reg, "app.acct.apps.test")
844 if err := reg.UpsertAlias(registry.RouteAlias{
845 Alias: "*.customer.example", Host: "app.acct.apps.test", Account: "acct", App: "app",
846 }); err != nil {
847 t.Fatal(err)
848 }
849 if err := reg.UpsertWildcardDomain(registry.WildcardDomain{
850 Host: "customer.example", Account: "acct", App: "app",
851 }); err != nil {
852 t.Fatal(err)
853 }
854 var lastBody []byte
855 if err := edgesFilePass(context.Background(), s, reg, edgesFile{Box: "annie"}, "1.2.3.4", &lastBody); err != nil {
856 t.Fatal(err)
857 }
858 now := time.Now()
859 if err := status.WriteBox(context.Background(), s, status.Box{Label: "annie", GeneratedAt: now}); err != nil {
860 t.Fatal(err)
861 }
862
863 routes := edgesfile.DeploymentRoutes(context.Background(), s, now)
864 if routes["*.customer.example"] {
865 t.Errorf("routes = %v, want the served catch-all NOT counted as a real route — counting it makes the claim refuse itself every pass", routes)
866 }
867 // The join really happened: without this, an empty set would pass the
868 // assertion above for the wrong reason (a snapshot/label mismatch reads
869 // as "nothing known", which is permissive by contract).
870 if !routes["app.acct.apps.test"] {
871 t.Fatalf("routes = %v, want the box's real route counted — the fold did not join", routes)
872 }
873}
874
875// The whole EIP fold rests on ONE identity: the `box` name this loop
876// publishes under and the `label` the status snapshot is written under must
877// be the same string, or edgesfile.EdgeIPs' liveness join drops every edge
878// silently and the deployment's edge set degrades to env-only forever — a
879// failure with no error, no log line and no red test.
880//
881// They are the same today because both resolve through metricsBoxLabel over
882// the same machine id: edgesFileLoop calls it directly, and statusTick's
883// snapshot label is status.Label(Probes.MachineID), which is exactly what
884// metricsBoxLabel falls through to off a fleet box. (A fleet box never gets
885// here: edgesFileLoop returns early on shouldSkipPublisherElection.) This
886// test is the tripwire on that, end to end through the real fold.
887func TestEdgesFileBoxNameJoinsTheStatusSnapshotLabel(t *testing.T) {
888 const machineID = "ip-10-0-0-7"
889 notAFleetBox := func(string) string { return "" }
890
891 box := metricsBoxLabel(notAFleetBox, machineID)
892 snapshot := status.CollectBox(nil, status.Probes{MachineID: machineID}, time.Now())
893 if box != snapshot.Label {
894 t.Fatalf("edges file box %q vs status snapshot label %q — the fold's liveness join needs one name", box, snapshot.Label)
895 }
896
897 ctx := context.Background()
898 s, reg := release.NewDirStore(t.TempDir()), edgesTestRegistry(t)
899 seedRoute(t, reg, "app.example.com")
900 var last []byte
901 if err := edgesFilePass(ctx, s, reg, edgesFile{Box: box, Region: "eu-west-1", Placement: "", DataBucket: "", DataBucketRegion: ""}, "203.0.113.7", &last); err != nil {
902 t.Fatal(err)
903 }
904 if err := status.WriteBox(ctx, s, snapshot); err != nil {
905 t.Fatal(err)
906 }
907 got := edgesfile.EdgeIPs(ctx, s, nil, time.Now())
908 if len(got) != 1 || got[0] != "203.0.113.7" {
909 t.Fatalf("fold = %v, want this box's own EIP — the two writers disagree about its name", got)
910 }
911}
912
913// The placement code (issue #329): CARLOS_REGION verbatim, additive between
914// region and generation, omitted entirely when the box has none — the
915// console's armed-placement gate stays dormant on a fleet that never
916// publishes it, which is what makes roll order irrelevant.
917func TestEdgesFileRawWireBodyPlacementField(t *testing.T) {
918 t.Parallel()
919 s, reg := release.NewDirStore(t.TempDir()), edgesTestRegistry(t)
920 seedRoute(t, reg, "app.example.com")
921 var last []byte
922 if err := edgesFilePass(context.Background(), s, reg, edgesFile{Box: "quiet-harbour", Region: "eu-west-2", Placement: "uk", DataBucket: "", DataBucketRegion: ""}, "203.0.113.10", &last); err != nil {
923 t.Fatal(err)
924 }
925 got, _, err := s.Get(context.Background(), "control/edges/quiet-harbour.json")
926 if err != nil {
927 t.Fatal(err)
928 }
929 var probe struct {
930 Placement string `json:"placement"`
931 Generation string `json:"generation"`
932 }
933 if err := json.Unmarshal(got, &probe); err != nil {
934 t.Fatal(err)
935 }
936 want := `{"v":1,"box":"quiet-harbour","eip":"203.0.113.10","region":"eu-west-2","placement":"uk","hibernation_configured":false,"serves_shared_pool":false,"accepts_inbound":false,"moves_protocol":0,"seeds_protocol":0,"generation":"` +
937 probe.Generation + `","hosts":{"app.example.com":{"role":"home","platform":false}}}`
938 if string(got) != want {
939 t.Fatalf("raw wire body (placement set) =\n%s\nwant\n%s", got, want)
940 }
941
942 // Unset placement publishes no key at all.
943 s2, reg2 := release.NewDirStore(t.TempDir()), edgesTestRegistry(t)
944 seedRoute(t, reg2, "app.example.com")
945 var last2 []byte
946 if err := edgesFilePass(context.Background(), s2, reg2, edgesFile{Box: "quiet-harbour", Region: "", Placement: "", DataBucket: "", DataBucketRegion: ""}, "203.0.113.10", &last2); err != nil {
947 t.Fatal(err)
948 }
949 got2, _, err := s2.Get(context.Background(), "control/edges/quiet-harbour.json")
950 if err != nil {
951 t.Fatal(err)
952 }
953 if strings.Contains(string(got2), "placement") {
954 t.Fatalf("raw wire body with no CARLOS_REGION carries a placement key: %s", got2)
955 }
956}
957
958// Data-bucket coordinates (instance-move spec): published verbatim from the
959// hibernate env pair, absent when unset — the same additive rule as
960// placement.
961func TestEdgesFileRawWireBodyDataBucketFields(t *testing.T) {
962 t.Parallel()
963 s, reg := release.NewDirStore(t.TempDir()), edgesTestRegistry(t)
964 seedRoute(t, reg, "app.example.com")
965 var last []byte
966 if err := edgesFilePass(context.Background(), s, reg, edgesFile{Box: "quiet-harbour", Region: "eu-west-2", Placement: "uk", DataBucket: "carlos-tito-uk-1", DataBucketRegion: "eu-west-2"}, "203.0.113.10", &last); err != nil {
967 t.Fatal(err)
968 }
969 got, _, err := s.Get(context.Background(), "control/edges/quiet-harbour.json")
970 if err != nil {
971 t.Fatal(err)
972 }
973 var probe struct {
974 DataBucket string `json:"data_bucket"`
975 DataBucketRegion string `json:"data_bucket_region"`
976 }
977 if err := json.Unmarshal(got, &probe); err != nil {
978 t.Fatal(err)
979 }
980 if probe.DataBucket != "carlos-tito-uk-1" || probe.DataBucketRegion != "eu-west-2" {
981 t.Fatalf("data bucket fields = %+v", probe)
982 }
983 s2, reg2 := release.NewDirStore(t.TempDir()), edgesTestRegistry(t)
984 seedRoute(t, reg2, "app.example.com")
985 var last2 []byte
986 if err := edgesFilePass(context.Background(), s2, reg2, edgesFile{Box: "quiet-harbour", Region: "", Placement: "", DataBucket: "", DataBucketRegion: ""}, "203.0.113.10", &last2); err != nil {
987 t.Fatal(err)
988 }
989 got2, _, err := s2.Get(context.Background(), "control/edges/quiet-harbour.json")
990 if err != nil {
991 t.Fatal(err)
992 }
993 if strings.Contains(string(got2), "data_bucket") {
994 t.Fatalf("unset hibernate env still published a data_bucket key: %s", got2)
995 }
996}
997
998// --- #162: the replica placement's edges-file entries ----------------------
999
1000// TestEdgesFilePublishesReplicaEntries pins the data plane of replica
1001// placements: a served row minted from a role=replica record — the Replica
1002// mark set, Account/App carried so the account apex derives — publishes its
1003// own host and the account's platform apex under role "home" with the
1004// replica mark set. A PLAIN row never gains the mark, whatever placement the
1005// box publishes; the mark is the row's, not the box's. (Raw map decode, not
1006// the struct: the test is about the WIRE, and a struct field would make an
1007// omitempty typo invisible.)
1008func TestEdgesFilePublishesReplicaEntries(t *testing.T) {
1009 s, reg := release.NewDirStore(t.TempDir()), edgesTestRegistry(t)
1010 if err := reg.Upsert(registry.Route{
1011 Host: "acme-uk.go.tito.io", Slug: "acme-uk", Kind: "service", Addr: "10.0.0.1:3000",
1012 Status: "ready", Account: "bab", App: "tito", Region: "uk",
1013 Replica: true,
1014 }); err != nil {
1015 t.Fatal(err)
1016 }
1017 // The home row, on the same box only for the test's convenience — its
1018 // entries must NOT carry the mark even when it sits here.
1019 if err := reg.Upsert(registry.Route{
1020 Host: "acme.go.tito.io", Slug: "acme", Kind: "service", Addr: "10.0.0.2:3000",
1021 Status: "ready", Account: "bab", App: "tito", Region: "ie",
1022 }); err != nil {
1023 t.Fatal(err)
1024 }
1025
1026 var lastBody []byte
1027 if err := edgesFilePass(context.Background(), s, reg, edgesFile{Box: "ukbox", Placement: "uk"}, "1.2.3.4", &lastBody); err != nil {
1028 t.Fatal(err)
1029 }
1030 got, _, err := s.Get(context.Background(), "control/edges/ukbox.json")
1031 if err != nil {
1032 t.Fatal(err)
1033 }
1034 var raw struct {
1035 Hosts map[string]struct {
1036 Role string `json:"role"`
1037 Replica bool `json:"replica"`
1038 } `json:"hosts"`
1039 }
1040 if err := json.Unmarshal(got, &raw); err != nil {
1041 t.Fatal(err)
1042 }
1043 for _, name := range []string{"acme-uk.go.tito.io"} {
1044 h, ok := raw.Hosts[name]
1045 if !ok {
1046 t.Errorf("replica row published nothing at %s (hosts: %+v)", name, raw.Hosts)
1047 continue
1048 }
1049 if h.Role != "home" || !h.Replica {
1050 t.Errorf("%s = %+v, want role home with the replica mark", name, h)
1051 }
1052 }
1053 if h := raw.Hosts["acme.go.tito.io"]; h.Replica {
1054 t.Errorf("the home row's entry carries the replica mark: %+v — the mark is the ROW's, and the home row is not a replica", h)
1055 }
1056 // The account apex is deliberately NOT published: a name the replica box
1057 // has no local row for must not be claimed (an apex key here would let
1058 // foldHomes' first-sorted-box rule hand the apex dial target to a
1059 // replica box fleet-wide).
1060 if _, ok := raw.Hosts["tito.bab"]; ok {
1061 t.Errorf("replica row published the account apex tito.bab — a name this box has no local row for must not be claimed (hosts: %+v)", raw.Hosts)
1062 }
1063}
1064
1065// TestEdgesFileReplicaMarkFollowsTheRow pins the fail-closed default: a
1066// plain served row publishes exactly as before #162 — no replica key at all
1067// on the wire — on a box with no placement published, with no placement, or
1068// with any placement. The mark cannot leak from the box's identity into an
1069// unrelated row.
1070func TestEdgesFileReplicaMarkFollowsTheRow(t *testing.T) {
1071 s, reg := release.NewDirStore(t.TempDir()), edgesTestRegistry(t)
1072 if err := reg.Upsert(registry.Route{
1073 Host: "acme.go.tito.io", Kind: "service", Addr: "10.0.0.1:3000",
1074 Status: "ready",
1075 }); err != nil {
1076 t.Fatal(err)
1077 }
1078 for _, ident := range []edgesFile{{Box: "annie"}, {Box: "annie", Placement: "uk"}} {
1079 var lastBody []byte
1080 if err := edgesFilePass(context.Background(), s, reg, ident, "1.2.3.4", &lastBody); err != nil {
1081 t.Fatal(err)
1082 }
1083 got, _, err := s.Get(context.Background(), "control/edges/annie.json")
1084 if err != nil {
1085 t.Fatal(err)
1086 }
1087 var raw struct {
1088 Hosts map[string]struct {
1089 Replica bool `json:"replica"`
1090 } `json:"hosts"`
1091 }
1092 if err := json.Unmarshal(got, &raw); err != nil {
1093 t.Fatal(err)
1094 }
1095 if h, ok := raw.Hosts["acme.go.tito.io"]; ok && h.Replica {
1096 t.Errorf("hosts = %+v, want no replica mark on a plain row (box placement %q)", raw.Hosts, ident.Placement)
1097 }
1098 }
1099}
1100
1101// ---- owners (#175 P7, the edge-local door) ---------------------------------
1102
1103// publishHosts runs one pass on box "annie" and reads its hosts back.
1104func publishHosts(t *testing.T, reg *registry.Registry) map[string]edgeHost {
1105 t.Helper()
1106 s := release.NewDirStore(t.TempDir())
1107 var last []byte
1108 if err := edgesFilePass(context.Background(), s, reg, edgesFile{Box: "annie"}, "1.2.3.4", &last); err != nil {
1109 t.Fatal(err)
1110 }
1111 body, _, err := s.Get(context.Background(), "control/edges/annie.json")
1112 if err != nil {
1113 t.Fatal(err)
1114 }
1115 var f edgesFile
1116 if err := json.Unmarshal(body, &f); err != nil {
1117 t.Fatal(err)
1118 }
1119 return f.Hosts
1120}
1121
1122func ownedRoute(t *testing.T, reg *registry.Registry, host, account, app string) {
1123 t.Helper()
1124 if err := reg.Upsert(registry.Route{Host: host, Kind: "service", Addr: "10.0.0.1:3000", Status: "ready", Account: account, App: app}); err != nil {
1125 t.Fatal(err)
1126 }
1127}
1128
1129func wantOwner(t *testing.T, hosts map[string]edgeHost, host, account, app string) {
1130 t.Helper()
1131 h, ok := hosts[host]
1132 if !ok {
1133 t.Fatalf("%s not published: %+v", host, hosts)
1134 }
1135 if h.Account != account || h.App != app {
1136 t.Errorf("%s owner = %q/%q, want %q/%q", host, h.Account, h.App, account, app)
1137 }
1138}
1139
1140// A home row publishes its own account and app, which the door checks
1141// remotely. A row with no app has no owner a door key could name.
1142func TestEdgesFilePublishesTheOwnerOfAHomeRow(t *testing.T) {
1143 reg := edgesTestRegistry(t)
1144 ownedRoute(t, reg, "acme.go.tito.io", "bae", "titogo")
1145 ownedRoute(t, reg, "*.go.tito.io", "bae", "titogo")
1146 seedRoute(t, reg, "appless.example.com")
1147 hosts := publishHosts(t, reg)
1148 wantOwner(t, hosts, "acme.go.tito.io", "bae", "titogo")
1149 wantOwner(t, hosts, "*.go.tito.io", "bae", "titogo")
1150 wantOwner(t, hosts, "appless.example.com", "", "")
1151}
1152
1153// An alias is served by its primary's route, so it carries the primary's
1154// owner, not the alias row's own columns.
1155func TestEdgesFilePublishesAnAliasWithItsPrimarysOwner(t *testing.T) {
1156 withheldAliasSaid = sync.Map{}
1157 reg := edgesTestRegistry(t)
1158 ownedRoute(t, reg, "acme.go.tito.io", "bae", "titogo")
1159 if err := reg.UpsertAlias(registry.RouteAlias{Alias: "tickets.acme.com", Host: "acme.go.tito.io",
1160 Account: "bae", App: "somethingelse", DNSStatus: "pointing"}); err != nil {
1161 t.Fatal(err)
1162 }
1163 wantOwner(t, publishHosts(t, reg), "tickets.acme.com", "bae", "titogo")
1164}
1165
1166// The served catch-all carries the wildcard claim's owner, when the primary
1167// it serves agrees.
1168func TestEdgesFilePublishesTheServedCatchAllsOwner(t *testing.T) {
1169 reg := edgesTestRegistry(t)
1170 ownedRoute(t, reg, "acme.go.tito.io", "bae", "titogo")
1171 if err := reg.UpsertAlias(registry.RouteAlias{Alias: "*.acme.com", Host: "acme.go.tito.io", Account: "bae", App: "titogo"}); err != nil {
1172 t.Fatal(err)
1173 }
1174 for _, w := range []registry.WildcardDomain{
1175 {Host: "acme.com", Account: "bae", App: "titogo"},
1176 {Host: "certonly.com", Account: "bae", App: "titogo"},
1177 } {
1178 if err := reg.UpsertWildcardDomain(w); err != nil {
1179 t.Fatal(err)
1180 }
1181 }
1182 hosts := publishHosts(t, reg)
1183 wantOwner(t, hosts, "*.acme.com", "bae", "titogo")
1184 // The cert half dials nothing, so it names no owner.
1185 wantOwner(t, hosts, "*.certonly.com", "", "")
1186}
1187
1188// Review focus 2: a hostname whose path prefixes reach two apps publishes no
1189// owner, nor does its alias, so the door refuses it remotely. A hostname whose
1190// prefixes all reach one app still publishes that owner.
1191func TestEdgesFilePublishesNoOwnerForAMixedPathPrefixHostname(t *testing.T) {
1192 withheldAliasSaid = sync.Map{}
1193 reg := edgesTestRegistry(t)
1194 ownedRoute(t, reg, "ti.to", "bae", "titogo")
1195 ownedRoute(t, reg, "a.go.tito.io", "bae", "titogo")
1196 ownedRoute(t, reg, "b.classic.tito.io", "bae", "classic")
1197 ownedRoute(t, reg, "one.example", "bae", "titogo")
1198 for _, p := range []registry.RoutePath{
1199 {Host: "ti.to", Prefix: "a", Target: "a.go.tito.io", Account: "bae", App: "titogo"},
1200 {Host: "ti.to", Prefix: "b", Target: "b.classic.tito.io", Account: "bae", App: "titogo"},
1201 {Host: "one.example", Prefix: "a", Target: "a.go.tito.io", Account: "bae", App: "titogo"},
1202 } {
1203 if err := reg.UpsertPath(p); err != nil {
1204 t.Fatal(err)
1205 }
1206 }
1207 if err := reg.UpsertAlias(registry.RouteAlias{Alias: "tickets.example", Host: "ti.to", Account: "bae", App: "titogo", DNSStatus: "pointing"}); err != nil {
1208 t.Fatal(err)
1209 }
1210 hosts := publishHosts(t, reg)
1211 wantOwner(t, hosts, "ti.to", "", "")
1212 wantOwner(t, hosts, "tickets.example", "", "")
1213 wantOwner(t, hosts, "one.example", "bae", "titogo")
1214 wantOwner(t, hosts, "b.classic.tito.io", "bae", "classic")
1215}
1216
1217// The two eligibility fields are NOT omitempty: absent means "a writer
1218// that predates the field" (unknown), false means "no" (spec §6.1).
1219func TestEdgesFileRawWireBodyEligibilityFields(t *testing.T) {
1220 t.Parallel()
1221 for _, want := range []bool{true, false} {
1222 s, reg := release.NewDirStore(t.TempDir()), edgesTestRegistry(t)
1223 seedRoute(t, reg, "app.example.com")
1224 var last []byte
1225 ident := edgesFile{Box: "quiet-harbour", Region: "eu-west-2", Placement: "uk",
1226 ServesSharedPool: want, AcceptsInbound: want}
1227 if err := edgesFilePass(context.Background(), s, reg, ident, "203.0.113.10", &last); err != nil {
1228 t.Fatal(err)
1229 }
1230 got, _, err := s.Get(context.Background(), "control/edges/quiet-harbour.json")
1231 if err != nil {
1232 t.Fatal(err)
1233 }
1234 var raw map[string]any
1235 if err := json.Unmarshal(got, &raw); err != nil {
1236 t.Fatal(err)
1237 }
1238 for _, k := range []string{"serves_shared_pool", "accepts_inbound"} {
1239 v, present := raw[k]
1240 if !present || v != want {
1241 t.Fatalf("%s = %v (present %v), want %v always published", k, v, present, want)
1242 }
1243 }
1244 }
1245}
1246
1247// What the loop publishes is the reconcile config's own answer, for every
1248// spelling an operator might write. CARLOS_POOLS=",tito" parses to
1249// ["tito"] (agent.go's parser drops empty elements), so it does NOT serve
1250// the shared pool and must publish false (codex R3-3).
1251func TestEdgesFileEligibilityFollowsTheReconcileConfig(t *testing.T) {
1252 cases := []struct {
1253 pools, inbound string
1254 shared, accept bool
1255 }{
1256 {"", "", true, true},
1257 {"tito", "", false, true},
1258 {",tito", "", false, true},
1259 {"", "none", true, false},
1260 {"tito", "NONE", false, false},
1261 }
1262 for _, c := range cases {
1263 getenv := func(k string) string {
1264 switch k {
1265 case "CARLOS_POOLS":
1266 return c.pools
1267 case "CARLOS_INBOUND":
1268 return c.inbound
1269 }
1270 return ""
1271 }
1272 shared, inbound := edgesFileEligibility(getenv)
1273 cfg := provisionTickConfig(getenv)
1274 wantShared, wantInbound := cfg.SharedInstanceEligibility()
1275 if shared != wantShared || inbound != wantInbound {
1276 t.Fatalf("%+v: published (%v, %v) but the reconciler says (%v, %v)", c, shared, inbound, wantShared, wantInbound)
1277 }
1278 if shared != c.shared || inbound != c.accept {
1279 t.Fatalf("%+v: got (%v, %v), want (%v, %v)", c, shared, inbound, c.shared, c.accept)
1280 }
1281 }
1282}
1283
1284// R9: the box publishes the fingerprint of its secrets key's public half,
1285// never the key; no key, or a key that does not parse, publishes nothing.
1286func TestEdgesFileSecretsKeyFingerprint(t *testing.T) {
1287 t.Parallel()
1288 priv, err := ecdh.P256().NewPrivateKey(bytes.Repeat([]byte{1}, 32))
1289 if err != nil {
1290 t.Fatal(err)
1291 }
1292 privB64 := base64.StdEncoding.EncodeToString(priv.Bytes())
1293 pubB64 := base64.StdEncoding.EncodeToString(priv.PublicKey().Bytes())
1294 env := func(v string) func(string) string {
1295 return func(k string) string {
1296 if k == "CARLOS_SECRETS_KEY" {
1297 return v
1298 }
1299 return ""
1300 }
1301 }
1302 want := appconfig.KeyFingerprint(priv.PublicKey())
1303 if got := edgesFileSecretsFP(env(privB64)); got != want {
1304 t.Fatalf("with a key = %q, want %q", got, want)
1305 }
1306 for _, v := range []string{"", "not base64!", base64.StdEncoding.EncodeToString([]byte("short"))} {
1307 if got := edgesFileSecretsFP(env(v)); got != "" {
1308 t.Errorf("CARLOS_SECRETS_KEY=%q published %q, want nothing", v, got)
1309 }
1310 }
1311
1312 for _, fp := range []string{want, ""} {
1313 s, reg := release.NewDirStore(t.TempDir()), edgesTestRegistry(t)
1314 seedRoute(t, reg, "app.example.com")
1315 var last []byte
1316 ident := edgesFile{Box: "quiet-harbour", Region: "eu-west-2", Placement: "uk", SecretsKeyFP: fp}
1317 if err := edgesFilePass(context.Background(), s, reg, ident, "203.0.113.10", &last); err != nil {
1318 t.Fatal(err)
1319 }
1320 body, _, err := s.Get(context.Background(), "control/edges/quiet-harbour.json")
1321 if err != nil {
1322 t.Fatal(err)
1323 }
1324 has := strings.Contains(string(body), `"secrets_key_fp"`)
1325 if has != (fp != "") {
1326 t.Fatalf("fp %q: key present = %v in %s", fp, has, body)
1327 }
1328 for _, secret := range []string{privB64, pubB64} {
1329 if strings.Contains(string(body), secret) {
1330 t.Fatal("the edges file carries key material")
1331 }
1332 }
1333 }
1334}
1335
1336func TestEdgesFileCapabilityFollowsTheFlag(t *testing.T) {
1337 env := func(v string) func(string) string {
1338 return func(k string) string {
1339 if k == "CARLOS_REGION_HA" {
1340 return v
1341 }
1342 return ""
1343 }
1344 }
1345 if got := edgesFileCapabilities(env("")); len(got) != 0 {
1346 t.Fatalf("unset: %v", got)
1347 }
1348 if got := edgesFileCapabilities(env("1")); len(got) != 1 || got[0] != "region-ha-1" {
1349 t.Fatalf("set: %v", got)
1350 }
1351 if got := edgesFileCapabilities(env("0")); len(got) != 0 {
1352 t.Fatalf("\"0\" is off: %v", got)
1353 }
1354}
1355
1356func TestEdgesFileRawWireBodyCarriesBoxEvidence(t *testing.T) {
1357 s, reg := release.NewDirStore(t.TempDir()), edgesTestRegistry(t)
1358 seedRoute(t, reg, "app.example.com")
1359 var last []byte
1360 ident := edgesFile{Box: "annie", BoxID: "i-0abc", Capabilities: []string{"region-ha-1"},
1361 Headroom: &edgeHeadroom{MemFreeMiB: 2210, DiskFreeMiB: 31200, Awake: 14}}
1362 if err := edgesFilePass(context.Background(), s, reg, ident, "1.2.3.4", &last); err != nil {
1363 t.Fatal(err)
1364 }
1365 body, _, err := s.Get(context.Background(), "control/edges/annie.json")
1366 if err != nil {
1367 t.Fatal(err)
1368 }
1369 var raw map[string]json.RawMessage
1370 if err := json.Unmarshal(body, &raw); err != nil {
1371 t.Fatal(err)
1372 }
1373 for key, want := range map[string]string{
1374 "box_id": `"i-0abc"`,
1375 "capabilities": `["region-ha-1"]`,
1376 "headroom": `{"mem_free_mib":2210,"disk_free_mib":31200,"awake":14}`,
1377 } {
1378 if string(raw[key]) != want {
1379 t.Errorf("%s = %s, want %s", key, raw[key], want)
1380 }
1381 }
1382 if _, ok := raw["draining"]; ok {
1383 t.Error("draining:false must be omitted")
1384 }
1385}
1386
1387func TestEdgesFileOmitsBoxEvidenceWhenAbsent(t *testing.T) {
1388 s, reg := release.NewDirStore(t.TempDir()), edgesTestRegistry(t)
1389 var last []byte
1390 if err := edgesFilePass(context.Background(), s, reg, edgesFile{Box: "annie"}, "1.2.3.4", &last); err != nil {
1391 t.Fatal(err)
1392 }
1393 body, _, _ := s.Get(context.Background(), "control/edges/annie.json")
1394 for _, key := range []string{"box_id", "capabilities", "draining", "headroom"} {
1395 if bytes.Contains(body, []byte(`"`+key+`"`)) {
1396 t.Errorf("%s present in %s", key, body)
1397 }
1398 }
1399}
1400
1401func TestEdgesFileHeadroomAloneDoesNotRewriteTheFile(t *testing.T) {
1402 st := &putCountingStore{Store: release.NewDirStore(t.TempDir())}
1403 reg := edgesTestRegistry(t)
1404 seedRoute(t, reg, "app.example.com")
1405 var last []byte
1406 ident := edgesFile{Box: "annie", Headroom: &edgeHeadroom{MemFreeMiB: 100}}
1407 if err := edgesFilePass(context.Background(), st, reg, ident, "1.2.3.4", &last); err != nil {
1408 t.Fatal(err)
1409 }
1410 ident.Headroom = &edgeHeadroom{MemFreeMiB: 99}
1411 if err := edgesFilePass(context.Background(), st, reg, ident, "1.2.3.4", &last); err != nil {
1412 t.Fatal(err)
1413 }
1414 if st.puts != 1 {
1415 t.Fatalf("puts = %d, want 1: a headroom change alone is not a reason to write", st.puts)
1416 }
1417}
1418
1419// The loop's timed pass refreshes headroom even when nothing else moved.
1420func TestEdgesFileTimedPassRefreshesHeadroom(t *testing.T) {
1421 st := &putCountingStore{Store: release.NewDirStore(t.TempDir())}
1422 reg := edgesTestRegistry(t)
1423 seedRoute(t, reg, "app.example.com")
1424 var last []byte
1425 ident := edgesFile{Box: "annie", Headroom: &edgeHeadroom{MemFreeMiB: 100}}
1426 if err := edgesFileWrite(context.Background(), st, reg, ident, "1.2.3.4", &last, false); err != nil {
1427 t.Fatal(err)
1428 }
1429 ident.Headroom = &edgeHeadroom{MemFreeMiB: 42}
1430 if err := edgesFileWrite(context.Background(), st, reg, ident, "1.2.3.4", &last, true); err != nil {
1431 t.Fatal(err)
1432 }
1433 if st.puts != 2 {
1434 t.Fatalf("puts = %d, want 2: the timed pass must publish the new reading", st.puts)
1435 }
1436 body, _, err := st.Get(context.Background(), "control/edges/annie.json")
1437 if err != nil {
1438 t.Fatal(err)
1439 }
1440 var f edgesFile
1441 if err := json.Unmarshal(body, &f); err != nil || f.Headroom == nil || f.Headroom.MemFreeMiB != 42 {
1442 t.Fatalf("published headroom = %+v, err %v", f.Headroom, err)
1443 }
1444 // A box that publishes no headroom keeps today's behaviour exactly:
1445 // a timed pass with nothing changed writes nothing.
1446 plain := &putCountingStore{Store: release.NewDirStore(t.TempDir())}
1447 var plainLast []byte
1448 for i := 0; i < 2; i++ {
1449 if err := edgesFileWrite(context.Background(), plain, reg, edgesFile{Box: "betty"}, "1.2.3.4", &plainLast, true); err != nil {
1450 t.Fatal(err)
1451 }
1452 }
1453 if plain.puts != 1 {
1454 t.Fatalf("puts = %d, want 1 for a box with no headroom", plain.puts)
1455 }
1456}
1457
1458// A restart must not rewrite an unchanged file just because the stored
1459// copy carries a headroom reading.
1460func TestEdgesFileSeededGateIgnoresStoredHeadroom(t *testing.T) {
1461 st := &putCountingStore{Store: release.NewDirStore(t.TempDir())}
1462 reg := edgesTestRegistry(t)
1463 seedRoute(t, reg, "app.example.com")
1464 var last []byte
1465 ident := edgesFile{Box: "annie", Headroom: &edgeHeadroom{MemFreeMiB: 100}}
1466 if err := edgesFilePass(context.Background(), st, reg, ident, "1.2.3.4", &last); err != nil {
1467 t.Fatal(err)
1468 }
1469 seeded := seedEdgesFileGate(context.Background(), st, "annie")
1470 ident.Headroom = &edgeHeadroom{MemFreeMiB: 7}
1471 if err := edgesFilePass(context.Background(), st, reg, ident, "1.2.3.4", &seeded); err != nil {
1472 t.Fatal(err)
1473 }
1474 if st.puts != 1 {
1475 t.Fatalf("puts = %d, want 1", st.puts)
1476 }
1477}
1478
1479func TestEdgesFileStampIsOmittedWhenZero(t *testing.T) {
1480 b, _ := json.Marshal(edgeHost{Role: "home"})
1481 if bytes.Contains(b, []byte("stamp")) {
1482 t.Fatalf("zero stamp on the wire: %s", b)
1483 }
1484 b, _ = json.Marshal(edgeHost{Role: "home", Stamp: 41})
1485 if !bytes.Contains(b, []byte(`"stamp":41`)) {
1486 t.Fatalf("stamp missing: %s", b)
1487 }
1488}
1489
1490// The move door (Task 4) admits a move only when every edges file of both
1491// regions shows the box's armed floor and its verdict on carlos.prev, so
1492// the writer publishes them from the box's own files at every write.
1493func TestEdgesFilePublishesMovesProtocolFloorAndPrev(t *testing.T) {
1494 t.Parallel()
1495 type published struct {
1496 MovesProtocol *int `json:"moves_protocol"`
1497 Floor *string `json:"moves_floor_min_release"`
1498 Prev *string `json:"prev_release"`
1499 Allowed *string `json:"prev_allowed_for"`
1500 }
1501 publish := func(t *testing.T, dd, opt string) (published, string) {
1502 t.Helper()
1503 s, reg := release.NewDirStore(t.TempDir()), edgesTestRegistry(t)
1504 seedRoute(t, reg, "app.example.com")
1505 var last []byte
1506 ident := withMovesFields(edgesFile{Box: "quiet-harbour"}, dd, opt)
1507 if err := edgesFilePass(context.Background(), s, reg, ident, "203.0.113.10", &last); err != nil {
1508 t.Fatal(err)
1509 }
1510 body, _, err := s.Get(context.Background(), "control/edges/quiet-harbour.json")
1511 if err != nil {
1512 t.Fatal(err)
1513 }
1514 var p published
1515 if err := json.Unmarshal(body, &p); err != nil {
1516 t.Fatal(err)
1517 }
1518 return p, string(body)
1519 }
1520
1521 // Nothing armed, no carlos.prev.
1522 dd, opt := t.TempDir(), t.TempDir()
1523 p, body := publish(t, dd, opt)
1524 if p.MovesProtocol == nil || *p.MovesProtocol != moves.ProtocolVersion || !strings.Contains(body, `"moves_protocol":1`) {
1525 t.Fatalf("moves_protocol missing: %s", body)
1526 }
1527 if p.Floor != nil || p.Allowed != nil || p.Prev == nil || *p.Prev != "none" {
1528 t.Fatalf("unarmed box with no carlos.prev published %s", body)
1529 }
1530
1531 // A carlos.prev with no sidecar: its release is unknown, so absent.
1532 os.WriteFile(systemPrevPath(opt), []byte("#!/bin/sh\n"), 0o755)
1533 if p, body = publish(t, dd, opt); p.Prev != nil {
1534 t.Fatalf("carlos.prev without a sidecar published %s", body)
1535 }
1536
1537 // Armed, judged: every value as the box's files hold it, one line each.
1538 if err := writeLocalFloor(dd, moves.Floor{Min: 1, MinRelease: "v0.30.0"}); err != nil {
1539 t.Fatal(err)
1540 }
1541 if err := writePrevVersion(opt, "v0.31.0"); err != nil {
1542 t.Fatal(err)
1543 }
1544 writeFileDurable(filepath.Join(opt, moves.PrevAllowedFile), []byte("v0.30.0\n"), 0o644)
1545 p, body = publish(t, dd, opt)
1546 if p.Floor == nil || *p.Floor != "v0.30.0" || p.Prev == nil || *p.Prev != "v0.31.0" || p.Allowed == nil || *p.Allowed != "v0.30.0" {
1547 t.Fatalf("armed box published %s", body)
1548 }
1549
1550 // No verdict: absent, which the door reads as below the floor.
1551 os.Remove(filepath.Join(opt, moves.PrevAllowedFile))
1552 if p, body = publish(t, dd, opt); p.Allowed != nil {
1553 t.Fatalf("no verdict, yet published %s", body)
1554 }
1555}
1556
1557// Region HA spec 2 §7 reads a box's edges file as stale after 15 minutes
1558// (boxchoice.MaxEdgesAge), and the gate clears Generation, so a quiet box
1559// stays placeable only if its timed pass rewrites an UNCHANGED body. With
1560// headroom published (region HA on) it does, every timed pass (at most six
1561// minutes apart); an untimed pass with nothing changed does not; and with
1562// region HA off (no headroom) nothing is ever rewritten.
1563func TestEdgesFileQuietBoxStaysFreshOnTimedPassesOnlyWithRegionHA(t *testing.T) {
1564 ctx, reg := context.Background(), edgesTestRegistry(t)
1565 seedRoute(t, reg, "app.example.com")
1566 run := func(ident edgesFile) (untimedPuts, timedPuts int) {
1567 st := &putCountingStore{Store: release.NewDirStore(t.TempDir())}
1568 var last []byte
1569 if err := edgesFileWrite(ctx, st, reg, ident, "1.2.3.4", &last, false); err != nil {
1570 t.Fatal(err)
1571 }
1572 if err := edgesFileWrite(ctx, st, reg, ident, "1.2.3.4", &last, false); err != nil {
1573 t.Fatal(err)
1574 }
1575 untimedPuts = st.puts
1576 if err := edgesFileWrite(ctx, st, reg, ident, "1.2.3.4", &last, true); err != nil {
1577 t.Fatal(err)
1578 }
1579 return untimedPuts, st.puts
1580 }
1581 on := edgesFile{Box: "annie", Capabilities: []string{capRegionHA1},
1582 Headroom: &edgeHeadroom{MemFreeMiB: 100, DiskFreeMiB: 100, Awake: 1, MemTotalMiB: 1000, DiskTotalMiB: 1000}}
1583 if untimed, timed := run(on); untimed != 1 || timed != 2 {
1584 t.Fatalf("region HA on: puts = %d untimed, %d after a timed pass; want 1, 2", untimed, timed)
1585 }
1586 if untimed, timed := run(edgesFile{Box: "annie"}); untimed != 1 || timed != 1 {
1587 t.Fatalf("region HA off: puts = %d untimed, %d after a timed pass; want 1, 1", untimed, timed)
1588 }
1589}
1590