host: contract test — status publish never derives from metrics/ or alerts/
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 file changed,
+57
−0
internal/host/host_test.go+57 −0
diff --git a/internal/host/host_test.go b/internal/host/host_test.go| index 4312df011..d5171d04e 100644 |
| --- a/internal/host/host_test.go |
| +++ b/internal/host/host_test.go |
| @@ -2,6 +2,7 @@ package host |
| import ( |
| "context" |
| + "encoding/json" |
| "net" |
| "os" |
| "path/filepath" |
| @@ -11,6 +12,7 @@ import ( |
| "time" |
| "github.com/carlosframework/platform/internal/domains" |
| + "github.com/carlosframework/platform/internal/metrics" |
| "github.com/carlosframework/platform/internal/registry" |
| "github.com/carlosframework/platform/internal/release" |
| "github.com/carlosframework/platform/internal/status" |
| @@ -485,3 +487,58 @@ func TestAdoptTimerStateReading(t *testing.T) { |
| } |
| func boolPtr(v bool) *bool { return &v } |
| + |
| +// TestStatusPublishNeverReadsMetricsPrefixes is the privacy tripwire the |
| +// monitoring spec commits to: operator-grade needles live under metrics/ and |
| +// alerts/, and nothing the public status publisher writes may ever be |
| +// derived from them. Sentinel values planted in metrics objects must not |
| +// appear anywhere under status/ after a full publish tick. |
| +func TestStatusPublishNeverReadsMetricsPrefixes(t *testing.T) { |
| + ctx := context.Background() |
| + reg, err := registry.Open(filepath.Join(t.TempDir(), "registry.db")) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + store := release.NewDirStore(t.TempDir()) |
| + sentinel := 0.987654321 |
| + chunk := metrics.RawChunk{V: 1, Box: "box-a", Hour: "2026-08-02-10", Samples: []metrics.Sample{ |
| + {At: time.Now(), Host: metrics.HostSample{CPUFrac: &sentinel}}, |
| + }} |
| + if err := metrics.WriteRaw(ctx, store, chunk); err != nil { |
| + t.Fatal(err) |
| + } |
| + |
| + // Plant sentinel in alerts/boxes/ as well |
| + alertState := metrics.AlertState{ |
| + V: 1, |
| + Box: "box-a", |
| + UpdatedAt: time.Now(), |
| + Rules: map[string]metrics.RuleState{ |
| + "disk": {Firing: true, Value: sentinel}, |
| + }, |
| + } |
| + alertBody, err := json.Marshal(alertState) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := store.Put(ctx, metrics.AlertKey("box-a"), alertBody, ""); err != nil { |
| + t.Fatal(err) |
| + } |
| + |
| + if err := statusTick(ctx, store, reg, status.Probes{MachineID: "m"}, true); err != nil { |
| + t.Fatal(err) |
| + } |
| + keys, err := store.List(ctx, "status/") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + for _, k := range keys { |
| + body, _, err := store.Get(ctx, k) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if strings.Contains(string(body), "987654321") { |
| + t.Errorf("%s contains the metrics sentinel — the status publisher read a metrics object", k) |
| + } |
| + } |
| +} |