mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 10:35:51 +00:00
139ee65b25
pkg/server tests boot the real server through Run() with the version literal "test-version", which internal/updates normalizes to 0.0.0-test-version. Each test runs against its own t.TempDir(), so every run minted a fresh install ID. The startup ping waits two minutes and so never fired inside a short test, but the service-health failure reporter added on 2026-08-29 sends synchronously from a deferred handler as soon as Run() returns an error, so every CI shard containing pkg/server posted one ping. The licence server recorded 317 single-ping installs between 2026-08-29 and 2026-09-03 - 311 from linux/amd64 CI runners, 3 from a maintainer workstation - still arriving at roughly 60 a day. The canonical clean denominator excludes single-ping installs and was unaffected, but raw install counts and the operator-evidence blocked-cause read counted them as real installations. A test binary is not an installation, which is the same reason mock mode already suppresses pings, so the guard belongs beside it in the telemetry package rather than at the four call sites: send() now refuses the production endpoint whenever testing.Testing() reports true. The check compares against productionPingEndpoint, so telemetry's own tests keep asserting on real ping content through a redirected endpoint, and the server tests additionally opt out at the config layer to say so locally.
1428 lines
51 KiB
Go
1428 lines
51 KiB
Go
package telemetry
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"regexp"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"context"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/testutil"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/updates"
|
|
)
|
|
|
|
func TestGetOrCreateInstallID_CreatesNew(t *testing.T) {
|
|
dir := t.TempDir()
|
|
now := time.Date(2026, 3, 28, 12, 0, 0, 0, time.UTC)
|
|
id := getOrCreateInstallIDAt(dir, now)
|
|
if id == "" {
|
|
t.Fatal("expected non-empty install ID")
|
|
}
|
|
|
|
// Verify file was persisted.
|
|
data, err := os.ReadFile(filepath.Join(dir, installIDFile))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
record := decodeInstallIDRecord(t, data)
|
|
if record.InstallID != id {
|
|
t.Fatalf("persisted install ID = %q, want %q", record.InstallID, id)
|
|
}
|
|
if !record.IssuedAt.Equal(now) {
|
|
t.Fatalf("persisted issued_at = %v, want %v", record.IssuedAt, now)
|
|
}
|
|
}
|
|
|
|
func TestGetOrCreateInstallID_ReusesExisting(t *testing.T) {
|
|
dir := t.TempDir()
|
|
now := time.Date(2026, 3, 28, 12, 0, 0, 0, time.UTC)
|
|
|
|
// Create first ID.
|
|
id1 := getOrCreateInstallIDAt(dir, now)
|
|
|
|
// Second call should return the same ID.
|
|
id2 := getOrCreateInstallIDAt(dir, now.Add(7*24*time.Hour))
|
|
if id1 != id2 {
|
|
t.Fatalf("expected same ID across calls, got %q and %q", id1, id2)
|
|
}
|
|
}
|
|
|
|
func TestGetOrCreateInstallID_RegeneratesInvalid(t *testing.T) {
|
|
dir := t.TempDir()
|
|
// Write garbage.
|
|
if err := os.WriteFile(filepath.Join(dir, installIDFile), []byte("not-a-uuid\n"), 0600); err != nil {
|
|
t.Fatalf("write install id file: %v", err)
|
|
}
|
|
|
|
id := getOrCreateInstallIDAt(dir, time.Date(2026, 3, 28, 12, 0, 0, 0, time.UTC))
|
|
if id == "" || id == "not-a-uuid" {
|
|
t.Fatalf("expected new valid UUID, got %q", id)
|
|
}
|
|
}
|
|
|
|
func TestGetOrCreateInstallID_RotatesExpiredRecord(t *testing.T) {
|
|
dir := t.TempDir()
|
|
start := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC)
|
|
|
|
first := getOrCreateInstallIDAt(dir, start)
|
|
second := getOrCreateInstallIDAt(dir, start.Add(installIDRotationWindow+time.Hour))
|
|
if first == second {
|
|
t.Fatalf("expected install ID rotation after %v, got same value %q", installIDRotationWindow, first)
|
|
}
|
|
}
|
|
|
|
func TestGetOrCreateInstallID_RotatesLegacyPlaintextID(t *testing.T) {
|
|
dir := t.TempDir()
|
|
legacyID := uuid.New().String()
|
|
if err := os.WriteFile(filepath.Join(dir, installIDFile), []byte(legacyID+"\n"), 0600); err != nil {
|
|
t.Fatalf("WriteFile: %v", err)
|
|
}
|
|
|
|
now := time.Date(2026, 3, 28, 12, 0, 0, 0, time.UTC)
|
|
rotated := getOrCreateInstallIDAt(dir, now)
|
|
if rotated == legacyID {
|
|
t.Fatalf("expected legacy plaintext install ID to rotate, got same value %q", rotated)
|
|
}
|
|
|
|
record := decodeInstallIDRecordFile(t, filepath.Join(dir, installIDFile))
|
|
if record.InstallID != rotated {
|
|
t.Fatalf("persisted install ID = %q, want %q", record.InstallID, rotated)
|
|
}
|
|
}
|
|
|
|
func TestResetInstallID_RewritesRecordImmediately(t *testing.T) {
|
|
dir := t.TempDir()
|
|
start := time.Date(2026, 3, 1, 12, 0, 0, 0, time.UTC)
|
|
original := getOrCreateInstallIDAt(dir, start)
|
|
|
|
resetAt := start.Add(12 * time.Hour)
|
|
rotated, err := resetInstallIDAt(dir, resetAt)
|
|
if err != nil {
|
|
t.Fatalf("resetInstallIDAt: %v", err)
|
|
}
|
|
if rotated == "" {
|
|
t.Fatal("expected non-empty rotated install ID")
|
|
}
|
|
if rotated == original {
|
|
t.Fatalf("expected reset to rotate install ID, got same value %q", rotated)
|
|
}
|
|
|
|
record := decodeInstallIDRecordFile(t, filepath.Join(dir, installIDFile))
|
|
if record.InstallID != rotated {
|
|
t.Fatalf("persisted install ID = %q, want %q", record.InstallID, rotated)
|
|
}
|
|
if !record.IssuedAt.Equal(resetAt) {
|
|
t.Fatalf("persisted issued_at = %v, want %v", record.IssuedAt, resetAt)
|
|
}
|
|
}
|
|
|
|
func TestIsEnabled(t *testing.T) {
|
|
tests := []struct {
|
|
value string
|
|
want bool
|
|
}{
|
|
{"true", true},
|
|
{"1", true},
|
|
{"false", false},
|
|
{"", true}, // enabled by default when env var is not set
|
|
{"0", false},
|
|
{"yes", false}, // only "true"/"1" are truthy; unknown values disable
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.value, func(t *testing.T) {
|
|
t.Setenv("PULSE_TELEMETRY", tt.value)
|
|
if got := IsEnabled(); got != tt.want {
|
|
t.Fatalf("IsEnabled() = %v for PULSE_TELEMETRY=%q, want %v", got, tt.value, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestClassifyPulseIntelligenceProActivationProof(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input PulseIntelligenceProActivationProofInput
|
|
want PulseIntelligenceProActivationProof
|
|
}{
|
|
{
|
|
name: "not started",
|
|
want: PulseIntelligenceProActivationProof{
|
|
ValueProofState: PulseIntelligenceProActivationValueProofNotStarted,
|
|
},
|
|
},
|
|
{
|
|
name: "starter only stays in progress",
|
|
input: PulseIntelligenceProActivationProofInput{
|
|
ProActivationStarterCount: 1,
|
|
},
|
|
want: PulseIntelligenceProActivationProof{
|
|
ValueProofState: PulseIntelligenceProActivationValueProofInProgress,
|
|
},
|
|
},
|
|
{
|
|
name: "approved verified outcome resolves without external agent readiness",
|
|
input: PulseIntelligenceProActivationProofInput{
|
|
ProActivationStarterCount: 1,
|
|
PatrolIssueEvidenceCount: 1,
|
|
ContextualCollaborationCount: 1,
|
|
ApprovedDecisionCount: 1,
|
|
VerifiedOutcomeCount: 1,
|
|
},
|
|
want: PulseIntelligenceProActivationProof{
|
|
Completed: true,
|
|
Resolved: true,
|
|
ValueProofState: PulseIntelligenceProActivationValueProofVerified,
|
|
},
|
|
},
|
|
{
|
|
name: "rejected governed decision records proof state before full completion",
|
|
input: PulseIntelligenceProActivationProofInput{
|
|
ProActivationStarterCount: 1,
|
|
RejectedDecisionCount: 1,
|
|
},
|
|
want: PulseIntelligenceProActivationProof{
|
|
ValueProofState: PulseIntelligenceProActivationValueProofGovernedDecisionRecorded,
|
|
},
|
|
},
|
|
{
|
|
name: "rejected governed decision completes without resolving",
|
|
input: PulseIntelligenceProActivationProofInput{
|
|
ProActivationStarterCount: 1,
|
|
PatrolIssueEvidenceCount: 1,
|
|
ContextualCollaborationCount: 1,
|
|
RejectedDecisionCount: 1,
|
|
},
|
|
want: PulseIntelligenceProActivationProof{
|
|
Completed: true,
|
|
ValueProofState: PulseIntelligenceProActivationValueProofGovernedDecisionRecorded,
|
|
},
|
|
},
|
|
{
|
|
name: "approved verified outcome resolves",
|
|
input: PulseIntelligenceProActivationProofInput{
|
|
ProActivationStarterCount: 1,
|
|
PatrolIssueEvidenceCount: 1,
|
|
ContextualCollaborationCount: 1,
|
|
ApprovedDecisionCount: 1,
|
|
VerifiedOutcomeCount: 1,
|
|
ExternalAgentReady: true,
|
|
},
|
|
want: PulseIntelligenceProActivationProof{
|
|
Completed: true,
|
|
Resolved: true,
|
|
ValueProofState: PulseIntelligenceProActivationValueProofVerified,
|
|
},
|
|
},
|
|
{
|
|
name: "approved decision without verified outcome stays in progress",
|
|
input: PulseIntelligenceProActivationProofInput{
|
|
ProActivationStarterCount: 1,
|
|
PatrolIssueEvidenceCount: 1,
|
|
ContextualCollaborationCount: 1,
|
|
ApprovedDecisionCount: 1,
|
|
ExternalAgentReady: true,
|
|
},
|
|
want: PulseIntelligenceProActivationProof{
|
|
ValueProofState: PulseIntelligenceProActivationValueProofInProgress,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := ClassifyPulseIntelligenceProActivationProof(tt.input); got != tt.want {
|
|
t.Fatalf("ClassifyPulseIntelligenceProActivationProof() = %#v, want %#v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestClassifyPulseIntelligencePatrolControlProof(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input PulseIntelligencePatrolControlProofInput
|
|
want PulseIntelligencePatrolControlProof
|
|
}{
|
|
{
|
|
name: "not started",
|
|
want: PulseIntelligencePatrolControlProof{
|
|
ValueProofState: PulseIntelligenceProActivationValueProofNotStarted,
|
|
},
|
|
},
|
|
{
|
|
name: "starter only stays in progress",
|
|
input: PulseIntelligencePatrolControlProofInput{
|
|
PatrolControlStarterCount: 1,
|
|
},
|
|
want: PulseIntelligencePatrolControlProof{
|
|
ValueProofState: PulseIntelligenceProActivationValueProofInProgress,
|
|
},
|
|
},
|
|
{
|
|
name: "rejected governed decision completes without resolving",
|
|
input: PulseIntelligencePatrolControlProofInput{
|
|
PatrolControlStarterCount: 1,
|
|
PatrolIssueEvidenceCount: 1,
|
|
ContextualCollaborationCount: 1,
|
|
RejectedDecisionCount: 1,
|
|
},
|
|
want: PulseIntelligencePatrolControlProof{
|
|
Completed: true,
|
|
ValueProofState: PulseIntelligenceProActivationValueProofGovernedDecisionRecorded,
|
|
},
|
|
},
|
|
{
|
|
name: "approved verified outcome resolves",
|
|
input: PulseIntelligencePatrolControlProofInput{
|
|
PatrolControlStarterCount: 1,
|
|
PatrolIssueEvidenceCount: 1,
|
|
ContextualCollaborationCount: 1,
|
|
ApprovedDecisionCount: 1,
|
|
VerifiedOutcomeCount: 1,
|
|
ExternalAgentReady: true,
|
|
},
|
|
want: PulseIntelligencePatrolControlProof{
|
|
Completed: true,
|
|
Resolved: true,
|
|
ValueProofState: PulseIntelligenceProActivationValueProofVerified,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := ClassifyPulseIntelligencePatrolControlProof(tt.input); got != tt.want {
|
|
t.Fatalf("ClassifyPulseIntelligencePatrolControlProof() = %#v, want %#v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
|
|
legacy := ClassifyPulseIntelligencePatrolAutonomyProof(PulseIntelligencePatrolAutonomyProofInput{
|
|
PatrolAutonomyStarterCount: 1,
|
|
PatrolIssueEvidenceCount: 1,
|
|
ContextualCollaborationCount: 1,
|
|
ApprovedDecisionCount: 1,
|
|
VerifiedOutcomeCount: 1,
|
|
})
|
|
if legacy != (PulseIntelligencePatrolAutonomyProof{
|
|
Completed: true,
|
|
Resolved: true,
|
|
ValueProofState: PulseIntelligenceProActivationValueProofVerified,
|
|
}) {
|
|
t.Fatalf("legacy Patrol autonomy classifier alias drifted: %#v", legacy)
|
|
}
|
|
}
|
|
|
|
func TestApplySnapshot(t *testing.T) {
|
|
base := Ping{
|
|
InstallID: "test-id",
|
|
Version: "6.0.0",
|
|
Platform: "docker",
|
|
OS: "linux",
|
|
Arch: "amd64",
|
|
}
|
|
|
|
snap := func() Snapshot {
|
|
return Snapshot{
|
|
PVENodes: 3,
|
|
VMs: 10,
|
|
Containers: 5,
|
|
AgentHosts: 2,
|
|
DockerContainers: 12,
|
|
KubernetesPods: 18,
|
|
StoragePools: 4,
|
|
PhysicalDisks: 9,
|
|
TrueNASSystems: 1,
|
|
TrueNASApps: 3,
|
|
VMwareHosts: 2,
|
|
AvailabilityTargets: 6,
|
|
AIEnabled: true,
|
|
PatrolEnabled: true,
|
|
DiscoveryEnabled: true,
|
|
NotificationsEnabled: true,
|
|
AIActionsEnabled: true,
|
|
AlertAIEnabled: true,
|
|
ActiveAlerts: 2,
|
|
PaidLicense: true,
|
|
HasAPITokens: true,
|
|
RBACCustomRoles: 3,
|
|
RBACUserAssignments: 7,
|
|
AuditReads30d: 41,
|
|
ReportSchedules: 5,
|
|
ReportSchedulesEnabled: 4,
|
|
ReportSchedulesRun30d: 2,
|
|
AgentProfiles: 9,
|
|
UpdateAttempts30d: 4,
|
|
UpdateSuccesses30d: 2,
|
|
UpdateFailures30d: 1,
|
|
UpdateLastFailureCategory: "download",
|
|
ActiveAlertsCritical: 2,
|
|
ActiveAlertsAge1h24h: 2,
|
|
AlertsFiredCritical30d: 5,
|
|
AlertsResolvedCritical30d: 3,
|
|
AlertsResolution1h24h30d: 3,
|
|
AlertsRepeatOccurrences30d: 2,
|
|
AlertsSnoozedOccurrences30d: 2,
|
|
AlertsResolvedWhileSnoozed30d: 1,
|
|
AlertManagerTenants: 2,
|
|
AlertDeliveryActiveTenants: 1,
|
|
AlertFlappingEnabledTenants: 2,
|
|
AlertIntentPolicyConfiguredTenants: 1,
|
|
AlertEventHistoryAuthoritativeTenants: 2,
|
|
AlertActiveStateAuthoritativeTenants: 2,
|
|
PulseIntelligenceLoopConfigured: true,
|
|
PulseIntelligenceLoopActive30d: true,
|
|
PulseIntelligenceCompleteOperationsLoop30d: true,
|
|
PulseIntelligenceApprovedExecutionLoop30d: true,
|
|
PulseIntelligenceResolvedOperationsLoop30d: true,
|
|
PulseIntelligencePatrolControlCompletedOperationsLoop30d: true,
|
|
PulseIntelligencePatrolControlResolvedOperationsLoop30d: true,
|
|
PulseIntelligencePatrolControlPaidCompletedOperationsLoop30d: true,
|
|
PulseIntelligencePatrolControlPaidResolvedOperationsLoop30d: true,
|
|
PulseIntelligenceProActivationCompletedOperationsLoop30d: true,
|
|
PulseIntelligenceProActivationResolvedOperationsLoop30d: true,
|
|
PulseIntelligenceProActivationPaidCompletedOperationsLoop30d: true,
|
|
PulseIntelligenceProActivationPaidResolvedOperationsLoop30d: true,
|
|
PulseIntelligenceGovernedActionActive30d: true,
|
|
PulseIntelligenceAssistantOperationsLoop30d: true,
|
|
PulseIntelligenceAssistantApprovedExecutionLoop30d: true,
|
|
PulseIntelligenceAssistantApprovedActionSuccessLoop30d: true,
|
|
PulseIntelligenceAssistantResolvedOperationsLoop30d: true,
|
|
PulseIntelligenceExternalAgentOperationsLoop30d: true,
|
|
PulseIntelligenceExternalAgentApprovedExecutionLoop30d: true,
|
|
PulseIntelligenceExternalAgentApprovedActionSuccessLoop30d: true,
|
|
PulseIntelligenceExternalAgentResolvedOperationsLoop30d: true,
|
|
PulseIntelligenceMCPAdapterOperationsLoop30d: true,
|
|
PulseIntelligenceMCPAdapterApprovedExecutionLoop30d: true,
|
|
PulseIntelligenceMCPAdapterApprovedActionSuccessLoop30d: true,
|
|
PulseIntelligenceMCPAdapterResolvedOperationsLoop30d: true,
|
|
PulseIntelligenceOperationsLoopStarterRequests30d: 5,
|
|
PulseIntelligenceAssistantOperationsLoopStarterRequests30d: 2,
|
|
PulseIntelligencePatrolOperationsLoopStarterRequests30d: 1,
|
|
PulseIntelligencePatrolControlOperationsLoopStarterRequests30d: 3,
|
|
PulseIntelligenceProActivationOperationsLoopStarterRequests30d: 1,
|
|
PulseIntelligenceMCPOperationsLoopStarterRequests30d: 1,
|
|
PulseIntelligenceAssistantAICalls30d: 7,
|
|
PulseIntelligenceAssistantContextAICalls30d: 4,
|
|
PulseIntelligenceAssistantToolCalls30d: 9,
|
|
PulseIntelligencePatrolAICalls30d: 3,
|
|
PulseIntelligencePatrolRuns30d: 4,
|
|
PulseIntelligencePatrolNewFindings30d: 5,
|
|
PulseIntelligencePatrolInvestigations30d: 6,
|
|
PulseIntelligencePatrolResolvedFindings30d: 3,
|
|
PulseIntelligencePatrolBlockedCause: "provider_not_configured",
|
|
PulseIntelligencePatrolAutonomyLevel: "approval",
|
|
PulseIntelligencePatrolInputTokensBucket30d: "5m_20m",
|
|
PulseIntelligencePatrolOutputTokensBucket30d: "100k_500k",
|
|
PulseIntelligencePatrolInvestigationOutcomeCannotFix30d: 4,
|
|
AIProviderClass: "local",
|
|
PulseIntelligenceExternalAgentEnabled: true,
|
|
PulseIntelligenceExternalAgentUsed30d: true,
|
|
PulseIntelligenceMCPAdapterUsed30d: true,
|
|
PulseIntelligenceExternalAgentContextRequests30d: 8,
|
|
PulseIntelligenceExternalAgentEventStreamRequests30d: 2,
|
|
PulseIntelligenceExternalAgentProvisioningRequests30d: 1,
|
|
PulseIntelligenceExternalAgentOperatorStateRequests30d: 3,
|
|
PulseIntelligenceExternalAgentFindingRequests30d: 5,
|
|
PulseIntelligenceExternalAgentActionRequests30d: 4,
|
|
PulseIntelligenceActionPlans30d: 6,
|
|
PulseIntelligenceApprovalRequests30d: 2,
|
|
PulseIntelligenceRejectedActionDecisions30d: 1,
|
|
PulseIntelligenceApprovedActionDecisions30d: 1,
|
|
PulseIntelligenceApprovedActionAttempts30d: 1,
|
|
PulseIntelligenceApprovedActionSuccesses30d: 1,
|
|
PulseIntelligenceApprovedActionInFlight30d: 2,
|
|
PulseIntelligenceApprovedActionUnclassified30d: 3,
|
|
PulseIntelligenceApprovedActionRefusalsPlanStale30d: 4,
|
|
PulseIntelligenceApprovedActionRefusalsPolicy30d: 5,
|
|
PulseIntelligenceApprovedActionRefusalsCapability30d: 6,
|
|
PulseIntelligenceApprovedActionRefusalsTargetChanged30d: 7,
|
|
PulseIntelligenceApprovedActionRefusalsPrerequisite30d: 8,
|
|
PulseIntelligenceApprovedActionRefusalsContract30d: 9,
|
|
PulseIntelligenceApprovedActionRefusalsOther30d: 10,
|
|
PulseIntelligenceVerifiedFindingResolutions30d: 8,
|
|
}
|
|
}
|
|
|
|
ping := applySnapshot(base, snap)
|
|
|
|
if ping.InstallID != "test-id" {
|
|
t.Fatal("base fields should be preserved")
|
|
}
|
|
if ping.PVENodes != 3 {
|
|
t.Fatalf("PVENodes = %d, want 3", ping.PVENodes)
|
|
}
|
|
if ping.VMs != 10 {
|
|
t.Fatalf("VMs = %d, want 10", ping.VMs)
|
|
}
|
|
if ping.AgentHosts != 2 || ping.DockerContainers != 12 || ping.KubernetesPods != 18 {
|
|
t.Fatalf("expanded workload counts not applied: %#v", ping)
|
|
}
|
|
if ping.StoragePools != 4 || ping.PhysicalDisks != 9 {
|
|
t.Fatalf("expanded storage counts not applied: %#v", ping)
|
|
}
|
|
if ping.TrueNASSystems != 1 || ping.TrueNASApps != 3 || ping.VMwareHosts != 2 || ping.AvailabilityTargets != 6 {
|
|
t.Fatalf("expanded platform counts not applied: %#v", ping)
|
|
}
|
|
if !ping.AIEnabled {
|
|
t.Fatal("AIEnabled should be true")
|
|
}
|
|
if !ping.PatrolEnabled || !ping.DiscoveryEnabled || !ping.NotificationsEnabled || !ping.AIActionsEnabled {
|
|
t.Fatalf("expanded feature flags not applied: %#v", ping)
|
|
}
|
|
if !ping.AlertAIEnabled {
|
|
t.Fatal("AlertAIEnabled should be true")
|
|
}
|
|
if !ping.PaidLicense {
|
|
t.Fatal("PaidLicense should be true")
|
|
}
|
|
if !ping.HasAPITokens {
|
|
t.Fatal("HasAPITokens should be true")
|
|
}
|
|
if ping.RBACCustomRoles != 3 || ping.RBACUserAssignments != 7 {
|
|
t.Fatalf("RBAC adoption counts not applied: %#v", ping)
|
|
}
|
|
if ping.AuditReads30d != 41 {
|
|
t.Fatalf("audit read count not applied: %#v", ping)
|
|
}
|
|
if ping.ReportSchedules != 5 || ping.ReportSchedulesEnabled != 4 || ping.ReportSchedulesRun30d != 2 {
|
|
t.Fatalf("report schedule adoption counts not applied: %#v", ping)
|
|
}
|
|
if ping.AgentProfiles != 9 {
|
|
t.Fatalf("agent profile count not applied: %#v", ping)
|
|
}
|
|
if ping.UpdateAttempts30d != 4 || ping.UpdateSuccesses30d != 2 || ping.UpdateFailures30d != 1 ||
|
|
ping.UpdateLastFailureCategory != "download" {
|
|
t.Fatalf("update telemetry counters not applied: %#v", ping)
|
|
}
|
|
if ping.ActiveAlertsCritical != 2 || ping.ActiveAlertsAge1h24h != 2 ||
|
|
ping.AlertsFiredCritical30d != 5 || ping.AlertsResolvedCritical30d != 3 ||
|
|
ping.AlertsResolution1h24h30d != 3 || ping.AlertsRepeatOccurrences30d != 2 ||
|
|
ping.AlertsSnoozedOccurrences30d != 2 || ping.AlertsResolvedWhileSnoozed30d != 1 ||
|
|
ping.AlertManagerTenants != 2 || ping.AlertDeliveryActiveTenants != 1 ||
|
|
ping.AlertFlappingEnabledTenants != 2 || ping.AlertIntentPolicyConfiguredTenants != 1 ||
|
|
ping.AlertEventHistoryAuthoritativeTenants != 2 || ping.AlertActiveStateAuthoritativeTenants != 2 {
|
|
t.Fatalf("alert quality snapshot not applied: %#v", ping)
|
|
}
|
|
if !ping.PulseIntelligenceLoopConfigured || !ping.PulseIntelligenceLoopActive30d ||
|
|
!ping.PulseIntelligenceCompleteOperationsLoop30d ||
|
|
!ping.PulseIntelligenceApprovedExecutionLoop30d ||
|
|
!ping.PulseIntelligenceResolvedOperationsLoop30d ||
|
|
!ping.PulseIntelligencePatrolControlCompletedOperationsLoop30d ||
|
|
!ping.PulseIntelligencePatrolControlResolvedOperationsLoop30d ||
|
|
!ping.PulseIntelligencePatrolControlPaidCompletedOperationsLoop30d ||
|
|
!ping.PulseIntelligencePatrolControlPaidResolvedOperationsLoop30d ||
|
|
!ping.PulseIntelligenceProActivationCompletedOperationsLoop30d ||
|
|
!ping.PulseIntelligenceProActivationResolvedOperationsLoop30d ||
|
|
!ping.PulseIntelligenceProActivationPaidCompletedOperationsLoop30d ||
|
|
!ping.PulseIntelligenceProActivationPaidResolvedOperationsLoop30d ||
|
|
!ping.PulseIntelligenceGovernedActionActive30d ||
|
|
!ping.PulseIntelligenceAssistantOperationsLoop30d ||
|
|
!ping.PulseIntelligenceAssistantApprovedExecutionLoop30d ||
|
|
!ping.PulseIntelligenceAssistantApprovedActionSuccessLoop30d ||
|
|
!ping.PulseIntelligenceAssistantResolvedOperationsLoop30d ||
|
|
!ping.PulseIntelligenceExternalAgentOperationsLoop30d ||
|
|
!ping.PulseIntelligenceExternalAgentApprovedExecutionLoop30d ||
|
|
!ping.PulseIntelligenceExternalAgentApprovedActionSuccessLoop30d ||
|
|
!ping.PulseIntelligenceExternalAgentResolvedOperationsLoop30d ||
|
|
!ping.PulseIntelligenceMCPAdapterOperationsLoop30d ||
|
|
!ping.PulseIntelligenceMCPAdapterApprovedExecutionLoop30d ||
|
|
!ping.PulseIntelligenceMCPAdapterApprovedActionSuccessLoop30d ||
|
|
!ping.PulseIntelligenceMCPAdapterResolvedOperationsLoop30d {
|
|
t.Fatalf("Pulse Intelligence adoption state not applied: %#v", ping)
|
|
}
|
|
if ping.PulseIntelligenceAssistantAICalls30d != 7 ||
|
|
ping.PulseIntelligenceOperationsLoopStarterRequests30d != 5 ||
|
|
ping.PulseIntelligenceAssistantOperationsLoopStarterRequests30d != 2 ||
|
|
ping.PulseIntelligencePatrolOperationsLoopStarterRequests30d != 1 ||
|
|
ping.PulseIntelligencePatrolControlOperationsLoopStarterRequests30d != 3 ||
|
|
ping.PulseIntelligenceProActivationOperationsLoopStarterRequests30d != 1 ||
|
|
ping.PulseIntelligenceMCPOperationsLoopStarterRequests30d != 1 ||
|
|
ping.PulseIntelligenceAssistantContextAICalls30d != 4 ||
|
|
ping.PulseIntelligenceAssistantToolCalls30d != 9 ||
|
|
ping.PulseIntelligencePatrolAICalls30d != 3 ||
|
|
ping.PulseIntelligencePatrolRuns30d != 4 ||
|
|
ping.PulseIntelligencePatrolNewFindings30d != 5 ||
|
|
ping.PulseIntelligencePatrolInvestigations30d != 6 ||
|
|
ping.PulseIntelligencePatrolResolvedFindings30d != 3 ||
|
|
ping.PulseIntelligencePatrolBlockedCause != "provider_not_configured" ||
|
|
ping.PulseIntelligencePatrolAutonomyLevel != "approval" ||
|
|
ping.PulseIntelligencePatrolInputTokensBucket30d != "5m_20m" ||
|
|
ping.PulseIntelligencePatrolOutputTokensBucket30d != "100k_500k" ||
|
|
ping.PulseIntelligencePatrolInvestigationOutcomeCannotFix30d != 4 ||
|
|
ping.AIProviderClass != "local" ||
|
|
ping.PulseIntelligenceActionPlans30d != 6 ||
|
|
ping.PulseIntelligenceApprovalRequests30d != 2 ||
|
|
ping.PulseIntelligenceRejectedActionDecisions30d != 1 ||
|
|
ping.PulseIntelligenceApprovedActionDecisions30d != 1 ||
|
|
ping.PulseIntelligenceApprovedActionAttempts30d != 1 ||
|
|
ping.PulseIntelligenceApprovedActionSuccesses30d != 1 ||
|
|
ping.PulseIntelligenceApprovedActionInFlight30d != 2 ||
|
|
ping.PulseIntelligenceApprovedActionUnclassified30d != 3 ||
|
|
ping.PulseIntelligenceApprovedActionRefusalsPlanStale30d != 4 ||
|
|
ping.PulseIntelligenceApprovedActionRefusalsPolicy30d != 5 ||
|
|
ping.PulseIntelligenceApprovedActionRefusalsCapability30d != 6 ||
|
|
ping.PulseIntelligenceApprovedActionRefusalsTargetChanged30d != 7 ||
|
|
ping.PulseIntelligenceApprovedActionRefusalsPrerequisite30d != 8 ||
|
|
ping.PulseIntelligenceApprovedActionRefusalsContract30d != 9 ||
|
|
ping.PulseIntelligenceApprovedActionRefusalsOther30d != 10 ||
|
|
ping.PulseIntelligenceVerifiedFindingResolutions30d != 8 {
|
|
t.Fatalf("Pulse Intelligence counters not applied: %#v", ping)
|
|
}
|
|
if !ping.PulseIntelligenceExternalAgentEnabled || !ping.PulseIntelligenceExternalAgentUsed30d ||
|
|
!ping.PulseIntelligenceMCPAdapterUsed30d {
|
|
t.Fatalf("Pulse Intelligence external-agent booleans not applied: %#v", ping)
|
|
}
|
|
if ping.PulseIntelligenceExternalAgentContextRequests30d != 8 ||
|
|
ping.PulseIntelligenceExternalAgentEventStreamRequests30d != 2 ||
|
|
ping.PulseIntelligenceExternalAgentProvisioningRequests30d != 1 ||
|
|
ping.PulseIntelligenceExternalAgentOperatorStateRequests30d != 3 ||
|
|
ping.PulseIntelligenceExternalAgentFindingRequests30d != 5 ||
|
|
ping.PulseIntelligenceExternalAgentActionRequests30d != 4 {
|
|
t.Fatalf("Pulse Intelligence external-agent class counters not applied: %#v", ping)
|
|
}
|
|
}
|
|
|
|
func TestApplyUpdateTelemetrySnapshotSummarizesHistory(t *testing.T) {
|
|
history, err := updates.NewUpdateHistory(t.TempDir())
|
|
if err != nil {
|
|
t.Fatalf("NewUpdateHistory: %v", err)
|
|
}
|
|
now := time.Date(2026, 6, 28, 14, 0, 0, 0, time.UTC)
|
|
ctx := context.Background()
|
|
entries := []updates.UpdateHistoryEntry{
|
|
{
|
|
EventID: "old-success",
|
|
Timestamp: now.Add(-(installIDRotationWindow + time.Hour)),
|
|
Action: "update",
|
|
Status: updates.StatusSuccess,
|
|
},
|
|
{
|
|
EventID: "recent-success",
|
|
Timestamp: now.Add(-2 * time.Hour),
|
|
Action: "update",
|
|
Status: updates.StatusSuccess,
|
|
},
|
|
{
|
|
EventID: "recent-failed-download",
|
|
Timestamp: now.Add(-time.Hour),
|
|
Action: "update",
|
|
Status: updates.StatusFailed,
|
|
Error: &updates.UpdateError{
|
|
Message: "failed to download update: upstream timed out",
|
|
Details: "raw details stay local",
|
|
},
|
|
},
|
|
{
|
|
EventID: "recent-check",
|
|
Timestamp: now.Add(-30 * time.Minute),
|
|
Action: "check",
|
|
Status: updates.StatusSuccess,
|
|
},
|
|
}
|
|
for _, entry := range entries {
|
|
if _, err := history.CreateEntry(ctx, entry); err != nil {
|
|
t.Fatalf("CreateEntry(%s): %v", entry.EventID, err)
|
|
}
|
|
}
|
|
|
|
var snap Snapshot
|
|
ApplyUpdateTelemetrySnapshot(&snap, history, now)
|
|
|
|
if snap.UpdateAttempts30d != 2 {
|
|
t.Fatalf("UpdateAttempts30d = %d, want 2", snap.UpdateAttempts30d)
|
|
}
|
|
if snap.UpdateSuccesses30d != 1 {
|
|
t.Fatalf("UpdateSuccesses30d = %d, want 1", snap.UpdateSuccesses30d)
|
|
}
|
|
if snap.UpdateFailures30d != 1 {
|
|
t.Fatalf("UpdateFailures30d = %d, want 1", snap.UpdateFailures30d)
|
|
}
|
|
if snap.UpdateLastFailureCategory != "download" {
|
|
t.Fatalf("UpdateLastFailureCategory = %q, want download", snap.UpdateLastFailureCategory)
|
|
}
|
|
}
|
|
|
|
func TestApplyUpdateTelemetrySnapshotDoesNotExposeRawFailureText(t *testing.T) {
|
|
history, err := updates.NewUpdateHistory(t.TempDir())
|
|
if err != nil {
|
|
t.Fatalf("NewUpdateHistory: %v", err)
|
|
}
|
|
now := time.Date(2026, 6, 28, 14, 0, 0, 0, time.UTC)
|
|
if _, err := history.CreateEntry(context.Background(), updates.UpdateHistoryEntry{
|
|
EventID: "recent-sensitive-failure",
|
|
Timestamp: now.Add(-time.Minute),
|
|
Action: "update",
|
|
Status: updates.StatusFailed,
|
|
Error: &updates.UpdateError{
|
|
Code: "UPSTREAM_503",
|
|
Message: "failed for https://updates.example.test/private/build?token=secret",
|
|
Details: "/home/alice/pulse/update.log contained local command output",
|
|
},
|
|
}); err != nil {
|
|
t.Fatalf("CreateEntry: %v", err)
|
|
}
|
|
|
|
var snap Snapshot
|
|
ApplyUpdateTelemetrySnapshot(&snap, history, now)
|
|
|
|
if snap.UpdateLastFailureCategory != "unknown" {
|
|
t.Fatalf("UpdateLastFailureCategory = %q, want unknown", snap.UpdateLastFailureCategory)
|
|
}
|
|
for _, disallowed := range []string{"https://", "updates.example", "/home/alice", "token=", "command output"} {
|
|
if strings.Contains(snap.UpdateLastFailureCategory, disallowed) {
|
|
t.Fatalf("UpdateLastFailureCategory leaked raw failure text %q in %q", disallowed, snap.UpdateLastFailureCategory)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAllTelemetryFieldsAreDisclosed(t *testing.T) {
|
|
pingType := reflect.TypeOf(Ping{})
|
|
fieldLabels := make([]string, 0)
|
|
for i := 0; i < pingType.NumField(); i++ {
|
|
jsonName := strings.Split(pingType.Field(i).Tag.Get("json"), ",")[0]
|
|
fieldLabels = append(fieldLabels, normalizedTelemetryDisclosureLabel(jsonName))
|
|
}
|
|
if len(fieldLabels) == 0 {
|
|
t.Fatal("expected telemetry fields on Ping")
|
|
}
|
|
|
|
for _, relativePath := range []string{
|
|
filepath.Join("..", "..", "docs", "PRIVACY.md"),
|
|
filepath.Join("..", "..", "frontend-modern", "public", "docs", "PRIVACY.md"),
|
|
} {
|
|
raw, err := os.ReadFile(relativePath)
|
|
if err != nil {
|
|
t.Fatalf("read %s: %v", relativePath, err)
|
|
}
|
|
doc := normalizedTelemetryDisclosureTableText(string(raw))
|
|
schemaDisclosure := fmt.Sprintf("schema version %d", TelemetrySchemaVersion)
|
|
if !strings.Contains(doc, schemaDisclosure) {
|
|
t.Errorf("%s must identify the current telemetry contract as %q", relativePath, schemaDisclosure)
|
|
}
|
|
for _, label := range fieldLabels {
|
|
if !strings.Contains(doc, label) {
|
|
t.Errorf("%s must disclose telemetry field %q", relativePath, label)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTelemetryPrivacyDocsDisclosePseudonymousIdentityAndIPHandling(t *testing.T) {
|
|
anonymousTelemetryClaimPattern := regexp.MustCompile(`(?i)\banonymous\b[^\n]{0,120}\btelemetry\b|\btelemetry\b[^\n]{0,120}\banonymous\b`)
|
|
for _, relativePath := range []string{
|
|
filepath.Join("..", "..", "docs", "PRIVACY.md"),
|
|
filepath.Join("..", "..", "frontend-modern", "public", "docs", "PRIVACY.md"),
|
|
} {
|
|
raw, err := os.ReadFile(relativePath)
|
|
if err != nil {
|
|
t.Fatalf("read %s: %v", relativePath, err)
|
|
}
|
|
content := string(raw)
|
|
normalized := normalizedTelemetryDisclosureText(content)
|
|
|
|
for _, required := range []string{
|
|
"outbound usage telemetry",
|
|
"enabled by default",
|
|
"rotating pseudonymous install ID",
|
|
"Pulse does not send raw browser events or an event-level clickstream",
|
|
"Lifecycle and outcome signals are deliberately limited to closed buckets, booleans, and aggregate counts",
|
|
"PULSE_TELEMETRY=false",
|
|
"The license server uses request IP addresses transiently for abuse/rate limiting",
|
|
} {
|
|
if !strings.Contains(content, required) {
|
|
t.Errorf("%s must disclose %q", relativePath, required)
|
|
}
|
|
}
|
|
if !strings.Contains(normalized, "does not store ip addresses in telemetry rows") {
|
|
t.Errorf("%s must disclose that telemetry rows do not store IP addresses", relativePath)
|
|
}
|
|
if anonymousTelemetryClaimPattern.MatchString(content) {
|
|
t.Errorf("%s must not describe outbound usage telemetry as anonymous", relativePath)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTelemetryPrivacyDocsKeepObserverProposalContentLocal(t *testing.T) {
|
|
for _, relativePath := range []string{
|
|
filepath.Join("..", "..", "docs", "PRIVACY.md"),
|
|
filepath.Join("..", "..", "frontend-modern", "public", "docs", "PRIVACY.md"),
|
|
} {
|
|
raw, err := os.ReadFile(relativePath)
|
|
if err != nil {
|
|
t.Fatalf("read %s: %v", relativePath, err)
|
|
}
|
|
content := string(raw)
|
|
for _, required := range []string{
|
|
"model-authored observer proposal",
|
|
"encrypts that artifact with the retained objective",
|
|
"excludes it from public objective reads and later prompt seeds",
|
|
"does not include its content in usage telemetry or audit messages",
|
|
} {
|
|
if !strings.Contains(content, required) {
|
|
t.Errorf("%s must disclose %q", relativePath, required)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRepositoryDoesNotClaimTelemetryIsAnonymous(t *testing.T) {
|
|
repoRoot := filepath.Clean(filepath.Join("..", ".."))
|
|
anonymousTelemetryClaimPattern := regexp.MustCompile(`(?i)\banonymous\b[^\n]{0,120}\btelemetry\b|\btelemetry\b[^\n]{0,120}\banonymous\b`)
|
|
scannableExtensions := map[string]bool{
|
|
".go": true,
|
|
".html": true,
|
|
".json": true,
|
|
".md": true,
|
|
".py": true,
|
|
".sh": true,
|
|
".ts": true,
|
|
".tsx": true,
|
|
".yaml": true,
|
|
".yml": true,
|
|
}
|
|
|
|
err := filepath.WalkDir(repoRoot, func(path string, entry fs.DirEntry, walkErr error) error {
|
|
if walkErr != nil {
|
|
return walkErr
|
|
}
|
|
if entry.IsDir() {
|
|
switch entry.Name() {
|
|
case ".claude", ".git", "build", "dist", "node_modules", "tmp", "vendor":
|
|
return fs.SkipDir
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
relativePath, err := filepath.Rel(repoRoot, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
normalizedPath := filepath.ToSlash(relativePath)
|
|
baseName := entry.Name()
|
|
if normalizedPath == "docs/release-control/v6/internal/status.json" ||
|
|
strings.Contains(normalizedPath, "/__tests__/") ||
|
|
strings.Contains(normalizedPath, "/tests/") ||
|
|
strings.HasSuffix(baseName, "_test.go") ||
|
|
strings.HasSuffix(baseName, "_test.py") ||
|
|
strings.Contains(baseName, ".test.") {
|
|
return nil
|
|
}
|
|
if !scannableExtensions[strings.ToLower(filepath.Ext(baseName))] {
|
|
return nil
|
|
}
|
|
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if match := anonymousTelemetryClaimPattern.Find(raw); match != nil {
|
|
t.Errorf("%s must describe telemetry as pseudonymous, not %q", normalizedPath, match)
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("scan repository telemetry language: %v", err)
|
|
}
|
|
}
|
|
|
|
func normalizedTelemetryDisclosureTableText(value string) string {
|
|
tableLines := make([]string, 0)
|
|
for _, line := range strings.Split(value, "\n") {
|
|
if strings.HasPrefix(strings.TrimSpace(line), "|") {
|
|
tableLines = append(tableLines, line)
|
|
}
|
|
}
|
|
return normalizedTelemetryDisclosureText(strings.Join(tableLines, "\n"))
|
|
}
|
|
|
|
func normalizedTelemetryDisclosureLabel(jsonName string) string {
|
|
if strings.HasPrefix(jsonName, "pulse_intelligence_") {
|
|
label := strings.TrimPrefix(jsonName, "pulse_intelligence_")
|
|
return normalizedTelemetryDisclosureText("Pulse Intelligence " + strings.ReplaceAll(label, "_", " "))
|
|
}
|
|
return normalizedTelemetryDisclosureText(strings.ReplaceAll(jsonName, "_", " "))
|
|
}
|
|
|
|
func normalizedTelemetryDisclosureText(value string) string {
|
|
value = strings.ToLower(value)
|
|
replacer := strings.NewReplacer(
|
|
"`", " ",
|
|
"|", " ",
|
|
"_", " ",
|
|
"-", " ",
|
|
"/", " ",
|
|
",", " ",
|
|
".", " ",
|
|
":", " ",
|
|
";", " ",
|
|
"*", " ",
|
|
"(", " ",
|
|
")", " ",
|
|
"[", " ",
|
|
"]", " ",
|
|
)
|
|
value = replacer.Replace(value)
|
|
return strings.Join(strings.Fields(value), " ")
|
|
}
|
|
|
|
func TestApplySnapshot_NilFunc(t *testing.T) {
|
|
base := Ping{InstallID: "test-id", Version: "6.0.0"}
|
|
ping := applySnapshot(base, nil)
|
|
if ping.InstallID != "test-id" {
|
|
t.Fatal("should return base when func is nil")
|
|
}
|
|
if ping.PVENodes != 0 {
|
|
t.Fatal("snapshot fields should be zero when func is nil")
|
|
}
|
|
}
|
|
|
|
func TestBuildPreview_UsesCurrentHeartbeatPayload(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
preview, err := BuildPreview(Config{
|
|
Version: "v6.0.0-rc.1-45-gABCDEF",
|
|
DataDir: dir,
|
|
IsDocker: true,
|
|
GetSnapshot: func() Snapshot {
|
|
return Snapshot{
|
|
PVENodes: 3,
|
|
VMs: 10,
|
|
ActiveAlerts: 2,
|
|
AIEnabled: true,
|
|
NotificationFailures7d: 3,
|
|
NotificationFailuresAuthentication7d: 2,
|
|
NotificationFailuresServerError7d: 1,
|
|
AlertsRepeatOccurrences30d: 4,
|
|
AlertManagerTenants: 2,
|
|
}
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("BuildPreview: %v", err)
|
|
}
|
|
|
|
if preview.Event != "heartbeat" {
|
|
t.Fatalf("preview event = %q, want heartbeat", preview.Event)
|
|
}
|
|
if preview.Platform != "docker" {
|
|
t.Fatalf("preview platform = %q, want docker", preview.Platform)
|
|
}
|
|
if preview.Version != "6.0.0-rc.1+git.45.gabcdef" {
|
|
t.Fatalf("preview version = %q, want normalized development version", preview.Version)
|
|
}
|
|
if preview.VersionRaw != "v6.0.0-rc.1-45-gABCDEF" {
|
|
t.Fatalf("preview raw version = %q, want original version string", preview.VersionRaw)
|
|
}
|
|
if preview.VersionChannel != "dev" {
|
|
t.Fatalf("preview version channel = %q, want dev", preview.VersionChannel)
|
|
}
|
|
if preview.VersionBuild != "git.45.gabcdef" {
|
|
t.Fatalf("preview version build = %q, want git.45.gabcdef", preview.VersionBuild)
|
|
}
|
|
if !preview.VersionDevelopment {
|
|
t.Fatal("expected preview to mark development build")
|
|
}
|
|
if preview.VersionPublished {
|
|
t.Fatal("development preview must not be marked as published release")
|
|
}
|
|
if preview.PVENodes != 3 || preview.VMs != 10 || preview.ActiveAlerts != 2 {
|
|
t.Fatalf("preview snapshot = %#v", preview)
|
|
}
|
|
if preview.NotificationFailures7d != 3 ||
|
|
preview.NotificationFailuresAuthentication7d != 2 ||
|
|
preview.NotificationFailuresServerError7d != 1 {
|
|
t.Fatalf("preview notification failure classes = %#v", preview)
|
|
}
|
|
if preview.AlertsRepeatOccurrences30d != 4 || preview.AlertManagerTenants != 2 {
|
|
t.Fatalf("preview alert-quality fields = %#v", preview)
|
|
}
|
|
if preview.InstallID == "" {
|
|
t.Fatal("expected preview install ID")
|
|
}
|
|
if preview.SchemaVersion != TelemetrySchemaVersion || preview.SentAt == "" {
|
|
t.Fatalf("preview schema identity = version %d sent_at %q", preview.SchemaVersion, preview.SentAt)
|
|
}
|
|
if preview.DeploymentMethod != "container_other" {
|
|
t.Fatalf("preview deployment method = %q, want container_other", preview.DeploymentMethod)
|
|
}
|
|
if preview.ActivationStage != "outcome_observed" || !preview.MonitoringActive {
|
|
t.Fatalf("preview lifecycle = stage %q active %v, want outcome_observed/true", preview.ActivationStage, preview.MonitoringActive)
|
|
}
|
|
if preview.EstateSizeBucket != "11_50" {
|
|
t.Fatalf("preview estate bucket = %q, want 11_50", preview.EstateSizeBucket)
|
|
}
|
|
if preview.TimeToFirstMonitoredResourceBucket != "present_at_first_observation" {
|
|
t.Fatalf("preview time-to-monitoring bucket = %q, want present_at_first_observation", preview.TimeToFirstMonitoredResourceBucket)
|
|
}
|
|
|
|
record := decodeInstallIDRecordFile(t, filepath.Join(dir, installIDFile))
|
|
if record.InstallID != preview.InstallID {
|
|
t.Fatalf("persisted install ID = %q, want %q", record.InstallID, preview.InstallID)
|
|
}
|
|
}
|
|
|
|
func TestAlertQualityPayloadContainsOnlyAggregateFields(t *testing.T) {
|
|
payload, err := json.Marshal(Ping{
|
|
AlertsRepeatOccurrences30d: 7,
|
|
AlertsResolvedWhileSnoozed30d: 5,
|
|
AlertIntentPolicyConfiguredTenants: 3,
|
|
AlertActiveStatePersistenceDegradedTenants: 1,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("marshal ping: %v", err)
|
|
}
|
|
text := strings.ToLower(string(payload))
|
|
for _, forbidden := range []string{
|
|
"resource_id", "hostname", "alert_text", "alert_message", "rule_content",
|
|
"destination", "resolved_at", "start_time", "error_text", "click",
|
|
} {
|
|
if strings.Contains(text, forbidden) {
|
|
t.Fatalf("alert-quality payload contains forbidden key fragment %q: %s", forbidden, text)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBuildPingAt_RotatesIdentifierDuringLongRunningSession(t *testing.T) {
|
|
dir := t.TempDir()
|
|
start := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC)
|
|
cfg := Config{DataDir: dir}
|
|
|
|
first, err := buildPingAt(cfg, "startup", start)
|
|
if err != nil {
|
|
t.Fatalf("build first ping: %v", err)
|
|
}
|
|
second, err := buildPingAt(cfg, "heartbeat", start.Add(installIDRotationWindow+time.Hour))
|
|
if err != nil {
|
|
t.Fatalf("build second ping: %v", err)
|
|
}
|
|
if first.InstallID == second.InstallID {
|
|
t.Fatalf("expected per-event install ID rotation, got %q twice", first.InstallID)
|
|
}
|
|
}
|
|
|
|
func TestDeploymentMethodRejectsFreeFormValues(t *testing.T) {
|
|
t.Setenv("PULSE_DEPLOYMENT_METHOD", "/home/alice/private-install")
|
|
if got := deploymentMethod(Config{IsDocker: true}); got != "container_other" {
|
|
t.Fatalf("docker invalid deployment value = %q, want container_other", got)
|
|
}
|
|
if got := deploymentMethod(Config{}); got != "binary_other" {
|
|
t.Fatalf("binary invalid deployment value = %q, want binary_other", got)
|
|
}
|
|
|
|
t.Setenv("PULSE_DEPLOYMENT_METHOD", "systemd")
|
|
if got := deploymentMethod(Config{}); got != "systemd" {
|
|
t.Fatalf("closed deployment value = %q, want systemd", got)
|
|
}
|
|
}
|
|
|
|
func TestBuildPingAt_PersistsOnlyCoarseLifecycleMilestones(t *testing.T) {
|
|
dir := t.TempDir()
|
|
start := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC)
|
|
snapshot := Snapshot{AuthConfigured: true}
|
|
cfg := Config{
|
|
DataDir: dir,
|
|
GetSnapshot: func() Snapshot {
|
|
return snapshot
|
|
},
|
|
}
|
|
|
|
first, err := buildPingAt(cfg, "heartbeat", start)
|
|
if err != nil {
|
|
t.Fatalf("build first ping: %v", err)
|
|
}
|
|
if first.ActivationStage != "secured" || first.TimeToFirstMonitoredResourceBucket != "not_observed" {
|
|
t.Fatalf("first lifecycle = %#v", first)
|
|
}
|
|
|
|
snapshot = Snapshot{
|
|
AuthConfigured: true,
|
|
ConfiguredConnections: 1,
|
|
PVENodes: 1,
|
|
VMs: 4,
|
|
AlertsFired30d: 1,
|
|
AlertsResolved30d: 1,
|
|
}
|
|
second, err := buildPingAt(cfg, "heartbeat", start.Add(2*time.Hour))
|
|
if err != nil {
|
|
t.Fatalf("build second ping: %v", err)
|
|
}
|
|
if second.ActivationStage != "outcome_observed" || !second.MonitoringActive || !second.OutcomeObserved30d {
|
|
t.Fatalf("second lifecycle = %#v", second)
|
|
}
|
|
if second.TimeToFirstMonitoredResourceBucket != "1_6h" {
|
|
t.Fatalf("time-to-monitoring bucket = %q, want 1_6h", second.TimeToFirstMonitoredResourceBucket)
|
|
}
|
|
|
|
snapshot = Snapshot{}
|
|
third, err := buildPingAt(cfg, "heartbeat", start.Add(8*24*time.Hour))
|
|
if err != nil {
|
|
t.Fatalf("build third ping: %v", err)
|
|
}
|
|
if third.ActivationStage != "outcome_observed" || third.MonitoringActive {
|
|
t.Fatalf("historical/current lifecycle split = stage %q active %v", third.ActivationStage, third.MonitoringActive)
|
|
}
|
|
if third.KnownInstallAgeBucket != "8_30d" {
|
|
t.Fatalf("known install age bucket = %q, want 8_30d", third.KnownInstallAgeBucket)
|
|
}
|
|
|
|
data, err := os.ReadFile(filepath.Join(dir, lifecycleStateFile))
|
|
if err != nil {
|
|
t.Fatalf("read lifecycle state: %v", err)
|
|
}
|
|
var stored map[string]any
|
|
if err := json.Unmarshal(data, &stored); err != nil {
|
|
t.Fatalf("decode lifecycle state: %v", err)
|
|
}
|
|
for key := range stored {
|
|
switch key {
|
|
case "first_observed_at", "first_monitored_resource_at", "highest_observed_activation":
|
|
default:
|
|
t.Fatalf("unexpected lifecycle state field %q", key)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSend_Success(t *testing.T) {
|
|
var received atomic.Int32
|
|
var lastPing Ping
|
|
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
body, _ := io.ReadAll(r.Body)
|
|
_ = json.Unmarshal(body, &lastPing)
|
|
received.Add(1)
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}))
|
|
defer ts.Close()
|
|
|
|
// Override the endpoint for testing.
|
|
origEndpoint := pingEndpoint
|
|
pingEndpoint = ts.URL
|
|
defer func() { pingEndpoint = origEndpoint }()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
ping := Ping{
|
|
InstallID: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
|
Version: "6.0.0-rc.1",
|
|
VersionRaw: "v6.0.0-rc.1",
|
|
VersionChannel: "rc",
|
|
VersionDevelopment: false,
|
|
VersionPublished: true,
|
|
Event: "startup",
|
|
Platform: "docker",
|
|
OS: "linux",
|
|
Arch: "amd64",
|
|
}
|
|
if err := send(ctx, ping); err != nil {
|
|
t.Fatalf("send: %v", err)
|
|
}
|
|
|
|
if received.Load() != 1 {
|
|
t.Fatalf("expected 1 request to reach server, got %d", received.Load())
|
|
}
|
|
if lastPing.InstallID != ping.InstallID {
|
|
t.Errorf("install_id = %q, want %q", lastPing.InstallID, ping.InstallID)
|
|
}
|
|
if lastPing.Event != "startup" {
|
|
t.Errorf("event = %q, want %q", lastPing.Event, "startup")
|
|
}
|
|
if lastPing.Version != "6.0.0-rc.1" {
|
|
t.Errorf("version = %q, want %q", lastPing.Version, "6.0.0-rc.1")
|
|
}
|
|
if lastPing.VersionRaw != "v6.0.0-rc.1" {
|
|
t.Errorf("version_raw = %q, want %q", lastPing.VersionRaw, "v6.0.0-rc.1")
|
|
}
|
|
if lastPing.VersionChannel != "rc" {
|
|
t.Errorf("version_channel = %q, want rc", lastPing.VersionChannel)
|
|
}
|
|
if !lastPing.VersionPublished {
|
|
t.Error("expected version_is_published_release to be true")
|
|
}
|
|
}
|
|
|
|
func TestSend_UsesReducedCommercialSignals(t *testing.T) {
|
|
var rawBody []byte
|
|
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
rawBody, _ = io.ReadAll(r.Body)
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}))
|
|
defer ts.Close()
|
|
|
|
origEndpoint := pingEndpoint
|
|
pingEndpoint = ts.URL
|
|
defer func() { pingEndpoint = origEndpoint }()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
if err := send(ctx, Ping{
|
|
InstallID: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
|
Version: "6.0.0",
|
|
Event: "heartbeat",
|
|
Platform: "binary",
|
|
OS: "linux",
|
|
Arch: "amd64",
|
|
PaidLicense: true,
|
|
HasAPITokens: true,
|
|
}); err != nil {
|
|
t.Fatalf("send: %v", err)
|
|
}
|
|
|
|
var payload map[string]any
|
|
if err := json.Unmarshal(rawBody, &payload); err != nil {
|
|
t.Fatalf("Unmarshal: %v", err)
|
|
}
|
|
if _, ok := payload["license_tier"]; ok {
|
|
t.Fatal("legacy license_tier field should not be sent")
|
|
}
|
|
if _, ok := payload["api_tokens"]; ok {
|
|
t.Fatal("legacy api_tokens field should not be sent")
|
|
}
|
|
if got, ok := payload["paid_license"].(bool); !ok || !got {
|
|
t.Fatalf("paid_license = %#v, want true", payload["paid_license"])
|
|
}
|
|
if got, ok := payload["has_api_tokens"].(bool); !ok || !got {
|
|
t.Fatalf("has_api_tokens = %#v, want true", payload["has_api_tokens"])
|
|
}
|
|
}
|
|
|
|
func TestSend_ReturnsNonSuccessStatus(t *testing.T) {
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
http.Error(w, "rejected", http.StatusUnprocessableEntity)
|
|
}))
|
|
defer ts.Close()
|
|
|
|
origEndpoint := pingEndpoint
|
|
pingEndpoint = ts.URL
|
|
defer func() { pingEndpoint = origEndpoint }()
|
|
|
|
err := send(context.Background(), Ping{InstallID: uuid.New().String()})
|
|
if err == nil || !strings.Contains(err.Error(), "HTTP 422") {
|
|
t.Fatalf("send error = %v, want HTTP 422", err)
|
|
}
|
|
}
|
|
|
|
func TestJitteredHeartbeat_WithinBounds(t *testing.T) {
|
|
min := heartbeatInterval - maxHeartbeatJitter
|
|
max := heartbeatInterval + maxHeartbeatJitter
|
|
|
|
for i := 0; i < 1000; i++ {
|
|
d := jitteredHeartbeat()
|
|
if d < min || d > max {
|
|
t.Fatalf("jitteredHeartbeat() = %v, want [%v, %v]", d, min, max)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestJitteredHeartbeat_NotConstant(t *testing.T) {
|
|
seen := make(map[time.Duration]bool)
|
|
for i := 0; i < 100; i++ {
|
|
seen[jitteredHeartbeat()] = true
|
|
}
|
|
if len(seen) < 2 {
|
|
t.Fatal("jitteredHeartbeat() returned the same value 100 times — jitter is not working")
|
|
}
|
|
}
|
|
|
|
func TestSendEvent_SuppressedWhileMockModeEnabled(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.StatusNoContent)
|
|
}))
|
|
defer ts.Close()
|
|
|
|
origEndpoint := pingEndpoint
|
|
pingEndpoint = ts.URL
|
|
defer func() { pingEndpoint = origEndpoint }()
|
|
|
|
testutil.SetMockMode(t, true)
|
|
|
|
cfg := Config{DataDir: t.TempDir()}
|
|
sendEvent(context.Background(), cfg, "startup")
|
|
sendEvent(context.Background(), cfg, "heartbeat")
|
|
|
|
if got := received.Load(); got != 0 {
|
|
t.Fatalf("expected no telemetry pings while mock mode is enabled, got %d", got)
|
|
}
|
|
}
|
|
|
|
func TestSendEvent_SendsWhenMockModeDisabled(t *testing.T) {
|
|
var received atomic.Int32
|
|
var lastPing Ping
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
body, _ := io.ReadAll(r.Body)
|
|
_ = json.Unmarshal(body, &lastPing)
|
|
received.Add(1)
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}))
|
|
defer ts.Close()
|
|
|
|
origEndpoint := pingEndpoint
|
|
pingEndpoint = ts.URL
|
|
defer func() { pingEndpoint = origEndpoint }()
|
|
|
|
testutil.SetMockMode(t, false)
|
|
|
|
sendEvent(context.Background(), Config{DataDir: t.TempDir()}, "heartbeat")
|
|
|
|
if got := received.Load(); got != 1 {
|
|
t.Fatalf("expected 1 telemetry ping with mock mode disabled, got %d", got)
|
|
}
|
|
if lastPing.Event != "heartbeat" {
|
|
t.Errorf("event = %q, want %q", lastPing.Event, "heartbeat")
|
|
}
|
|
}
|
|
|
|
func TestStartStop_DisabledByDefault(t *testing.T) {
|
|
// Start should be a no-op when Enabled is false (the default).
|
|
Start(context.Background(), Config{
|
|
Version: "6.0.0",
|
|
DataDir: t.TempDir(),
|
|
Enabled: false,
|
|
})
|
|
|
|
// Stop should also be safe when nothing was started.
|
|
Stop()
|
|
}
|
|
|
|
func decodeInstallIDRecordFile(t *testing.T, path string) installIDRecord {
|
|
t.Helper()
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("ReadFile(%s): %v", path, err)
|
|
}
|
|
return decodeInstallIDRecord(t, data)
|
|
}
|
|
|
|
func decodeInstallIDRecord(t *testing.T, data []byte) installIDRecord {
|
|
t.Helper()
|
|
var record installIDRecord
|
|
if err := json.Unmarshal(data, &record); err != nil {
|
|
t.Fatalf("Unmarshal install ID record: %v", err)
|
|
}
|
|
return record
|
|
}
|
|
|
|
// The node connection test counts are only useful if they leave the install, so
|
|
// assert the snapshot-to-ping mapping rather than trusting field order.
|
|
func TestBuildPingCarriesNodeTestCounts(t *testing.T) {
|
|
ping := BuildPingForSnapshot(Snapshot{
|
|
NodeTestAttempts30d: 9,
|
|
NodeTestFailures30d: 4,
|
|
})
|
|
if ping.NodeTestAttempts30d != 9 {
|
|
t.Fatalf("node_test_attempts_30d = %d, want 9", ping.NodeTestAttempts30d)
|
|
}
|
|
if ping.NodeTestFailures30d != 4 {
|
|
t.Fatalf("node_test_failures_30d = %d, want 4", ping.NodeTestFailures30d)
|
|
}
|
|
}
|
|
|
|
// TestTelemetryPrivacyDocsDiscloseSetupChoiceAndPayloadChanges pins the two
|
|
// disclosure surfaces that replaced the in-app payload-update banner: the
|
|
// first-run setup choice and the dated payload changelog. The changelog must
|
|
// carry a row for the current schema so a bump cannot land undisclosed.
|
|
func TestTelemetryPrivacyDocsDiscloseSetupChoiceAndPayloadChanges(t *testing.T) {
|
|
for _, relativePath := range []string{
|
|
filepath.Join("..", "..", "docs", "PRIVACY.md"),
|
|
filepath.Join("..", "..", "frontend-modern", "public", "docs", "PRIVACY.md"),
|
|
} {
|
|
raw, err := os.ReadFile(relativePath)
|
|
if err != nil {
|
|
t.Fatalf("read %s: %v", relativePath, err)
|
|
}
|
|
content := string(raw)
|
|
for _, required := range []string{
|
|
"During first-run setup, switch off **Usage statistics** on the admin-account step",
|
|
"The first startup ping is sent about two minutes after Pulse starts",
|
|
"#### Payload changes",
|
|
"Every change to the payload bumps the schema version, is listed here with its date, and appears in the release notes",
|
|
"An in-app notice is reserved for a change in kind",
|
|
"#### What it is not used for",
|
|
"It is not sold, licensed, or shared with anyone else",
|
|
"It is not linked to a Pulse account, license key, purchase, or email address",
|
|
fmt.Sprintf("| %d | 2026-", TelemetrySchemaVersion),
|
|
} {
|
|
if !strings.Contains(content, required) {
|
|
t.Errorf("%s must disclose %q", relativePath, required)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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. Regression guard for the 317 single-ping
|
|
// 0.0.0-test-version installs that pkg/server tests reported between
|
|
// 2026-08-29 and 2026-09-03.
|
|
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 two-minute 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)
|
|
}
|
|
}
|