| index 9048d09d4..039ba4b19 100644 |
| --- a/internal/provision/reconcile_test.go |
| +++ b/internal/provision/reconcile_test.go |
| @@ -2039,6 +2039,79 @@ func TestReconcileRemovesEverywhereRouteWhenItsRecordGoes(t *testing.T) { |
| } |
| } |
| |
| +// TestEverywhereClassAgreesAcrossRecordAndRow is the drift pin between the two |
| +// halves of the class rule: everywhereClass reads a RECORD (the mint gate) and |
| +// everywhereRoute reads a ROW (the removal gate, asked when the record is gone |
| +// and the row is all that is left). routeFor is the one renderer between them, |
| +// so the property holds by construction today — and it is exactly what a class |
| +// rule added to one function alone would break, silently, with every other test |
| +// in this file still green. That divergence is the immortality bug the pair was |
| +// written to close: a far box that mints an everywhere-class route and then |
| +// cannot see it as one serves a retired site forever. |
| +// |
| +// The fixture set is every shape the file already exercises, plus the two |
| +// directions a new rule would most plausibly be written into one side only (a |
| +// probe config, an explicitly non-hibernating addr record). |
| +func TestEverywhereClassAgreesAcrossRecordAndRow(t *testing.T) { |
| + off := false |
| + on := true |
| + base := func(host string) Record { |
| + return Record{Schema: 1, Host: host, Slug: "s", App: "tito", Account: "bac", |
| + Pool: "tito", Region: "ie", Channel: "stable"} |
| + } |
| + with := func(host string, mut func(*Record)) Record { |
| + rec := base(host) |
| + mut(&rec) |
| + return rec |
| + } |
| + fixtures := []struct { |
| + name string |
| + rec Record |
| + }{ |
| + {"socket instance (no kind, no addr)", base("exec.go.tito.io")}, |
| + {"static", with("site.go.tito.io", func(r *Record) { r.Kind = "static" })}, |
| + {"static, status-named", with("named-site.go.tito.io", func(r *Record) { r.Kind, r.StatusNamed = "static", &on })}, |
| + {"public single addr", with("pub.go.tito.io", func(r *Record) { r.Kind, r.Addr = "service", "203.0.113.10:443" })}, |
| + {"public addrs set", with("pubs.go.tito.io", func(r *Record) { |
| + r.Kind, r.Addrs = "service", []string{"203.0.113.11:443", "198.51.100.7:443"} |
| + })}, |
| + {"public addrs with an explicit probe", with("probed.go.tito.io", func(r *Record) { |
| + r.Kind, r.Addrs, r.Health = "service", []string{"203.0.113.12:443", "198.51.100.8:443"}, &Health{Path: "/healthz"} |
| + })}, |
| + {"public addr, explicitly never hibernating", with("awake.go.tito.io", func(r *Record) { |
| + r.Kind, r.Addr, r.Hibernate = "service", "203.0.113.13:443", &off |
| + })}, |
| + {"loopback addr", with("loop.go.tito.io", func(r *Record) { r.Kind, r.Addr = "service", "127.0.0.1:8090" })}, |
| + {"private addrs", with("priv.go.tito.io", func(r *Record) { r.Kind, r.Addrs = "service", []string{"10.0.0.5:3000"} })}, |
| + {"mixed public and private addrs", with("mixed.go.tito.io", func(r *Record) { |
| + r.Kind, r.Addrs = "service", []string{"203.0.113.14:443", "192.168.1.9:3000"} |
| + })}, |
| + {"name-based addr", with("named.go.tito.io", func(r *Record) { r.Kind, r.Addr = "service", "backend.example.com:443" })}, |
| + {"unit-owned public addr", with("unit.go.tito.io", func(r *Record) { |
| + r.Addr, r.Unit = "203.0.113.20:8090", "console.service" |
| + })}, |
| + {"environment-bound static", with("env-site.go.tito.io", func(r *Record) { r.Kind, r.Environment = "static", "staging" })}, |
| + {"fleet-placed static", with("pisite.go.tito.io", func(r *Record) { r.Kind, r.Region = "static", "fleet/pi" })}, |
| + } |
| + for _, f := range fixtures { |
| + t.Run(f.name, func(t *testing.T) { |
| + // Every fixture must be one the pass would actually materialize — |
| + // a shape ValidateShape refuses never reaches either gate, so a |
| + // property proved on one would prove nothing. |
| + if err := ValidateShape(f.rec); err != nil { |
| + t.Fatalf("fixture is not a shape this pass would ever see: %v", err) |
| + } |
| + fromRecord := everywhereClass(f.rec) |
| + fromRow := everywhereRoute(routeFor(f.rec, "/data", `["seed"]`)) |
| + if fromRecord != fromRow { |
| + t.Fatalf("class disagrees across the mint and removal gates: everywhereClass = %v, "+ |
| + "everywhereRoute(routeFor(...)) = %v — a record this box mints must be a row it can retire", |
| + fromRecord, fromRow) |
| + } |
| + }) |
| + } |
| +} |
| + |
| // TestRouteForCarriesAddrsAndHealth is Task 3's mint-side proof: a record |
| // with an explicit multi-upstream Addrs set mints a route whose Addrs is the |
| // comma-joined string and whose Health carries a fully-materialized default |