From 85c37443a6bdd8ec7de06747befc89651f8d0257 Mon Sep 17 00:00:00 2001 From: Richard Courtman Date: Fri, 4 Sep 2026 10:07:27 +0100 Subject: [PATCH 1/2] Stop test binaries reporting to the production telemetry endpoint Backport of the main-branch guard. pkg/server tests boot the real server through Run() with the version literal "test-version", which normalizes to 0.0.0-test-version, and each test runs against its own t.TempDir(), so every run mints a fresh install ID. The service-health failure reporter sends synchronously from a deferred handler as soon as Run() returns an error, so any test exercising a startup failure posts one ping. This line still emitted after main was fixed: release-line lane work runs pkg/server tests on this branch, and those pings arrive with the old version classifier too, so they land mislabelled as ordinary prereleases and re-contaminate install-population reads that were just corrected. send() now refuses the production endpoint whenever testing.Testing() reports true. The check compares against productionPingEndpoint, so telemetry's own tests keep asserting on ping content through a redirected endpoint. Verified on this branch: three runs of the failing-startup tests, zero pings received. --- .../internal/subsystems/security-privacy.md | 9 ++++ internal/telemetry/telemetry.go | 19 ++++++- internal/telemetry/telemetry_test.go | 53 +++++++++++++++++++ 3 files changed, 80 insertions(+), 1 deletion(-) diff --git a/docs/release-control/v6/internal/subsystems/security-privacy.md b/docs/release-control/v6/internal/subsystems/security-privacy.md index 93bebb1ed..794dd1927 100644 --- a/docs/release-control/v6/internal/subsystems/security-privacy.md +++ b/docs/release-control/v6/internal/subsystems/security-privacy.md @@ -445,6 +445,15 @@ the `white_label` branding entitlement. runs per event so runtime mock toggles take effect immediately — because a mock-mode snapshot describes the synthetic fixture fleet rather than a real installation. + A Go test binary is the same kind of suppression boundary: while + `testing.Testing()` reports true, `internal/telemetry` must not post to the + production receiver on any path, including the synchronous service-health + failure event `pkg/server.Run` sends from its deferred handler. A test that + boots the real server runs against a throwaway data directory, so it mints + a fresh install ID on every run and the receiver counts it as a distinct + live installation. The guard compares the resolved endpoint against + `productionPingEndpoint`, so tests that redirect `pingEndpoint` at a local + server keep asserting on real ping content. Pulse Intelligence external-agent/MCP telemetry may expose only content-free adapter-origin usage and capability-class counters for context, event stream, provisioning, operator state, finding, and action requests. It must diff --git a/internal/telemetry/telemetry.go b/internal/telemetry/telemetry.go index 0506f9e2e..6a9001bef 100644 --- a/internal/telemetry/telemetry.go +++ b/internal/telemetry/telemetry.go @@ -96,6 +96,7 @@ import ( "runtime" "strings" "sync" + "testing" "time" "github.com/google/uuid" @@ -106,10 +107,17 @@ import ( // pingEndpoint is the URL that receives outbound usage telemetry pings. // It is a var (not const) so that tests can redirect it to a local server. -var pingEndpoint = "https://license.pulserelay.pro/v1/telemetry/ping" +// productionPingEndpoint is the live receiver for outbound usage telemetry. +const productionPingEndpoint = "https://license.pulserelay.pro/v1/telemetry/ping" + +var pingEndpoint = productionPingEndpoint var errInstallIDUnavailable = errors.New("telemetry install id unavailable") +// errProductionEndpointUnderTest reports a ping suppressed because a test +// binary tried to reach the live receiver. +var errProductionEndpointUnderTest = errors.New("telemetry: refusing to post to the production endpoint from a test binary") + const ( // heartbeatInterval is the base interval between daily pings. // Each cycle adds random jitter of ±maxHeartbeatJitter to prevent @@ -1657,6 +1665,15 @@ func buildPingAt(cfg Config, event string, now time.Time) (Ping, error) { // send posts a ping to the telemetry endpoint. Errors are observable in debug // logs but never affect normal Pulse operation. func send(ctx context.Context, ping Ping) error { + // A test binary is not a real installation. Any test that boots the real + // server (pkg/server.Run and anything like it) runs against a throwaway + // data directory, so it mints a fresh install ID per run and would be + // counted as a distinct live install. Telemetry's own tests redirect + // pingEndpoint at a local server and are unaffected by this guard. + if testing.Testing() && pingEndpoint == productionPingEndpoint { + return errProductionEndpointUnderTest + } + body, err := json.Marshal(ping) if err != nil { return err diff --git a/internal/telemetry/telemetry_test.go b/internal/telemetry/telemetry_test.go index 70aeaa5c2..ec3b68d0d 100644 --- a/internal/telemetry/telemetry_test.go +++ b/internal/telemetry/telemetry_test.go @@ -2,6 +2,7 @@ package telemetry import ( "encoding/json" + "errors" "fmt" "io" "io/fs" @@ -1324,3 +1325,55 @@ func TestBuildPingCarriesNodeTestCounts(t *testing.T) { t.Fatalf("node_test_failures_30d = %d, want 4", ping.NodeTestFailures30d) } } + +// The production telemetry receiver must be unreachable from a Go test binary. +// A test that boots the real server runs against a throwaway data directory, +// so it mints a fresh install ID on every run and lands at the receiver as a +// distinct live installation. +func TestSendRefusesProductionEndpointUnderTest(t *testing.T) { + if pingEndpoint != productionPingEndpoint { + t.Fatalf("pingEndpoint = %q, want the production endpoint by default", pingEndpoint) + } + + err := send(context.Background(), Ping{Event: "startup"}) + if !errors.Is(err, errProductionEndpointUnderTest) { + t.Fatalf("send() to the production endpoint = %v, want errProductionEndpointUnderTest", err) + } +} + +// SendServiceHealthEvent is the path pkg/server.Run takes when startup fails, +// and it sends synchronously rather than after the startup delay, which is why +// the failing server tests reported and the passing ones did not. +func TestSendServiceHealthEventRefusesProductionEndpointUnderTest(t *testing.T) { + cfg := Config{Version: "test-version", DataDir: t.TempDir(), Enabled: true} + + err := SendServiceHealthEvent(context.Background(), cfg, "startup", ServiceHealthObservation{ + Observed: true, + FailureCategory: ServiceHealthFailureListener, + }) + if !errors.Is(err, errProductionEndpointUnderTest) { + t.Fatalf("SendServiceHealthEvent() = %v, want errProductionEndpointUnderTest", err) + } +} + +// A redirected endpoint is how telemetry's own tests assert on ping content, +// so the guard must not block it. +func TestSendAllowsRedirectedEndpointUnderTest(t *testing.T) { + var received atomic.Int32 + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + received.Add(1) + w.WriteHeader(http.StatusOK) + })) + defer ts.Close() + + origEndpoint := pingEndpoint + pingEndpoint = ts.URL + defer func() { pingEndpoint = origEndpoint }() + + if err := send(context.Background(), Ping{Event: "startup"}); err != nil { + t.Fatalf("send() to a redirected endpoint: %v", err) + } + if got := received.Load(); got != 1 { + t.Fatalf("redirected endpoint received %d pings, want 1", got) + } +} From 16564289821dca1ebfe0a207fc96b5c7c2eda265 Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Fri, 4 Sep 2026 05:48:37 +0100 Subject: [PATCH 2/2] Keep explicitly included tmpfs mounts distinct Operators can explicitly select tmpfs paths that automatic filesystem filtering would otherwise hide. Multiple such mounts commonly share both the generic tmpfs device name and capacity, so applying normal storage deduplication to them silently dropped configured disks from collection. Limit device-and-capacity deduplication to normally visible storage while preserving it for NAS bind mounts and subvolumes. Change-source: issue #1875 --- internal/hostmetrics/collector.go | 39 +++++++++++++++----------- internal/hostmetrics/collector_test.go | 36 ++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 17 deletions(-) diff --git a/internal/hostmetrics/collector.go b/internal/hostmetrics/collector.go index 557f8f6f5..ec0769d92 100644 --- a/internal/hostmetrics/collector.go +++ b/internal/hostmetrics/collector.go @@ -402,29 +402,34 @@ func collectDisksWithIncludes(ctx context.Context, diskExclude, diskInclude []st // - Virtual/pseudo filesystems (tmpfs, devtmpfs, cgroup, etc.) // - Container overlay paths (Docker/Podman layers on ZFS, including TrueNAS .ix-apps) // See issues #505, #690, #718, #790. - if shouldSkip, _ := fsfilters.ShouldSkipFilesystem(part.Fstype, part.Mountpoint, usage.Total, usage.Used); shouldSkip && !explicitlyIncluded { + automaticallyFiltered, _ := fsfilters.ShouldSkipFilesystem(part.Fstype, part.Mountpoint, usage.Total, usage.Used) + if automaticallyFiltered && !explicitlyIncluded { continue } - // Deduplicate by device + total bytes (issue #953). - // Synology NAS and similar systems create multiple "shared folders" as bind mounts - // or BTRFS subvolumes that all report the same device and total capacity. - // Only count each unique device+total combination once. - deviceKey := fmt.Sprintf("%s:%d", part.Device, usage.Total) - if existingMount, exists := deviceTotals[deviceKey]; exists { - // Prefer shorter/shallower mountpoints (e.g., /volume1 over /volume1/docker) - if len(part.Mountpoint) >= len(existingMount) { - continue - } - // This mountpoint is shallower - remove the old entry and use this one - for i := len(disks) - 1; i >= 0; i-- { - if disks[i].Mountpoint == existingMount { - disks = append(disks[:i], disks[i+1:]...) - break + // Deduplicate normally visible storage by device + total bytes (issue + // #953). Synology NAS and similar systems create multiple shared folders + // that report the same underlying capacity. Automatically filtered + // filesystems are different: generic sources such as "tmpfs" can name + // multiple independent mounts with equal capacity, and these entries are + // present only because the operator explicitly selected each one. + if !automaticallyFiltered { + deviceKey := fmt.Sprintf("%s:%d", part.Device, usage.Total) + if existingMount, exists := deviceTotals[deviceKey]; exists { + // Prefer shorter/shallower mountpoints (e.g., /volume1 over /volume1/docker) + if len(part.Mountpoint) >= len(existingMount) { + continue + } + // This mountpoint is shallower - remove the old entry and use this one + for i := len(disks) - 1; i >= 0; i-- { + if disks[i].Mountpoint == existingMount { + disks = append(disks[:i], disks[i+1:]...) + break + } } } + deviceTotals[deviceKey] = part.Mountpoint } - deviceTotals[deviceKey] = part.Mountpoint disks = append(disks, agentshost.Disk{ Device: part.Device, diff --git a/internal/hostmetrics/collector_test.go b/internal/hostmetrics/collector_test.go index c93327db6..7f9ff896a 100644 --- a/internal/hostmetrics/collector_test.go +++ b/internal/hostmetrics/collector_test.go @@ -225,6 +225,42 @@ func TestCollectDisksIncludesExplicitTmpfsMount(t *testing.T) { } } +func TestCollectDisksKeepsDistinctExplicitTmpfsMountsWithEqualCapacity(t *testing.T) { + origPartitions := diskPartitions + origUsage := diskUsage + t.Cleanup(func() { + diskPartitions = origPartitions + diskUsage = origUsage + }) + + diskPartitions = func(context.Context, bool) ([]godisk.PartitionStat, error) { + return []godisk.PartitionStat{ + {Device: "tmpfs", Mountpoint: "/var/log", Fstype: "tmpfs"}, + {Device: "tmpfs", Mountpoint: "/mnt/ramdisk/plex-transcode", Fstype: "tmpfs"}, + }, nil + } + diskUsage = func(_ context.Context, path string) (*godisk.UsageStat, error) { + return &godisk.UsageStat{ + Path: path, + Total: 1024, + Used: 768, + Free: 256, + UsedPercent: 75, + }, nil + } + + disks := collectDisksWithIncludes(context.Background(), nil, []string{ + "/var/log", + "/mnt/ramdisk/plex-transcode", + }) + if len(disks) != 2 { + t.Fatalf("distinct explicitly included tmpfs mounts were deduplicated: %+v", disks) + } + if disks[0].Mountpoint != "/mnt/ramdisk/plex-transcode" || disks[1].Mountpoint != "/var/log" { + t.Fatalf("explicit tmpfs mounts = %+v", disks) + } +} + func TestCollectDisksExplicitExcludeWinsOverInclude(t *testing.T) { origPartitions := diskPartitions origUsage := diskUsage