carlos / platform Public

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>
Bill Horsman pushed by bill@keymail.dev a3170dec97e5a76f0f0adfb09bcc39ac3cb52e06 parent 6cb9f87
4 files changed, +94 −1
  • internal/applog/sink_test.go +28 −0
  • internal/console/apierrors_test.go +8 −1
  • internal/edge/edgeerrors_test.go +50 −0
  • internal/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