edge, applog, console: pin the causes the front-box rule depends on
The third Fable review found no code defect but four behaviours no test pinned; two were silent-loss paths. Because a box proxying over a reverse channel files only failures a proxy noted a cause for, removing tunnelProxy's ErrorHandler or fleetProxy's fleet_dark note made those 502/503s vanish with every test green. - TestEdgeFilesHopFailuresThroughServeHTTP: a hub with no connected channel, through ServeHTTP — the -tunnel route files one 502 `tunnel`, the fleet route one 503 `fleet_dark`. - The static 502 notes `static` (TestStaticNeverResolvedHasNoVersionHeader). - TestIsDropNotice, and TestAvailability now seeds the rate-cap line and asserts Availability leaves it out. Each new assertion was mutation-checked: removing the behaviour it pins fails it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4 files changed,
+94
−1
internal/applog/sink_test.go+28 −0internal/console/apierrors_test.go+8 −1internal/edge/edgeerrors_test.go+50 −0internal/edge/static_test.go+8 −0
CI passed — run details
diff --git a/internal/applog/sink_test.go b/internal/applog/sink_test.go| index efc060a73..fcdaa2ac0 100644 |
| --- a/internal/applog/sink_test.go |
| +++ b/internal/applog/sink_test.go |
| @@ -134,3 +134,31 @@ func TestSinkDrainEmptyIsEmpty(t *testing.T) { |
| t.Fatalf("empty sink drained %v", got) |
| } |
| } |
| + |
| +// TestIsDropNotice: the rate-cap line Drain appends is recognised, and only |
| +// it — Availability leaves it out by this test, and a platform event that |
| +// merely starts with "dropped" must not vanish with it. |
| +func TestIsDropNotice(t *testing.T) { |
| + now, _ := testClock(time.Date(2026, 9, 24, 12, 0, 0, 0, time.UTC)) |
| + sk := NewSink("box1", now) |
| + sk.RatePerMin, sk.Burst = 300, 300 |
| + sk.App("bab", "web", "h", strings.Repeat("a", 200)) // fits burst |
| + sk.App("bab", "web", "h", strings.Repeat("b", 200)) // bucket empty → dropped |
| + var notice *Record |
| + for _, r := range sk.Drain()[AppKey{"bab", "web"}] { |
| + if r.Stream == StreamPlatform { |
| + notice = &r |
| + } |
| + } |
| + if notice == nil || !IsDropNotice(*notice) { |
| + t.Fatalf("drain's rate-cap line not recognised: %+v", notice) |
| + } |
| + for _, r := range []Record{ |
| + {Stream: StreamPlatform, Level: LevelWarn, Msg: "dropped connection to the database"}, |
| + {Stream: StreamApp, Msg: notice.Msg}, |
| + } { |
| + if IsDropNotice(r) { |
| + t.Errorf("IsDropNotice(%+v) = true", r) |
| + } |
| + } |
| +} |
diff --git a/internal/console/apierrors_test.go b/internal/console/apierrors_test.go| index dec272a99..bc5dcab34 100644 |
| --- a/internal/console/apierrors_test.go |
| +++ b/internal/console/apierrors_test.go |
| @@ -34,7 +34,9 @@ func seedErrors(t *testing.T, fx *consoleFixture, now time.Time) { |
| warn := []applog.Record{{TS: now.Add(-7 * time.Minute), Box: "box1", Host: "titogo.example.com", |
| Stream: applog.StreamApp, Level: applog.LevelWarn, Msg: "slow query 4123ms", Seq: 150}, |
| {TS: now.Add(-6 * time.Minute), Box: "box1", Host: "titogo.example.com", |
| - Stream: applog.StreamPlatform, Level: applog.LevelWarn, Msg: "instance exited: signal: killed", Seq: 151}} |
| + Stream: applog.StreamPlatform, Level: applog.LevelWarn, Msg: "instance exited: signal: killed", Seq: 151}, |
| + {TS: now.Add(-6 * time.Minute), Box: "box1", Host: "titogo.example.com", |
| + Stream: applog.StreamPlatform, Level: applog.LevelWarn, Msg: "dropped 12 app log line(s) (rate cap)", Seq: 152}} |
| body, _ := applog.EncodeChunk(warn) |
| if _, err := fx.rel.Put(context.Background(), applog.ChunkKey(release.OpsAccount, "titogo", "box1", applog.HourOf(warn[0].TS), 150, len(body)), body, ""); err != nil { |
| t.Fatal(err) |
| @@ -308,6 +310,11 @@ func TestAvailability(t *testing.T) { |
| if _, ok := byName["500 POST /checkout"]; ok { |
| t.Fatal("the app's own 500 is Errors', not Availability's") |
| } |
| + for name := range byName { |
| + if strings.Contains(name, "rate cap") { |
| + t.Fatalf("the logging budget's drop notice is not an availability event: %q", name) |
| + } |
| + } |
| // Expanding the regrouped row finds both paths, by the FP the tab shows |
| // rather than the one stored. |
| resp = api("fp=" + old.FP) |
diff --git a/internal/edge/edgeerrors_test.go b/internal/edge/edgeerrors_test.go| index ca6f9764d..2f98961e1 100644 |
| --- a/internal/edge/edgeerrors_test.go |
| +++ b/internal/edge/edgeerrors_test.go |
| @@ -381,3 +381,53 @@ func TestEdgeFilesAbandonedWaitEndToEnd(t *testing.T) { |
| t.Fatalf("records = %+v, want one abandoned 502", recs) |
| } |
| } |
| + |
| +// TestEdgeFilesHopFailuresThroughServeHTTP pins the other half of the |
| +// tunnel rule end to end: the front box files nothing a proxy did not note |
| +// a cause for, so each hop failure it produces itself MUST note one, or it |
| +// is filed nowhere. A hub with no connected channel answers a -tunnel route |
| +// with 502 (tunnel) and a fleet route with 503 (fleet_dark). |
| +func TestEdgeFilesHopFailuresThroughServeHTTP(t *testing.T) { |
| + reg := openReg(t) |
| + if err := reg.UpsertFleet(registry.Fleet{Name: "acme", Account: "amadan", Bucket: "acme-data"}); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := reg.AddFleetBox(registry.FleetBox{Fleet: "acme", Label: "pi-1", TokenHash: sha256Hex("tok1"), Status: "active"}); err != nil { |
| + t.Fatal(err) |
| + } |
| + for _, rt := range []registry.Route{ |
| + {Host: "fleet.example", Kind: "instance", Status: "ready", App: "web", Region: registry.PlacementTarget("acme"), FleetBox: "pi-1"}, |
| + {Host: "tun.example", Kind: "service", Status: "ready", App: "web", Addr: "127.0.0.1:1", Tunnel: "pi-box"}, |
| + } { |
| + if err := reg.Upsert(rt); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + if err := reg.SetTunnel("tun.example", "pi-box"); err != nil { |
| + t.Fatal(err) |
| + } |
| + e, err := New(reg, nil) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + e.EnableTunnels(NewTunnelHub("tunnel.test", map[string]string{"pi-box": "sekrit"})) |
| + if err := e.Reload(); err != nil { |
| + t.Fatal(err) |
| + } |
| + sink := &memErrSink{} |
| + e.EnableErrors(sink) |
| + for host, want := range map[string]int{"fleet.example": 503, "tun.example": 502} { |
| + rec := httptest.NewRecorder() |
| + e.ServeHTTP(rec, httptest.NewRequest("GET", "http://"+host+"/", nil)) |
| + if rec.Code != want { |
| + t.Fatalf("%s = %d, want %d", host, rec.Code, want) |
| + } |
| + } |
| + causes := map[string]string{} |
| + for _, r := range sink.snap() { |
| + causes[r.Host] = r.Cause |
| + } |
| + if len(causes) != 2 || causes["fleet.example"] != apperr.CauseFleetDark || causes["tun.example"] != apperr.CauseTunnel { |
| + t.Fatalf("hop failures filed = %+v, want fleet_dark and tunnel, one each", sink.snap()) |
| + } |
| +} |
diff --git a/internal/edge/static_test.go b/internal/edge/static_test.go| index 3f74d442b..05d0c4e28 100644 |
| --- a/internal/edge/static_test.go |
| +++ b/internal/edge/static_test.go |
| @@ -15,6 +15,7 @@ import ( |
| "testing" |
| "time" |
| + "github.com/carlosframework/platform/internal/apperr" |
| "github.com/carlosframework/platform/internal/registry" |
| "github.com/carlosframework/platform/internal/release" |
| ) |
| @@ -431,6 +432,13 @@ func TestStaticNeverResolvedHasNoVersionHeader(t *testing.T) { |
| if got, ok := resp.Header["X-Carlos-Version"]; ok { |
| t.Fatalf("X-Carlos-Version present (%v) on a handler that has never served a version", got) |
| } |
| + // The 502 is the edge's own, and says so: Availability files it as |
| + // "the static site could not be read", not as the app answering. |
| + req, out := withOutcome(httptest.NewRequest("GET", "http://site.example.com/", nil)) |
| + h.ServeHTTP(httptest.NewRecorder(), req) |
| + if out.cause != apperr.CauseStatic { |
| + t.Fatalf("static 502 cause = %q, want %q", out.cause, apperr.CauseStatic) |
| + } |
| } |
| // TestStaticUnconfiguredHasNoVersionHeader: the pre-EnableStatic 404 is not a |