mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-15 16:42:15 +00:00
1202 lines
41 KiB
Go
1202 lines
41 KiB
Go
package telemetry
|
|
|
|
import (
|
|
"encoding/json"
|
|
"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,
|
|
ActiveAlerts: 2,
|
|
PaidLicense: true,
|
|
HasAPITokens: true,
|
|
UpdateAttempts30d: 4,
|
|
UpdateSuccesses30d: 2,
|
|
UpdateFailures30d: 1,
|
|
UpdateLastFailureCategory: "download",
|
|
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,
|
|
PulseIntelligencePatrolAutofixes30d: 2,
|
|
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,
|
|
PulseIntelligenceApprovedActionRefusalsOther30d: 7,
|
|
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.PaidLicense {
|
|
t.Fatal("PaidLicense should be true")
|
|
}
|
|
if !ping.HasAPITokens {
|
|
t.Fatal("HasAPITokens should be true")
|
|
}
|
|
if ping.UpdateAttempts30d != 4 || ping.UpdateSuccesses30d != 2 || ping.UpdateFailures30d != 1 ||
|
|
ping.UpdateLastFailureCategory != "download" {
|
|
t.Fatalf("update telemetry counters 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.PulseIntelligencePatrolAutofixes30d != 2 ||
|
|
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.PulseIntelligenceApprovedActionRefusalsOther30d != 7 ||
|
|
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))
|
|
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 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 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,
|
|
NotificationFailuresConnectivity7d: 1,
|
|
}
|
|
},
|
|
})
|
|
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.NotificationFailuresConnectivity7d != 1 {
|
|
t.Fatalf("preview notification failure classes = %#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 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",
|
|
}
|
|
send(ctx, ping)
|
|
|
|
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()
|
|
|
|
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,
|
|
})
|
|
|
|
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
|
|
}
|