test: Add comprehensive tests for Host Agent threshold normalization with Trigger=0. Related to #864

This commit is contained in:
rcourtman 2025-12-20 20:32:59 +00:00
parent 19c7cf6970
commit 81cb333997
3 changed files with 166 additions and 5 deletions

View file

@ -1097,6 +1097,7 @@ You have access to tools to execute commands on the target system. You should:
2. Actually execute the commands - don't just explain what commands to run
3. For Proxmox operations (resize disk, manage containers/VMs), run commands on the HOST (target_type=host)
4. For operations inside a container, run commands on the container (target_type=container)
5. **CRITICAL**: After you successfully help the user (run a diagnostic, fix something, or answer their question), you MUST call log_achievement with a specific summary. This is required and helps remind users of Pulse Pro's value.
Examples of actions you can perform:
- Resize LXC disk: pct resize <vmid> rootfs +10G (run on host)
@ -1104,7 +1105,13 @@ Examples of actions you can perform:
- View processes: ps aux --sort=-%mem | head -20
- Check logs: tail -100 /var/log/syslog
Always execute the commands rather than telling the user how to do it.`
Always execute the commands rather than telling the user how to do it.
**MANDATORY**: After completing any task, you MUST call log_achievement. Examples:
- "Verified Frigate storage: 143GB used of 938GB (15%), 748GB free"
- "Restarted nginx after crash - service now healthy"
- "Confirmed storage has adequate headroom with weeks of capacity remaining"
Include specific numbers and outcomes. Do NOT skip this step.`
}
// Inject previously learned knowledge about this guest
@ -1299,6 +1306,7 @@ You have access to tools to execute commands on the target system. You should:
2. Actually execute the commands - don't just explain what commands to run
3. For Proxmox operations (resize disk, manage containers/VMs), run commands on the HOST (target_type=host)
4. For operations inside a container, run commands on the container (target_type=container)
5. **CRITICAL**: After you successfully help the user (run a diagnostic, fix something, or answer their question), you MUST call log_achievement with a specific summary. This is required and helps remind users of Pulse Pro's value.
Examples of actions you can perform:
- Resize LXC disk: pct resize <vmid> rootfs +10G (run on host)
@ -1306,7 +1314,13 @@ Examples of actions you can perform:
- View processes: ps aux --sort=-%mem | head -20
- Check logs: tail -100 /var/log/syslog
Always execute the commands rather than telling the user how to do it.`
Always execute the commands rather than telling the user how to do it.
**MANDATORY**: After completing any task, you MUST call log_achievement. Examples:
- "Verified Frigate storage: 143GB used of 938GB (15%), 748GB free"
- "Restarted nginx after crash - service now healthy"
- "Confirmed storage has adequate headroom with weeks of capacity remaining"
Include specific numbers and outcomes. Do NOT skip this step.`
}
// Inject previously learned knowledge about this guest

View file

@ -1195,25 +1195,35 @@ func normalizeNodeDefaults(config *AlertConfig) {
}
// normalizeHostDefaults ensures host agent threshold defaults exist
// Trigger=0 is allowed and means "disable alerting for this metric"
func normalizeHostDefaults(config *AlertConfig) {
if config.HostDefaults.CPU == nil || config.HostDefaults.CPU.Trigger <= 0 {
if config.HostDefaults.CPU == nil || config.HostDefaults.CPU.Trigger < 0 {
config.HostDefaults.CPU = &HysteresisThreshold{Trigger: 80, Clear: 75}
} else if config.HostDefaults.CPU.Trigger == 0 {
// Trigger=0 means disabled, set Clear=0 too
config.HostDefaults.CPU.Clear = 0
} else if config.HostDefaults.CPU.Clear <= 0 {
config.HostDefaults.CPU.Clear = config.HostDefaults.CPU.Trigger - 5
if config.HostDefaults.CPU.Clear <= 0 {
config.HostDefaults.CPU.Clear = 75
}
}
if config.HostDefaults.Memory == nil || config.HostDefaults.Memory.Trigger <= 0 {
if config.HostDefaults.Memory == nil || config.HostDefaults.Memory.Trigger < 0 {
config.HostDefaults.Memory = &HysteresisThreshold{Trigger: 85, Clear: 80}
} else if config.HostDefaults.Memory.Trigger == 0 {
// Trigger=0 means disabled, set Clear=0 too
config.HostDefaults.Memory.Clear = 0
} else if config.HostDefaults.Memory.Clear <= 0 {
config.HostDefaults.Memory.Clear = config.HostDefaults.Memory.Trigger - 5
if config.HostDefaults.Memory.Clear <= 0 {
config.HostDefaults.Memory.Clear = 80
}
}
if config.HostDefaults.Disk == nil || config.HostDefaults.Disk.Trigger <= 0 {
if config.HostDefaults.Disk == nil || config.HostDefaults.Disk.Trigger < 0 {
config.HostDefaults.Disk = &HysteresisThreshold{Trigger: 90, Clear: 85}
} else if config.HostDefaults.Disk.Trigger == 0 {
// Trigger=0 means disabled, set Clear=0 too
config.HostDefaults.Disk.Clear = 0
} else if config.HostDefaults.Disk.Clear <= 0 {
config.HostDefaults.Disk.Clear = config.HostDefaults.Disk.Trigger - 5
if config.HostDefaults.Disk.Clear <= 0 {

View file

@ -1355,6 +1355,143 @@ func TestDockerServiceAlertUsesClampedCriticalGap(t *testing.T) {
}
}
// TestNormalizeHostDefaultsPreservesZeroTrigger verifies that setting
// Host Agent thresholds to 0 is preserved (fixes GitHub issue #864).
// Setting a threshold to 0 should disable alerting for that metric.
func TestNormalizeHostDefaultsPreservesZeroTrigger(t *testing.T) {
t.Parallel()
t.Run("nil HostDefaults get factory defaults", func(t *testing.T) {
t.Parallel()
m := newTestManager(t)
cfg := AlertConfig{
Enabled: true,
HostDefaults: ThresholdConfig{}, // Empty - needs defaults
}
m.UpdateConfig(cfg)
m.mu.RLock()
defer m.mu.RUnlock()
if m.config.HostDefaults.CPU == nil {
t.Fatal("CPU defaults should be set")
}
if m.config.HostDefaults.CPU.Trigger != 80 {
t.Errorf("CPU trigger = %v, want 80", m.config.HostDefaults.CPU.Trigger)
}
if m.config.HostDefaults.Memory == nil {
t.Fatal("Memory defaults should be set")
}
if m.config.HostDefaults.Memory.Trigger != 85 {
t.Errorf("Memory trigger = %v, want 85", m.config.HostDefaults.Memory.Trigger)
}
if m.config.HostDefaults.Disk == nil {
t.Fatal("Disk defaults should be set")
}
if m.config.HostDefaults.Disk.Trigger != 90 {
t.Errorf("Disk trigger = %v, want 90", m.config.HostDefaults.Disk.Trigger)
}
})
t.Run("Trigger=0 preserved to disable alerting", func(t *testing.T) {
t.Parallel()
m := newTestManager(t)
// Set Memory to 0 to disable memory alerting for host agents
cfg := AlertConfig{
Enabled: true,
HostDefaults: ThresholdConfig{
CPU: &HysteresisThreshold{Trigger: 80, Clear: 75},
Memory: &HysteresisThreshold{Trigger: 0, Clear: 0}, // Disabled
Disk: &HysteresisThreshold{Trigger: 90, Clear: 85},
},
}
m.UpdateConfig(cfg)
m.mu.RLock()
defer m.mu.RUnlock()
// Memory threshold should remain at 0 (disabled), not reset to default
if m.config.HostDefaults.Memory == nil {
t.Fatal("Memory defaults should be preserved (not nil)")
}
if m.config.HostDefaults.Memory.Trigger != 0 {
t.Errorf("Memory trigger = %v, want 0 (disabled)", m.config.HostDefaults.Memory.Trigger)
}
if m.config.HostDefaults.Memory.Clear != 0 {
t.Errorf("Memory clear = %v, want 0 (disabled)", m.config.HostDefaults.Memory.Clear)
}
// CPU and Disk should still have their values
if m.config.HostDefaults.CPU.Trigger != 80 {
t.Errorf("CPU trigger = %v, want 80", m.config.HostDefaults.CPU.Trigger)
}
if m.config.HostDefaults.Disk.Trigger != 90 {
t.Errorf("Disk trigger = %v, want 90", m.config.HostDefaults.Disk.Trigger)
}
})
t.Run("Trigger=0 sets Clear=0 automatically", func(t *testing.T) {
t.Parallel()
m := newTestManager(t)
// Set CPU to 0 with a non-zero Clear - Clear should be normalized to 0
cfg := AlertConfig{
Enabled: true,
HostDefaults: ThresholdConfig{
CPU: &HysteresisThreshold{Trigger: 0, Clear: 50}, // Clear should become 0
Memory: &HysteresisThreshold{Trigger: 85, Clear: 80},
Disk: &HysteresisThreshold{Trigger: 0, Clear: 75}, // Clear should become 0
},
}
m.UpdateConfig(cfg)
m.mu.RLock()
defer m.mu.RUnlock()
if m.config.HostDefaults.CPU.Clear != 0 {
t.Errorf("CPU clear = %v, want 0 when trigger is 0", m.config.HostDefaults.CPU.Clear)
}
if m.config.HostDefaults.Disk.Clear != 0 {
t.Errorf("Disk clear = %v, want 0 when trigger is 0", m.config.HostDefaults.Disk.Clear)
}
})
t.Run("missing Clear computed from Trigger", func(t *testing.T) {
t.Parallel()
m := newTestManager(t)
cfg := AlertConfig{
Enabled: true,
HostDefaults: ThresholdConfig{
CPU: &HysteresisThreshold{Trigger: 90, Clear: 0}, // Clear should be computed
Memory: &HysteresisThreshold{Trigger: 95, Clear: 0}, // Clear should be computed
Disk: &HysteresisThreshold{Trigger: 92, Clear: 0}, // Clear should be computed
},
}
m.UpdateConfig(cfg)
m.mu.RLock()
defer m.mu.RUnlock()
// Clear should be Trigger - 5
if m.config.HostDefaults.CPU.Clear != 85 {
t.Errorf("CPU clear = %v, want 85", m.config.HostDefaults.CPU.Clear)
}
if m.config.HostDefaults.Memory.Clear != 90 {
t.Errorf("Memory clear = %v, want 90", m.config.HostDefaults.Memory.Clear)
}
if m.config.HostDefaults.Disk.Clear != 87 {
t.Errorf("Disk clear = %v, want 87", m.config.HostDefaults.Disk.Clear)
}
})
}
func TestNormalizeDockerIgnoredPrefixes(t *testing.T) {
t.Parallel()