refactor: Add testability improvements to core packages

hostagent/commands.go:
- Extract execCommandContext as mockable variable

hostagent/proxmox_setup.go:
- Convert stateFilePath constants to variables (testable)
- Extract runCommand and lookPath as mockable functions
- Add duplicate comment (minor cleanup needed)

notifications/notifications.go:
- Add GetQueueStats() method for interface compliance
- Used by NotificationMonitor interface

updates/manager.go:
- Add AddSSEClient, RemoveSSEClient, GetSSECachedStatus methods
- Enables interface-based SSE client management

pkg/audit/export.go:
- Minor testability improvements

go.mod/go.sum:
- Add stretchr/objx v0.5.2 (test mocking dependency)
This commit is contained in:
rcourtman 2026-01-19 19:25:38 +00:00
parent dc16c94766
commit d06ed2edb3
7 changed files with 65 additions and 11 deletions

1
go.mod
View file

@ -96,6 +96,7 @@ require (
github.com/russellhaering/goxmldsig v1.4.0 // indirect
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/tklauser/go-sysconf v0.3.16 // indirect
github.com/tklauser/numcpus v0.11.0 // indirect
github.com/x448/float16 v0.8.4 // indirect

2
go.sum
View file

@ -208,6 +208,8 @@ github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3A
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=

View file

@ -17,6 +17,8 @@ import (
"github.com/rs/zerolog"
)
var execCommandContext = exec.CommandContext
// CommandClient handles WebSocket connection to Pulse for AI command execution
type CommandClient struct {
pulseURL string
@ -413,9 +415,9 @@ func (c *CommandClient) executeCommand(ctx context.Context, payload executeComma
// Execute the command
var cmd *exec.Cmd
if runtime.GOOS == "windows" {
cmd = exec.CommandContext(cmdCtx, "cmd", "/C", command)
cmd = execCommandContext(cmdCtx, "cmd", "/C", command)
} else {
cmd = exec.CommandContext(cmdCtx, "sh", "-c", command)
cmd = execCommandContext(cmdCtx, "sh", "-c", command)
}
var stdout, stderr bytes.Buffer

View file

@ -43,8 +43,11 @@ const (
proxmoxUserPVE = "pulse-monitor@pam"
proxmoxUserPBS = "pulse-monitor@pbs"
proxmoxComment = "Pulse monitoring service"
stateFilePath = "/var/lib/pulse-agent/proxmox-registered" // Legacy, kept for backward compat
stateFileDir = "/var/lib/pulse-agent"
)
var (
stateFilePath = "/var/lib/pulse-agent/proxmox-registered" // Legacy, kept for backward compat
stateFileDir = "/var/lib/pulse-agent"
// Per-type state files for multi-product support (PVE+PBS on same host)
stateFilePVE = "/var/lib/pulse-agent/proxmox-pve-registered"
stateFilePBS = "/var/lib/pulse-agent/proxmox-pbs-registered"
@ -210,6 +213,9 @@ func (p *ProxmoxSetup) detectProxmoxType() string {
return ""
}
// detectProxmoxTypes checks for ALL Proxmox products on this system.
// Returns a slice of detected types (e.g., ["pve", "pbs"] if both are installed).
// This is common when PBS is installed directly on a PVE host.
// detectProxmoxTypes checks for ALL Proxmox products on this system.
// Returns a slice of detected types (e.g., ["pve", "pbs"] if both are installed).
// This is common when PBS is installed directly on a PVE host.
@ -217,12 +223,12 @@ func (p *ProxmoxSetup) detectProxmoxTypes() []string {
var types []string
// Check for PVE
if _, err := exec.LookPath("pvesh"); err == nil {
if _, err := lookPath("pvesh"); err == nil {
types = append(types, "pve")
}
// Check for PBS
if _, err := exec.LookPath("proxmox-backup-manager"); err == nil {
if _, err := lookPath("proxmox-backup-manager"); err == nil {
types = append(types, "pbs")
}
@ -707,14 +713,16 @@ func (p *ProxmoxSetup) markTypeAsRegistered(ptype string) {
}
// runCommand executes a command and returns any error.
func runCommand(ctx context.Context, name string, args ...string) error {
var runCommand = func(ctx context.Context, name string, args ...string) error {
cmd := exec.CommandContext(ctx, name, args...)
return cmd.Run()
}
// runCommandOutput executes a command and returns the output.
func runCommandOutput(ctx context.Context, name string, args ...string) (string, error) {
var runCommandOutput = func(ctx context.Context, name string, args ...string) (string, error) {
cmd := exec.CommandContext(ctx, name, args...)
output, err := cmd.CombinedOutput()
return string(output), err
}
// lookPath searches for an executable in the directories named by the PATH environment variable.
var lookPath = exec.LookPath

View file

@ -2585,6 +2585,18 @@ func buildNotificationTestAlert() *alerts.Alert {
}
}
// GetQueueStats returns statistics about the notification queue
func (n *NotificationManager) GetQueueStats() (map[string]int, error) {
n.mu.RLock()
queue := n.queue
n.mu.RUnlock()
if queue == nil {
return nil, fmt.Errorf("notification queue not initialized")
}
return queue.GetQueueStats()
}
// SendTestNotification sends a test notification
func (n *NotificationManager) SendTestNotification(method string) error {
testAlert := buildNotificationTestAlert()

View file

@ -142,6 +142,29 @@ func (m *Manager) GetQueue() *UpdateQueue {
return m.queue
}
// AddSSEClient adds a new SSE client for update progress streaming
func (m *Manager) AddSSEClient(w http.ResponseWriter, clientID string) *SSEClient {
if m.sseBroadcast == nil {
return nil
}
return m.sseBroadcast.AddClient(w, clientID)
}
// RemoveSSEClient removes an SSE client
func (m *Manager) RemoveSSEClient(clientID string) {
if m.sseBroadcast != nil {
m.sseBroadcast.RemoveClient(clientID)
}
}
// GetCachedStatus returns the last broadcasted status
func (m *Manager) GetSSECachedStatus() (UpdateStatus, time.Time) {
if m.sseBroadcast == nil {
return UpdateStatus{}, time.Time{}
}
return m.sseBroadcast.GetCachedStatus()
}
// CheckForUpdates checks GitHub for available updates using saved config channel
func (m *Manager) CheckForUpdates(ctx context.Context) (*UpdateInfo, error) {
return m.CheckForUpdatesWithChannel(ctx, "")

View file

@ -38,13 +38,19 @@ type ExportEvent struct {
SignatureValid *bool `json:"signature_valid,omitempty"`
}
// PersistentLogger defines the interface for loggers that support querying and verification.
type PersistentLogger interface {
Query(filter QueryFilter) ([]Event, error)
VerifySignature(event Event) bool
}
// Exporter provides export functionality for audit logs.
type Exporter struct {
logger *SQLiteLogger
logger PersistentLogger
}
// NewExporter creates a new exporter for the given logger.
func NewExporter(logger *SQLiteLogger) *Exporter {
func NewExporter(logger PersistentLogger) *Exporter {
return &Exporter{logger: logger}
}