Merge branch 'console-ux-11-status-check' into main
2 files changed,
+72
−8
internal/status/dryrun.go+45 −8internal/status/dryrun_internal_test.go+27 −0
CI passed — run details
diff --git a/internal/status/dryrun.go b/internal/status/dryrun.go| index 557002f3d..bf1f99eea 100644 |
| --- a/internal/status/dryrun.go |
| +++ b/internal/status/dryrun.go |
| @@ -32,20 +32,23 @@ const dryRunOptInApp = "demoapp" |
| // registry and probes. None of these substrings may appear in ANY object |
| // DryRun publishes. "prod-secret" is listed both whole and as a bare prefix |
| // so a leak that only carries part of the hostname (e.g. truncated in some |
| -// future summary field) still trips the scan. "0.73"/"0.95" are the exact |
| -// literal fractions fed into the probes — if either ever showed up verbatim |
| -// in published JSON, a raw ratio escaped banding somewhere upstream of the |
| -// digit-in-band-fields check below (which catches band words carrying ANY |
| -// digit, not just these two). |
| +// future summary field) still trips the scan. The fixture fractions are |
| +// checked as JSON numbers separately: timestamps can legitimately contain |
| +// the same digits in their fractional seconds. |
| var dryRunNeedles = []string{ |
| "prod-secret.example.com", // fixture hostname |
| "prod-secret", // hostname's leak-prone substring on its own |
| "example.com", // the fixture domain generally |
| "ip-10-0-0-7", // fixture machine id |
| - "0.73", // fixture MemFrac, verbatim |
| - "0.95", // fixture DiskFrac, verbatim |
| } |
| +const ( |
| + dryRunMemFrac = 0.73 |
| + dryRunDiskFrac = 0.95 |
| +) |
| + |
| +var dryRunFractionNeedles = []float64{dryRunMemFrac, dryRunDiskFrac} |
| + |
| // dryRunMetricsNeedles are the distinctive raw values Task 5's BoxMetrics |
| // carries — real numbers and strings that Box.Metrics is the ONE deliberate |
| // exception privileged to publish (see snapshot.go's package doc), and which |
| @@ -153,7 +156,7 @@ func dryRunFixtures() ([]registry.Route, Probes) { |
| }, |
| } |
| - memFrac, diskFrac := 0.73, 0.95 |
| + memFrac, diskFrac := dryRunMemFrac, dryRunDiskFrac |
| lag := int64(120) |
| adopting := true // a known reading, so adoption_running publishes non-null and gets scanned |
| configPath := true // ditto for config_path_running (issue #44) |
| @@ -251,6 +254,37 @@ func scanBandFieldsForDigits(v interface{}) []string { |
| return bad |
| } |
| +func scanForNumericLeaks(v interface{}, banned []float64) []string { |
| + seen := map[float64]bool{} |
| + var walk func(interface{}) |
| + walk = func(v interface{}) { |
| + switch t := v.(type) { |
| + case map[string]interface{}: |
| + for _, value := range t { |
| + walk(value) |
| + } |
| + case []interface{}: |
| + for _, value := range t { |
| + walk(value) |
| + } |
| + case float64: |
| + for _, needle := range banned { |
| + if t == needle { |
| + seen[needle] = true |
| + } |
| + } |
| + } |
| + } |
| + walk(v) |
| + var found []string |
| + for _, needle := range banned { |
| + if seen[needle] { |
| + found = append(found, fmt.Sprintf("%g", needle)) |
| + } |
| + } |
| + return found |
| +} |
| + |
| // DryRun runs the real publish pipeline into a DirStore rooted at dir, then |
| // reads back everything it just published and fails on any leaked needle — |
| // see the package doc comment above for why. Progress (each published key) |
| @@ -372,6 +406,9 @@ func DryRun(dir string, out io.Writer) error { |
| // fields. |
| var decoded interface{} |
| if err := json.Unmarshal(body, &decoded); err == nil { |
| + if leaks := scanForNumericLeaks(decoded, dryRunFractionNeedles); len(leaks) > 0 { |
| + return fmt.Errorf("dry run: %s leaked %v", key, leaks) |
| + } |
| if bad := scanBandFieldsForDigits(decoded); len(bad) > 0 { |
| return fmt.Errorf("dry run: %s: band field carries a digit: %v", key, bad) |
| } |
diff --git a/internal/status/dryrun_internal_test.go b/internal/status/dryrun_internal_test.go| new file mode 100644 |
| index 000000000..142552f28 |
| --- /dev/null |
| +++ b/internal/status/dryrun_internal_test.go |
| @@ -0,0 +1,27 @@ |
| +package status |
| + |
| +import ( |
| + "encoding/json" |
| + "reflect" |
| + "testing" |
| +) |
| + |
| +func TestScanForNumericLeaksFindsNestedFixtureFractions(t *testing.T) { |
| + var decoded interface{} |
| + if err := json.Unmarshal([]byte(`{"object":{"ratio":0.95},"list":[{"ratio":0.73}]}`), &decoded); err != nil { |
| + t.Fatal(err) |
| + } |
| + if got, want := scanForNumericLeaks(decoded, dryRunFractionNeedles), []string{"0.73", "0.95"}; !reflect.DeepEqual(got, want) { |
| + t.Fatalf("numeric leaks = %v, want %v", got, want) |
| + } |
| +} |
| + |
| +func TestScanForNumericLeaksIgnoresTimestampText(t *testing.T) { |
| + var decoded interface{} |
| + if err := json.Unmarshal([]byte(`{"generated_at":"2026-09-09T16:15:10.950730Z"}`), &decoded); err != nil { |
| + t.Fatal(err) |
| + } |
| + if got := scanForNumericLeaks(decoded, dryRunFractionNeedles); len(got) != 0 { |
| + t.Fatalf("timestamp text reported as numeric leak: %v", got) |
| + } |
| +} |