package host import ( "context" "encoding/json" "fmt" "path/filepath" "testing" "time" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" ) func snapTestRegistry(t *testing.T) *registry.Registry { t.Helper() reg, err := registry.Open(filepath.Join(t.TempDir(), "registry.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { reg.Close() }) if err := reg.Upsert(registry.Route{Host: "a.example.com", Kind: "service", Addr: "10.0.0.1:3000", Status: "ready"}); err != nil { t.Fatal(err) } return reg } func TestSnapshotPassWritesCurrentSidecarAndGeneration(t *testing.T) { s := release.NewDirStore(t.TempDir()) ctx := context.Background() reg := snapTestRegistry(t) at := time.Date(2026, 8, 12, 3, 0, 0, 0, time.UTC) var lastFP string if err := registrySnapshotPass(ctx, s, reg, "box1", at, &lastFP); err != nil { t.Fatal(err) } raw, _, err := s.Get(ctx, snapshotDBKey("box1")) if err != nil { t.Fatalf("current snapshot missing: %v", err) } if len(raw) == 0 { t.Fatal("current snapshot is empty") } metaRaw, _, err := s.Get(ctx, snapshotMetaKey("box1")) if err != nil { t.Fatalf("sidecar missing: %v", err) } var meta snapshotMeta if err := json.Unmarshal(metaRaw, &meta); err != nil { t.Fatal(err) } if meta.V != 1 || meta.Box != "box1" || meta.Fingerprint == "" || meta.Bytes != len(raw) { t.Fatalf("sidecar = %+v (snapshot %d bytes)", meta, len(raw)) } if lastFP != meta.Fingerprint { t.Fatalf("lastFP %q not updated to %q", lastFP, meta.Fingerprint) } gens, err := s.List(ctx, snapshotGenPrefix("box1")) if err != nil { t.Fatal(err) } if len(gens) != 1 || gens[0] != snapshotGenPrefix("box1")+"20260812T030000Z.db" { t.Fatalf("generations = %v", gens) } } func TestSnapshotPassSkipsWhenFingerprintUnchanged(t *testing.T) { t.Parallel() s := release.NewDirStore(t.TempDir()) ctx := context.Background() reg := snapTestRegistry(t) at := time.Date(2026, 8, 12, 3, 0, 0, 0, time.UTC) var lastFP string if err := registrySnapshotPass(ctx, s, reg, "box1", at, &lastFP); err != nil { t.Fatal(err) } if err := registrySnapshotPass(ctx, s, reg, "box1", at.Add(5*time.Minute), &lastFP); err != nil { t.Fatal(err) } gens, err := s.List(ctx, snapshotGenPrefix("box1")) if err != nil { t.Fatal(err) } if len(gens) != 1 { t.Fatalf("unchanged registry grew generations: %v", gens) } // A registry change makes the next pass write again. if err := reg.Upsert(registry.Route{Host: "b.example.com", Kind: "service", Addr: "10.0.0.2:3000", Status: "ready"}); err != nil { t.Fatal(err) } if err := registrySnapshotPass(ctx, s, reg, "box1", at.Add(10*time.Minute), &lastFP); err != nil { t.Fatal(err) } gens, err = s.List(ctx, snapshotGenPrefix("box1")) if err != nil { t.Fatal(err) } if len(gens) != 2 { t.Fatalf("changed registry did not write a new generation: %v", gens) } // A caps grant touches no edge-reload column, so registry.Fingerprint // sits still for it — which is why this pass gates on // SnapshotFingerprint instead. Caps and unit stamps are exactly the // box-scoped rows only the snapshot can rebuild; if they did not trigger // a pass, the standby would restore a registry missing them. if err := reg.SetCaps("a.example.com", "", "net_bind_service"); err != nil { t.Fatal(err) } if err := registrySnapshotPass(ctx, s, reg, "box1", at.Add(15*time.Minute), &lastFP); err != nil { t.Fatal(err) } gens, err = s.List(ctx, snapshotGenPrefix("box1")) if err != nil { t.Fatal(err) } if len(gens) != 3 { t.Fatalf("caps-only change did not snapshot: generations = %v (want 3) — a caps grant would never reach the bucket", gens) } // Same for a console unit stamp. if err := reg.SetUnit("a.example.com", "console.service"); err != nil { t.Fatal(err) } if err := registrySnapshotPass(ctx, s, reg, "box1", at.Add(20*time.Minute), &lastFP); err != nil { t.Fatal(err) } gens, err = s.List(ctx, snapshotGenPrefix("box1")) if err != nil { t.Fatal(err) } if len(gens) != 4 { t.Fatalf("unit-only change did not snapshot: generations = %v (want 4)", gens) } } // The sidecar's fingerprint field must be the digest the pass actually gates // on, or seedSnapshotFingerprint would seed lastFP with a value the next // pass can never match and every restart would re-snapshot. func TestSnapshotSidecarCarriesSnapshotFingerprint(t *testing.T) { s := release.NewDirStore(t.TempDir()) ctx := context.Background() reg := snapTestRegistry(t) var lastFP string if err := registrySnapshotPass(ctx, s, reg, "box1", time.Date(2026, 8, 12, 3, 0, 0, 0, time.UTC), &lastFP); err != nil { t.Fatal(err) } want, err := reg.SnapshotFingerprint() if err != nil { t.Fatal(err) } metaRaw, _, err := s.Get(ctx, snapshotMetaKey("box1")) if err != nil { t.Fatal(err) } var meta snapshotMeta if err := json.Unmarshal(metaRaw, &meta); err != nil { t.Fatal(err) } if meta.Fingerprint != want { t.Fatalf("sidecar fingerprint = %q, want SnapshotFingerprint %q", meta.Fingerprint, want) } } // readSnapshotMeta is box-scoped: it must report the prior writer at THIS // box's own key, degrade to nil when there is no sidecar there yet, and // never see another box's sidecar at all — per-box keys is exactly what // makes a foreign-differently-named box's write harmless (it lands under a // different key entirely, unlike the pre-multi-region shared key this // guard was written against). func TestReadSnapshotMetaReportsPriorWriter(t *testing.T) { t.Parallel() s := release.NewDirStore(t.TempDir()) ctx := context.Background() reg := snapTestRegistry(t) at := time.Date(2026, 8, 12, 3, 0, 0, 0, time.UTC) if got := readSnapshotMeta(ctx, s, "dead-box"); got != nil { t.Fatalf("readSnapshotMeta with no sidecar = %+v, want nil", got) } var lastFP string if err := registrySnapshotPass(ctx, s, reg, "dead-box", at, &lastFP); err != nil { t.Fatal(err) } got := readSnapshotMeta(ctx, s, "dead-box") if got == nil || got.Box != "dead-box" { t.Fatalf("readSnapshotMeta = %+v, want Box \"dead-box\"", got) } // A different box name never sees dead-box's sidecar — per-box keys // isolate them, unlike the pre-multi-region shared key. if got := readSnapshotMeta(ctx, s, "standby-box"); got != nil { t.Fatalf("readSnapshotMeta(\"standby-box\") = %+v, want nil (no cross-box read)", got) } // The runbook's restore path: a rebuilt standby deliberately reuses the // dead box's name, so its pass writes to the SAME key. The write must // still land (the pass never refuses it — a legitimate promotion looks // identical from here), and readSnapshotMeta for that name reports the // newest sidecar. if err := reg.Upsert(registry.Route{Host: "c.example.com", Kind: "service", Addr: "10.0.0.3:3000", Status: "ready"}); err != nil { t.Fatal(err) } if err := registrySnapshotPass(ctx, s, reg, "dead-box", at.Add(5*time.Minute), &lastFP); err != nil { t.Fatal(err) } got = readSnapshotMeta(ctx, s, "dead-box") if got == nil || !got.CreatedAt.Equal(at.Add(5*time.Minute)) { t.Fatalf("after the reused-name write readSnapshotMeta = %+v, want CreatedAt %s", got, at.Add(5*time.Minute)) } } func TestSnapshotPassPrunesGenerationsBeyondKeep(t *testing.T) { t.Parallel() s := release.NewDirStore(t.TempDir()) ctx := context.Background() reg := snapTestRegistry(t) at := time.Date(2026, 8, 12, 0, 0, 0, 0, time.UTC) var lastFP string for i := 0; i < snapshotKeep+3; i++ { // Change the registry each round so no pass skips. if err := reg.Upsert(registry.Route{Host: "a.example.com", Kind: "service", Addr: "10.0.0.1:3000", Status: "ready", Version: string(rune('a' + i))}); err != nil { t.Fatal(err) } if err := registrySnapshotPass(ctx, s, reg, "box1", at.Add(time.Duration(i)*5*time.Minute), &lastFP); err != nil { t.Fatal(err) } } gens, err := s.List(ctx, snapshotGenPrefix("box1")) if err != nil { t.Fatal(err) } if len(gens) != snapshotKeep { t.Fatalf("kept %d generations, want %d: %v", len(gens), snapshotKeep, gens) } // The survivors must be the newest ones: the earliest kept stamp is round 3's. if gens[0] != snapshotGenPrefix("box1")+"20260812T001500Z.db" { t.Fatalf("oldest kept generation = %q, want the round-3 stamp", gens[0]) } } func TestSnapshotKeysArePerBox(t *testing.T) { if got, want := snapshotDBKey("annie"), "control/registry-snapshot/annie.db"; got != want { t.Fatalf("db key = %q, want %q", got, want) } if got, want := snapshotMetaKey("annie"), "control/registry-snapshot/annie.json"; got != want { t.Fatalf("meta key = %q, want %q", got, want) } if got, want := snapshotGenPrefix("annie"), "control/registry-snapshots/annie/"; got != want { t.Fatalf("gen prefix = %q, want %q", got, want) } } func TestTwoBoxesSnapshotWithoutColliding(t *testing.T) { t.Parallel() s := release.NewDirStore(t.TempDir()) regA, regB := snapTestRegistry(t), snapTestRegistry(t) var fpA, fpB string now := time.Date(2026, 8, 20, 12, 0, 0, 0, time.UTC) if err := registrySnapshotPass(context.Background(), s, regA, "annie", now, &fpA); err != nil { t.Fatal(err) } if err := registrySnapshotPass(context.Background(), s, regB, "betty", now, &fpB); err != nil { t.Fatal(err) } for _, key := range []string{ "control/registry-snapshot/annie.db", "control/registry-snapshot/annie.json", "control/registry-snapshot/betty.db", "control/registry-snapshot/betty.json", } { if _, _, err := s.Get(context.Background(), key); err != nil { t.Fatalf("missing %s: %v", key, err) } } // betty's pass must not have disturbed annie's generations. gens, err := s.List(context.Background(), "control/registry-snapshots/annie/") if err != nil || len(gens) != 1 { t.Fatalf("annie generations = %v, %v; want exactly 1", gens, err) } // Prove the prune is scoped per box, not global. Interleave four more // fingerprint-changing passes for each box (registry content varies // each round so the fingerprint gate always writes). After this, each // box has 5 generations of its own (1 above + 4 here) — under // snapshotKeep (8), so a correctly per-box-scoped prune leaves both at // exactly 5. But the AGGREGATE across both boxes is 10, which crosses // snapshotKeep — so a prune that (wrongly) lists the GLOBAL // "control/registry-snapshots/" prefix would prune down to 8 total, // eating into one or both boxes' tails. This is exactly the mutation // the reviewer flagged: snapshotGenPrefix(box) collapsed back to a // shared "control/registry-snapshots/" would pass every other test in // this file (each of which uses only one box) but must fail here. for i := 0; i < 4; i++ { at := now.Add(time.Duration(i+1) * time.Minute) if err := regA.Upsert(registry.Route{Host: fmt.Sprintf("annie-%d.example.com", i), Kind: "service", Addr: "10.0.1.1:3000", Status: "ready"}); err != nil { t.Fatal(err) } if err := registrySnapshotPass(context.Background(), s, regA, "annie", at, &fpA); err != nil { t.Fatal(err) } if err := regB.Upsert(registry.Route{Host: fmt.Sprintf("betty-%d.example.com", i), Kind: "service", Addr: "10.0.2.1:3000", Status: "ready"}); err != nil { t.Fatal(err) } if err := registrySnapshotPass(context.Background(), s, regB, "betty", at, &fpB); err != nil { t.Fatal(err) } } gensA, err := s.List(context.Background(), "control/registry-snapshots/annie/") if err != nil || len(gensA) != 5 { t.Fatalf("annie generations after interleaving = %v, %v; want exactly 5 (per-box prune must not see betty's writes)", gensA, err) } gensB, err := s.List(context.Background(), "control/registry-snapshots/betty/") if err != nil || len(gensB) != 5 { t.Fatalf("betty generations after interleaving = %v, %v; want exactly 5 (per-box prune must not see annie's writes)", gensB, err) } } func TestSeedSnapshotFingerprintEmptyWhenNoSidecar(t *testing.T) { s := release.NewDirStore(t.TempDir()) if got := seedSnapshotFingerprint(context.Background(), s, "box1"); got != "" { t.Fatalf("seedSnapshotFingerprint = %q, want empty with no sidecar written yet", got) } } // TestSeedSnapshotFingerprintMatchingSidecarSkipsFirstPass proves the // registrySnapshotLoop wiring this backs: reading lastFP from the bucket // sidecar at loop start (rather than starting it empty in memory) makes the // very first pass after a restart a no-op when the registry has not changed // since the last snapshot — the crash-loop case addition 1 of the Task 2 // review exists to fix. func TestSeedSnapshotFingerprintMatchingSidecarSkipsFirstPass(t *testing.T) { s := release.NewDirStore(t.TempDir()) ctx := context.Background() reg := snapTestRegistry(t) at := time.Date(2026, 8, 12, 3, 0, 0, 0, time.UTC) // A prior process ran a pass and wrote the sidecar, then exited (or // crashed) — its in-memory lastFP is gone. var priorFP string if err := registrySnapshotPass(ctx, s, reg, "box1", at, &priorFP); err != nil { t.Fatal(err) } // A fresh process seeds lastFP from the bucket instead of starting empty. seeded := seedSnapshotFingerprint(ctx, s, "box1") if seeded == "" || seeded != priorFP { t.Fatalf("seeded fingerprint = %q, want %q", seeded, priorFP) } lastFP := seeded if err := registrySnapshotPass(ctx, s, reg, "box1", at.Add(5*time.Minute), &lastFP); err != nil { t.Fatal(err) } gens, err := s.List(ctx, snapshotGenPrefix("box1")) if err != nil { t.Fatal(err) } if len(gens) != 1 { t.Fatalf("seeded lastFP did not skip the unchanged registry: generations = %v (want 1, no new write)", gens) } }