status: close allowedBoxKeys gap on Box.Metrics — schema guard was silently exempt
contract_test.go's allowedBoxKeys promises "any new field in Box must be explicitly added here to pass the test," but Box.Metrics landed without a fixture ever populating it, so the promise went unverified with nothing scheduled to close the gap later. Populates fixtureProbes()'s six Metrics fields (values kept distinct from dryrun.go's own needles, so the two harnesses never share fixture data), adds "metrics" to allowedBoxKeys with a comment on why it's exempt from the band-word rule, and adds the nested "metrics" path. allowedFleetKeys is untouched by design: that absence is now an active assertion (verified by temporarily wiring Metrics through FleetBox — TestFleetSchemaAllowlisted failed immediately with "unexpected key \"metrics\" at path \"boxes[]\"" — then reverting), a second, independent proof alongside dryrun.go's leak scan that BuildFleet never copies the block.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 file changed,
+40
−8
internal/status/contract_test.go+40 −8
diff --git a/internal/status/contract_test.go b/internal/status/contract_test.go| index 02cb6b135..16602c4da 100644 |
| --- a/internal/status/contract_test.go |
| +++ b/internal/status/contract_test.go |
| @@ -32,9 +32,22 @@ var allowedBoxKeys = map[string][]string{ |
| // "expired_canaries" is likewise deliberate (pipelines spec §5 "Canaries |
| // expire"): a COUNT of this box's own canary/* routes past their TTL, |
| // never which ones — no hostname, no channel name, no tenant identity. |
| - "": {"generated_at", "label", "region", "routes", "load", "capacity", "replication", "adoption_running", "expired_canaries"}, |
| + // |
| + // "metrics" is the one key in this allowlist that is EXEMPT from the |
| + // band-word-not-a-number rule every other Box field follows (Task 5, |
| + // internal/status/snapshot.go's package doc): it carries raw byte |
| + // counts, a load average, a release sha, and tunnel state, unbanded. |
| + // That's safe only because Box (and this "metrics" key with it) never |
| + // leaves the bucket — status/boxes/<label>.json is read by fleet-status |
| + // machinery and operators, never served publicly (internal/edge/status.go's |
| + // fixed three-object menu doesn't include it) — and because |
| + // allowedFleetKeys below has no "metrics" entry: BuildFleet's explicit |
| + // field-by-field copy into FleetBox never names it, so TestFleetSchemaAllowlisted |
| + // would fail the moment that ever changed. |
| + "": {"generated_at", "label", "region", "routes", "load", "capacity", "replication", "adoption_running", "expired_canaries", "metrics"}, |
| "routes": {"static", "resident", "hibernating", "awake", "asleep"}, |
| "replication": {"healthy", "lag_seconds"}, |
| + "metrics": {"mem_total_bytes", "mem_avail_bytes", "load1", "disk_free_bytes", "release", "tunnel"}, |
| } |
| // allowedFleetKeys are THE schema for Fleet's JSON output. Same friction as |
| @@ -180,12 +193,25 @@ func fixtureRoutes() []registry.Route { |
| // fixtureProbes creates test probes with known fractions so that banding |
| // produces non-nil results. It uses values in the middle bands so that |
| // load and capacity both resolve to band words (not nil). |
| +// |
| +// The six Metrics fields (memTotal/memAvail/load1/diskFree/release/tunnel) |
| +// are populated here too, with values DISTINCT from dryrun.go's own |
| +// dryRunMetricsNeedles fixtures — the two harnesses check different things |
| +// (this file's allowlist tests guard Box/Fleet's declared JSON schema; |
| +// dryrun.go's leak scan guards the real publish pipeline end to end) and |
| +// must never share fixture values, or a bug that only one harness would |
| +// catch could hide behind the other's coincidentally-matching needle. |
| func fixtureProbes() status.Probes { |
| memFrac := 0.75 // Between 0.70 and 0.90 -> "busy" |
| diskFrac := 0.80 // Between 0.70 and 0.90 -> "getting-full" |
| lagSecs := int64(300) // 5 minutes, healthy (< 15 min) |
| adopting := true // a real reading, so the field is scanned with a value in it |
| + memTotal := int64(8123456789) // distinct from dryrun.go's 4242424242 |
| + memAvail := int64(3123456789) // distinct from dryrun.go's 4242424241 |
| + diskFree := int64(1123456789) // distinct from dryrun.go's 4242424240 |
| + load1 := 1.23 // distinct from dryrun.go's 42.42 |
| + |
| // Listening function returns true for hibernating routes, false otherwise. |
| listening := func(sock string) *bool { |
| if strings.HasPrefix(sock, "/var/run/") { |
| @@ -197,13 +223,19 @@ func fixtureProbes() status.Probes { |
| } |
| return status.Probes{ |
| - MachineID: "test-machine-id", |
| - Region: "us-west-1", |
| - MemFrac: &memFrac, |
| - DiskFrac: &diskFrac, |
| - Listening: listening, |
| - LagSecs: &lagSecs, |
| - AdoptRunning: &adopting, |
| + MachineID: "test-machine-id", |
| + Region: "us-west-1", |
| + MemFrac: &memFrac, |
| + DiskFrac: &diskFrac, |
| + Listening: listening, |
| + LagSecs: &lagSecs, |
| + AdoptRunning: &adopting, |
| + MemTotalBytes: &memTotal, |
| + MemAvailBytes: &memAvail, |
| + Load1: &load1, |
| + DiskFreeBytes: &diskFree, |
| + Release: "contract-fixture-sha", // distinct from dryrun.go's "dryrun4242sha" |
| + Tunnel: "connected", // distinct from dryrun.go's "reconnecting" |
| } |
| } |